Sentinel预热限流器深度解析
·
WarmUpRateLimiterController是 Sentinel(阿里巴巴开源的流量控制组件)中的一种 WarmUp + Rate Limiter 混合限流控制器,它继承自 WarmUpController,并在此基础上增加了 请求排队等待(Rate Limiter) 的能力。
一、整体目标
这个类的作用是:
- 在系统刚启动或低负载时,限制请求速率逐渐提升(Warm Up 预热阶段);
- 同时支持 请求排队等待:如果当前无法立即通过,可以等待一段时间(不超过
timeoutInMs),超时则拒绝。
这结合了两种策略:
- Warm Up(冷启动保护):防止系统在冷启动时被突发流量打垮;
- Rate Limiter(匀速排队):让请求匀速通过,避免瞬间高峰。
二、关键字段解释
private final int timeoutInMs; // 最大等待时间(毫秒)
private final AtomicLong latestPassedTime = new AtomicLong(-1); // 上一个请求通过的时间戳
latestPassedTime用于记录上一次允许通过请求的时间点,从而计算下一个请求最早能通过的时间。timeoutInMs是最大容忍等待时间,超过就直接拒绝。
三、核心方法 canPass
这是限流判断的核心逻辑,我们分段解析:
1. 同步令牌(基于 QPS)
long previousQps = (long) node.previousPassQps();
syncToken(previousQps);
syncToken是父类WarmUpController的方法,根据历史 QPS 更新当前可用的令牌数storedTokens。- 这个过程模拟了“令牌桶”机制,但结合了 Warm Up 的斜率变化(冷启动时生成令牌慢)。
2. 计算当前请求所需时间(costTime)
long restToken = storedTokens.get();
if (restToken >= warningToken) {
// 处于预热阶段(冷启动)
long aboveToken = restToken - warningToken;
double warmingQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
costTime = Math.round(1.0 * acquireCount / warmingQps * 1000);
} else {
// 已度过预热期,以稳定速率处理
costTime = Math.round(1.0 * acquireCount / count * 1000);
}
count:配置的 QPS 阈值(比如 10 QPS → 每个请求间隔 100ms)。warningToken和slope是 WarmUpController 中用于控制预热曲线的参数。- 在预热阶段,实际允许的 QPS 小于
count,所以costTime更长(即请求间隔更大)。 acquireCount表示本次请求需要消耗多少“许可”(通常为 1)。
✅ 本质:根据当前系统处于预热阶段还是稳定阶段,动态计算处理
acquireCount个请求所需的毫秒数。
3. 判断是否能立即通过
expectedTime = costTime + latestPassedTime.get();
if (expectedTime <= currentTime) {
latestPassedTime.set(currentTime);
return true;
}
- 如果上一个请求通过时间 + 本次所需间隔 ≤ 当前时间 → 可以立即通过。
- 更新
latestPassedTime为当前时间(注意:这里用的是当前时间,不是 expectedTime,是为了避免累积误差?但可能有争议)。
4. 如果不能立即通过,尝试等待(Rate Limiter 行为)
long waitTime = costTime + latestPassedTime.get() - currentTime;
if (waitTime > timeoutInMs) {
return false; // 等待太久,直接拒绝
}
- 先粗略判断是否值得等。
然后:
long oldTime = latestPassedTime.addAndGet(costTime);
- 原子地将
latestPassedTime增加costTime,表示“预定”了下一个通过时间。 oldTime是更新后的值(即本次请求计划通过的时间)。
再检查:
waitTime = oldTime - TimeUtil.currentTimeMillis();
if (waitTime > timeoutInMs) {
latestPassedTime.addAndGet(-costTime); // 回滚
return false;
}
- 再次确认当前是否还能在 timeout 内等到。
- 如果不能,回滚(减去刚才加的
costTime),拒绝请求。
最后:
if (waitTime > 0) {
Thread.sleep(waitTime);
}
return true;
- 真正 sleep 等待,实现“匀速排队”。
- 注意:sleep 可能被中断,此时 catch 后返回 false。
四、举个例子 🌰
假设:
count = 5 QPS→ 稳定时每 200ms 一个请求;warmUpPeriodSec = 10s,coldFactor = 3→ 预热期间最慢 1/3 QPS(约 1.67 QPS,600ms 间隔);timeoutInMs = 500ms。
场景:
- 系统刚启动,处于预热阶段。
- 第一个请求:
latestPassedTime = -1→expectedTime = costTime + (-1)很小,肯定 ≤ 当前时间 → 立即通过,设latestPassedTime = now。 - 第二个请求在 100ms 后到来:
- 预热阶段,
costTime ≈ 600ms expectedTime = now + 600ms > currentTime→ 需要等待。waitTime = 500ms(刚好等于 timeout)→ 允许等待。- 调用
Thread.sleep(500),睡完后通过。
- 预热阶段,
- 如果第三个请求在 200ms 后又来:
- 此时
latestPassedTime已经是第一个请求时间 + 600ms - 新请求还需等 600ms,但当前只过了 200ms → 还需等 400ms
- 如果
timeoutInMs = 300ms→ 超时,拒绝。
- 此时
五、设计亮点 & 注意事项
✅ 优点:
- 结合 Warm Up 和 Rate Limiter,既防冷启动冲击,又平滑流量。
- 使用
AtomicLong保证并发安全。 - 支持
prioritized参数(虽然当前未使用,但预留扩展)。
⚠️ 注意:
Thread.sleep()会阻塞线程!在高并发 Web 场景下慎用(可能导致线程池耗尽)。- Sentinel 通常建议在 异步/非阻塞 场景下使用此模式,或配合响应式编程。
latestPassedTime.set(currentTime)在立即通过时使用当前时间而非expectedTime,可能导致令牌生成略微“加速”,但影响不大。
六、总结
WarmUpRateLimiterController 实现了一种 带预热的匀速排队限流器:
| 阶段 | 行为 |
|---|---|
| 预热期 | 请求间隔较长(QPS 低),随时间逐渐缩短 |
| 稳定期 | 请求按固定间隔(1/count 秒)通过 |
| 超过速率 | 请求排队等待,最多等 timeoutInMs,超时拒绝 |
适用于对 系统稳定性要求高、且能接受 少量延迟 的场景,如数据库写入、支付接口等。
如果你有具体疑问(比如某行代码、为什么用 Math.nextUp、slope 怎么算等),欢迎继续问!
源码如下
public class WarmUpRateLimiterController extends WarmUpController {
private final int timeoutInMs;
private final AtomicLong latestPassedTime = new AtomicLong(-1);
public WarmUpRateLimiterController(double count, int warmUpPeriodSec, int timeOutMs, int coldFactor) {
super(count, warmUpPeriodSec, coldFactor);
this.timeoutInMs = timeOutMs;
}
@Override
public boolean canPass(Node node, int acquireCount) {
return canPass(node, acquireCount, false);
}
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
long previousQps = (long) node.previousPassQps();
syncToken(previousQps);
long currentTime = TimeUtil.currentTimeMillis();
long restToken = storedTokens.get();
long costTime = 0;
long expectedTime = 0;
if (restToken >= warningToken) {
long aboveToken = restToken - warningToken;
// current interval = restToken*slope+1/count
double warmingQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
costTime = Math.round(1.0 * (acquireCount) / warmingQps * 1000);
} else {
costTime = Math.round(1.0 * (acquireCount) / count * 1000);
}
expectedTime = costTime + latestPassedTime.get();
if (expectedTime <= currentTime) {
latestPassedTime.set(currentTime);
return true;
} else {
long waitTime = costTime + latestPassedTime.get() - currentTime;
if (waitTime > timeoutInMs) {
return false;
} else {
long oldTime = latestPassedTime.addAndGet(costTime);
try {
waitTime = oldTime - TimeUtil.currentTimeMillis();
if (waitTime > timeoutInMs) {
latestPassedTime.addAndGet(-costTime);
return false;
}
if (waitTime > 0) {
Thread.sleep(waitTime);
}
return true;
} catch (InterruptedException e) {
}
}
}
return false;
}
}
更多推荐
所有评论(0)