WarmUpRateLimiterController是 Sentinel(阿里巴巴开源的流量控制组件)中的一种 WarmUp + Rate Limiter 混合限流控制器,它继承自 WarmUpController,并在此基础上增加了 请求排队等待(Rate Limiter) 的能力。


一、整体目标

这个类的作用是:

  • 在系统刚启动或低负载时,限制请求速率逐渐提升(Warm Up 预热阶段);
  • 同时支持 请求排队等待:如果当前无法立即通过,可以等待一段时间(不超过 timeoutInMs),超时则拒绝。

这结合了两种策略:

  1. Warm Up(冷启动保护):防止系统在冷启动时被突发流量打垮;
  2. 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。

场景:

  1. 系统刚启动,处于预热阶段。
  2. 第一个请求:latestPassedTime = -1 → expectedTime = costTime + (-1) 很小,肯定 ≤ 当前时间 → 立即通过,设 latestPassedTime = now。
  3. 第二个请求在 100ms 后到来:
    • 预热阶段,costTime ≈ 600ms
    • expectedTime = now + 600ms > currentTime → 需要等待。
    • waitTime = 500ms(刚好等于 timeout)→ 允许等待。
    • 调用 Thread.sleep(500),睡完后通过。
  4. 如果第三个请求在 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;
    }
}
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐