Sentinel 中的 WarmUpController ,用于实现 "预热/冷启动"限流(Warm Up / Cold Start) 。它的核心思想来源于 Google Guava 的 RateLimiter 中的 SmoothWarmingUp 模型 ,但 Sentinel 做了适配和改造,更贴近 令牌桶(Token Bucket) 的语义。
下面我将结合注释、代码逻辑和实际场景,帮你彻底理解它。
🎯 一、设计目标:解决什么问题?
系统刚启动时处理能力弱(如 DB 连接未建立、JIT 未优化、缓存未预热),不能立即承受满负荷流量。
- 如果一上来就允许最大 QPS(比如 1000),系统可能直接被打垮;
- 所以需要 "慢慢加热":开始只允许少量请求,随着时间推移,逐步提升到最大 QPS。
✅ 这就是 Warm Up(预热)限流 的意义。
🔑 二、核心思想:用"令牌桶"模拟系统热度
Sentinel 把 系统当前的"冷热程度" 映射为 令牌桶中的令牌数量(storedTokens):
| 令牌数量 | 系统状态 | 允许的 QPS |
|---|---|---|
0 ~ warningToken |
热区(Hot) | 最大 QPS = count |
warningToken ~ maxToken |
冷区(Cold) | QPS < count,越冷越小 |
maxToken |
最冷(刚启动) | QPS ≈ count / coldFactor |
💡 令牌越多,系统越"冷";令牌越少,系统越"热" ------ 这和直觉相反,但很巧妙!
为什么?因为:
- 刚启动时,没有请求进来 → 令牌不断累积 →
storedTokens趋近maxToken→ 系统判定为"冷"; - 随着请求持续进入,消耗令牌 →
storedTokens下降 → 系统变"热" → 允许更高 QPS。
⚙️ 三、关键参数解析
java
double count; // 稳定状态下的最大 QPS(如 100)
int coldFactor; // 冷却因子,默认 3 → 最冷时 QPS = count / 3
int warningToken; // "警戒线"令牌数(冷热分界点)
int maxToken; // 令牌桶最大容量
double slope; // 冷区 QPS 随令牌数变化的斜率
初始化逻辑(construct 方法)
java
// 警戒线:warmUpPeriod 内以最大 QPS 能处理的请求数的 1/(coldFactor-1)
warningToken = (warmUpPeriodInSec * count) / (coldFactor - 1);
// 最大令牌数 = warningToken + 额外冷区容量
maxToken = warningToken + (2 * warmUpPeriodInSec * count) / (1.0 + coldFactor);
// 斜率:决定冷区 QPS 下降速度
slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
举个例子:
count = 100(稳定 QPS=100)warmUpPeriodInSec = 10(预热 10 秒)coldFactor = 3
则:
warningToken = (10 * 100) / (3 - 1) = 500maxToken = 500 + (2 * 10 * 100) / (1 + 3) = 500 + 500 = 1000slope = (3 - 1) / 100 / (1000 - 500) = 2 / 100 / 500 = 0.00004
⏱️ 四、令牌同步机制:syncToken()
每秒更新一次令牌桶状态:
java
protected void syncToken(long passQps) {
long currentTime = TimeUtil.currentTimeMillis();
currentTime = currentTime - currentTime % 1000; // 对齐到秒
if (currentTime <= lastFilledTime.get()) return;
long oldValue = storedTokens.get();
long newValue = coolDownTokens(currentTime, passQps); // 计算新增令牌
if (storedTokens.compareAndSet(oldValue, newValue)) {
// 扣除本秒已通过的请求(消耗令牌)
long currentValue = storedTokens.addAndGet(0 - passQps);
if (currentValue < 0) storedTokens.set(0);
lastFilledTime.set(currentTime);
}
}
coolDownTokens():如何加令牌?
java
private long coolDownTokens(long currentTime, long passQps) {
long oldValue = storedTokens.get();
long delta = (currentTime - lastFilledTime.get()) * count / 1000; // 按最大速率加
if (oldValue < warningToken) {
// 热区:正常加令牌
return Math.min(oldValue + delta, maxToken);
} else if (oldValue > warningToken) {
// 冷区:只有当当前 QPS 很低(< count/coldFactor)时才加令牌
if (passQps < (int)count / coldFactor) {
return Math.min(oldValue + delta, maxToken);
}
}
return oldValue;
}
✅ 冷区保护:如果系统已经在高负载(QPS > count/coldFactor),就不继续加令牌,防止"虚假冷却"。
🧮 五、限流判断逻辑:canPass()
java
long restToken = storedTokens.get();
if (restToken >= warningToken) {
// 冷区:计算当前允许的 QPS(低于 count)
long aboveToken = restToken - warningToken;
double interval = aboveToken * slope + 1.0 / count; // 请求间隔(秒)
double warningQps = 1.0 / interval;
if (passQps + acquireCount <= warningQps) {
return true;
}
} else {
// 热区:直接用最大 QPS 判断
if (passQps + acquireCount <= count) {
return true;
}
}
return false;
关键公式(来自 Guava):
在冷区,请求间隔 = aboveToken * slope + stableInterval
→ 允许 QPS = 1 / 请求间隔
aboveToken越大(越冷)→ 间隔越大 → QPS 越小;- 当
aboveToken = 0(刚好到警戒线)→ QPS =count; - 当
aboveToken = maxToken - warningToken→ QPS ≈count / coldFactor。
📈 六、QPS 随时间变化曲线
假设 count=100, coldFactor=3, warmUp=10s:
| 时间 | 令牌数 | 允许 QPS |
|---|---|---|
| t=0s | 1000 | ~33 |
| t=5s | 750 | ~50 |
| t=10s | 500 | 100 |
| t>10s | <500 | 100 |
曲线是 线性上升(Guava 默认是梯形,Sentinel 简化为线性)。
❓ 七、常见疑问解答
Q1: 为什么令牌越多系统越"冷"?
- 因为 令牌是"未被消耗的能力"。
- 刚启动时没请求 → 能力堆积 → 令牌多 → 系统"冷";
- 稳定运行时请求持续 → 能力被消耗 → 令牌少 → 系统"热"。
Q2: 和 ThrottlingController 有什么区别?
| 控制器 | 行为 | 并发模型 | 适用场景 |
|---|---|---|---|
ThrottlingController |
匀速排队,阻塞等待 | 同步阻塞 | 需要严格匀速 |
WarmUpController |
动态调整 QPS 上限 | 非阻塞,直接拒绝 | 冷启动保护 |
Q3: prioritized 参数有用吗?
- 在这个类中 没有使用,仅为接口兼容。
✅ 八、总结:设计精髓
用令牌桶的"剩余令牌数"表征系统冷热状态,动态调整 QPS 上限,实现平滑预热。
- ✅ 冷启动保护:避免系统刚启动被打垮;
- ✅ 自适应调整:根据实际 QPS 动态加令牌;
- ✅ 线性模型:简单高效,易于理解和配置;
- ✅ 非阻塞:超限直接拒绝,不阻塞线程。
🛠️ 使用示例
java
// 100 QPS,预热 10 秒,冷因子 3(最冷时 33 QPS)
FlowRule rule = new FlowRule("myResource")
.setGrade(RuleConstant.FLOW_GRADE_QPS)
.setCount(100)
.setWarmUpPeriodSec(10)
.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
FlowRuleManager.loadRules(Collections.singletonList(rule));
系统启动后,QPS 会从 33 逐步上升到 100,10 秒后达到稳定。
希望这个解释让你彻底理解了 WarmUpController!如果还有疑问,欢迎继续问 😊
源码如下
java
/**
* WarmUpController 是一个流量控制控制器,用于实现预热(Warm Up)限流策略。
* 它通过逐步增加允许的请求速率来应对突发流量,避免系统在冷启动时被瞬间高负载冲击。
*
* 该类使用令牌桶算法,并根据当前存储的令牌数量动态调整请求通过的阈值。
*/
public class WarmUpController implements TrafficShapingController {
/**
* 基础的每秒请求数限制(稳定状态下的最大QPS)
*/
protected double count;
/**
* 冷却因子,表示冷却状态下速率与正常速率的比例关系。必须大于1。
*/
private int coldFactor;
/**
* 警戒令牌数,当剩余令牌超过此值时进入预热阶段
*/
protected int warningToken = 0;
/**
* 最大令牌数,即令牌桶的最大容量
*/
private int maxToken;
/**
* 预热阶段的斜率,用于计算当前可接受的QPS
*/
protected double slope;
/**
* 存储当前可用令牌数的原子变量
*/
protected AtomicLong storedTokens = new AtomicLong(0);
/**
* 上次填充令牌的时间戳(毫秒级)
*/
protected AtomicLong lastFilledTime = new AtomicLong(0);
/**
* 构造方法:初始化 WarmUpController 实例
*
* @param count 稳定状态下的最大QPS
* @param warmUpPeriodInSec 预热期持续时间(单位:秒)
* @param coldFactor 冷却因子,决定冷却状态下的速率比例,必须大于1
*/
public WarmUpController(double count, int warmUpPeriodInSec, int coldFactor) {
construct(count, warmUpPeriodInSec, coldFactor);
}
/**
* 构造方法:初始化 WarmUpController 实例,默认冷却因子为3
*
* @param count 稳定状态下的最大QPS
* @param warmUpPeriodInSec 预热期持续时间(单位:秒)
*/
public WarmUpController(double count, int warmUpPeriodInSec) {
construct(count, warmUpPeriodInSec, 3);
}
/**
* 初始化控制器的核心参数,包括警戒令牌、最大令牌和斜率等
*
* @param count 稳定状态下的最大QPS
* @param warmUpPeriodInSec 预热期持续时间(单位:秒)
* @param coldFactor 冷却因子,必须大于1
*/
private void construct(double count, int warmUpPeriodInSec, int coldFactor) {
if (coldFactor <= 1) {
throw new IllegalArgumentException("Cold factor should be larger than 1");
}
this.count = count;
this.coldFactor = coldFactor;
// 计算警戒令牌数:thresholdPermits = 0.5 * warmupPeriod / stableInterval.
warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
// 计算最大令牌数:maxPermits = thresholdPermits + 2 * warmupPeriod / (stableInterval + coldInterval)
maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));
// 计算预热阶段的斜率
slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
}
/**
* 判断是否可以通过本次请求(默认非优先处理)
*
* @param node 当前节点信息
* @param acquireCount 请求获取的令牌数量
* @return 是否允许通过
*/
@Override
public boolean canPass(Node node, int acquireCount) {
return canPass(node, acquireCount, false);
}
/**
* 判断是否可以通过本次请求
*
* @param node 当前节点信息
* @param acquireCount 请求获取的令牌数量
* @param prioritized 是否是优先请求
* @return 是否允许通过
*/
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
long passQps = (long) node.passQps();
long previousQps = (long) node.previousPassQps();
// 同步令牌桶中的令牌数量
syncToken(previousQps);
// 获取当前剩余令牌数
long restToken = storedTokens.get();
// 如果剩余令牌超过警戒线,则进入预热逻辑
if (restToken >= warningToken) {
long aboveToken = restToken - warningToken;
// 根据当前超出警戒线的令牌数计算当前允许的最大QPS
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
// 若当前已通过请求数加上申请数不超过当前允许的最大QPS,则放行
if (passQps + acquireCount <= warningQps) {
return true;
}
} else {
// 否则按照基础QPS进行判断
if (passQps + acquireCount <= count) {
return true;
}
}
return false;
}
/**
* 同步并更新令牌桶中存储的令牌数量
*
* @param passQps 上一周期的实际通过QPS
*/
protected void syncToken(long passQps) {
long currentTime = TimeUtil.currentTimeMillis();
currentTime = currentTime - currentTime % 1000; // 对齐到秒级别
long oldLastFillTime = lastFilledTime.get();
if (currentTime <= oldLastFillTime) {
return;
}
long oldValue = storedTokens.get();
long newValue = coolDownTokens(currentTime, passQps); // 计算新的令牌数
// 使用CAS操作更新令牌数和最后填充时间
if (storedTokens.compareAndSet(oldValue, newValue)) {
long currentValue = storedTokens.addAndGet(0 - passQps); // 减去本次消耗的令牌
if (currentValue < 0) {
storedTokens.set(0L); // 不允许负值
}
lastFilledTime.set(currentTime);
}
}
/**
* 在冷却期间计算应新增的令牌数量
*
* @param currentTime 当前时间戳(毫秒)
* @param passQps 上一周期实际通过的QPS
* @return 新增后的令牌总数(不超过最大令牌数)
*/
private long coolDownTokens(long currentTime, long passQps) {
long oldValue = storedTokens.get();
long newValue = oldValue;
// 如果当前令牌数小于警戒线,则按固定速率添加令牌
if (oldValue < warningToken) {
newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
} else if (oldValue > warningToken) {
// 如果当前令牌数大于警戒线且上一周期QPS较低,则继续添加令牌
if (passQps < (int)count / coldFactor) {
newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
}
}
return Math.min(newValue, maxToken); // 返回不超过最大令牌数的新值
}
}