Sentinel预热限流:WarmUpController原理详解

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) = 500
  • maxToken = 500 + (2 * 10 * 100) / (1 + 3) = 500 + 500 = 1000
  • slope = (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); // 返回不超过最大令牌数的新值
    }

}
相关推荐
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ3 小时前
SSE技术详解及应用场景
java
2501_941982053 小时前
RPA 赋能企业微信外部群:多群同步操作的技术实现
java·开发语言
Seven973 小时前
剑指offer-49、把字符串转换成整数
java
编程修仙3 小时前
第六篇 HttpServletRequest对象
java·spring boot·后端
Lio n J3 小时前
基于SpringBoot常用脱敏方案
spring boot·spring·mvc·状态模式
杀死那个蝈坦3 小时前
微服务网关(Spring Cloud Gateway)实战攻略
java·微服务·架构
lang201509283 小时前
Sentinel流量整形控制器全解析
sentinel
凌云若寒3 小时前
半导体标签打印的核心痛点分析
java
灰乌鸦乌卡3 小时前
泛微OA集成档案信息包生成
java