← 返回博客
2026-08-26 08:00:02

Sentinel源码解析:滑动窗口与规则引擎

Sentinel源码解析:滑动窗口与规则引擎

场景:一个接口被刷爆了

生产环境有个订单接口,平时QPS 200,某天凌晨被脚本刷到2000,CPU直接飙红。限流阈值设的500,但Sentinel没拦住——因为阈值配的是QPS,而脚本是短时间脉冲式请求,1秒内冲到2000,Sentinel的统计窗口还没反应过来就放过去了。

这就是Sentinel滑动窗口要解决的核心问题:如何在毫秒级感知流量突变,而不是等统计周期结束才反应过来。

滑动窗口:不是"一整块"时间,是"切碎的"时间

Sentinel的统计结构是MetricBucket,每个bucket存1秒内的通过数、拒绝数、耗时等指标。LeapArray是核心容器,它维护一个环形数组,每个元素是一个bucket。

// LeapArray的核心:根据当前时间计算应该在哪个槽位
public int calculateTimeIdx(long timeMillis) {
    long timeId = timeMillis / windowLengthInMs;
    return (int)(timeId % array.length());
}

关键点:

问题来了:如果窗口是固定的,那1秒内的前500ms和后500ms是"两个世界"。QPS=2000如果集中在第400ms~600ms,前500ms窗口只统计到400ms的流量,后500ms窗口统计到600ms的流量,每个窗口都不到500,但1秒内实际是2000。

Sentinel的解法:当前时间戳取模后,如果发现槽位已经过期,就重置bucket再写入

public WindowWrap<T> currentWindow(long timeMillis) {
    int idx = calculateTimeIdx(timeMillis);
    long windowStart = timeMillis - timeMillis % windowLengthInMs;
    // 循环获取,如果为空就创建,如果过期就重置
    while (true) {
        WindowWrap<T> old = array.get(idx);
        if (old == null) {
            // CAS创建新窗口
        } else if (windowStart == old.windowStart()) {
            return old;
        } else if (windowStart > old.windowStart()) {
            // 窗口过期,重置
            if (updateLock.tryLock()) {
                old.resetTo(windowStart);
                return old;
            }
        }
    }
}

这里的resetTo是性能关键点:不是new一个新bucket,而是复用旧对象,重置时间戳和计数。避免了频繁GC。

规则引擎:从FlowRuleCheckerTracer的链式调用

限流规则不是直接写在FlowRule里的。Sentinel的规则引擎分三层:

  1. ProcessorSlotChain:责任链模式,每个slot负责一件事。FlowSlot负责限流,DegradeSlot负责熔断,StatisticSlot负责统计。
  2. FlowRuleChecker:对每个通过的请求,检查是否触发规则。
  3. Tracer:记录异常,供熔断判断。
// FlowSlot的entry方法
public void entry(Context context, ResourceWrapper resourceWrapper, ...) {
    FlowRuleChecker.checkFlow(resourceWrapper, context, node, count, prioritized);
    // 检查通过才放行
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

核心逻辑在FlowRuleChecker.canPassCheck

public boolean canPassCheck(/* ... */) {
    // 遍历所有规则
    for (FlowRule rule : rules) {
        if (!canPassByParam(rule, context, node, count)) {
            return false;
        }
    }
    return true;
}

每个FlowRule内部有一个TrafficShapingController,这是个策略接口。不同限流算法对应不同实现:

为什么这样设计? 把"统计"和"判断"解耦。统计是StatisticSlot的事,判断是FlowRuleChecker的事,规则算法是TrafficShapingController的事。你要加一种新限流算法,只需要实现TrafficShapingController,不用碰统计链路。

熔断:状态机+时间窗口

熔断比限流复杂一点。限流是"超了就拒",熔断是"失败多了就断开,过一会儿再试"。

Sentinel的DegradeSlot维护一个CircuitBreaker状态机:

CLOSED -> OPEN -> HALF_OPEN -> CLOSED

核心实现是ExceptionCircuitBreaker,它内部用LeapArray统计"最近N秒的异常比例":

public boolean tryPassCurrentNode() {
    // 从统计窗口拿最近的数据
    double exceptionRatio = getExceptionRatio();
    if (exceptionRatio < maxAllowedRatio) {
        return true; // 未超过阈值,放行
    }
    // 超过阈值,进入OPEN状态
    return false;
}

熔断状态切换的触发点:不是每次请求都判断状态,而是DegradeSlot在entry时先检查状态,OPEN直接拒绝,CLOSED则记录异常并判断是否要切换到OPEN。

踩过的坑

  1. 窗口大小和阈值要匹配。如果阈值是500 QPS,窗口是1000ms,那500ms内的脉冲流量是测不出来的。把windowLengthInMs调小到200ms,会有更细粒度统计,但代价是更多bucket,GC压力变大。
  2. 规则热更新。Sentinel的规则存在RuleManager里,用volatile引用+CopyOnWrite更新。但FlowRuleChecker每次检查都遍历规则列表,如果规则很多(几百条),每次请求都遍历会损耗性能。建议规则数量控制在50条以内。
  3. 熔断的恢复时间HALF_OPEN状态下只放一个探针请求,如果这个请求恰好是慢请求(比如超时5秒),那后续所有请求都要等这个探针完成才能判断状态。生产环境把maxWaitTime调小,避免探针阻塞太久。

核心收获

Sentinel的限流熔断本质是"时间窗口+状态机"的组合。滑动窗口解决"怎么统计",规则引擎解决"怎么判断",熔断状态机解决"怎么恢复"。三者解耦,各自独立演进。

下一步可以试:SentinelDashboard的源码。看控制台怎么把规则推送到客户端(RuleManager的加载机制),以及客户端怎么上报统计数据(SentinelTransport)。这会让你理解限流组件在分布式场景下的控制面和数据面分离设计。