Sentinel 核心实现剖析:SlotChain、SPI、限流算法与熔断降级

Sentinel 核心实现剖析:SlotChain、SPI、限流算法与熔断降级

一、SPI 扩展点体系

Sentinel 大量使用 SPI(Service Provider Interface)机制实现组件的动态加载与替换。与 Java 原生 ServiceLoader 不同,Sentinel 自定义了一套更灵活的 SPI 框架,支持按别名加载、优先级排序以及默认实现回退。

核心类为 SpiLoader,入口通过 SpiLoader.of(Class<T> service) 获取指定接口的加载器,然后通过 loadDefault()loadInstance(String alias) 等方法实例化实现。实现类通过 @Spi 注解标记,并可以指定 value 作为别名。若存在多个实现,可以通过 @Spi(order = -100) 控制优先级。

Sentinel 中通过 SPI 加载的关键扩展点包括:

  • SlotChainBuilder:负责构建 ProcessorSlotChain。
  • ProcessorSlot:构成 SlotChain 的单个节点,如 FlowSlotDegradeSlot 等。
  • InitFunc:初始化回调,用于启动时执行一些初始化逻辑(如加载规则数据源)。
  • MetricCallback:监控指标输出回调。
  • CommandCenter:与 Dashboard 通信的命令中心实现。
  • TransportClientFactory:Dashboard 通信的客户端工厂。

示例:通过 SPI 获取默认 SlotChainBuilder 的代码路径:

java 复制代码
SlotChainBuilder builder = SpiLoader.of(SlotChainBuilder.class).loadDefaultInstance();

其中 DefaultSlotChainBuilder 标注了 @Spi(isDefault = true),因此被当作默认实现加载。

二、ProcessorSlotChain 的构建过程

Sentinel 的拦截逻辑由一条责任链(SlotChain)完成。链的构建入口在 CtSph.lookProcessChain() 方法中。对于每个资源首次被访问时,会触发链的创建。

创建流程:

  1. 调用 SlotChainProvider.newSlotChain()
  2. 通过 SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault() 加载 SlotChainBuilder,默认实现为 DefaultSlotChainBuilder
  3. DefaultSlotChainBuilder.build() 方法中,遍历所有通过 SPI 加载的 ProcessorSlot 实现类,并按照 @Spi 注解的 order 升序排列,依次调用 addLast() 构建单向链表。

最终形成的链结构如下(按 order 排序):

复制代码
NodeSelectorSlot -> ClusterBuilderSlot -> LogSlot -> StatisticSlot -> AuthoritySlot -> SystemSlot -> FlowSlot -> DegradeSlot

每个 Slot 的实现类通过 @Spi 标注,并携带 order 值。默认实现分别位于对应的 Slot 类中(如 DefaultNodeSelectorSlot)。每个 Slot 持有下一个 Slot 的引用,entry() 方法内部调用 fireEntry(context, resourceWrapper, node, count, prioritized, args) 传递到下游。

自定义 Slot 可以通过 SPI 方式插入链中,只需实现 ProcessorSlot 接口,使用 @Spi(order = ...) 注解指定位置,并添加到 SPI 配置文件中。

三、StatisticSlot 与滑动窗口统计

StatisticSlot 是负责调用统计的核心 Slot。在 entry() 方法中会调用后续 Slot 链执行业务逻辑,并在调用前后记录指标。其统计基于 Metric 接口,默认实现为 ArrayMetric,内部使用滑动窗口 LeapArray<MetricBucket>

滑动窗口结构:

  • LeapArray 总长度(intervalInMs)默认为 1000ms,划分为 sampleCount 个桶(默认 2 个,每个 500ms)。
  • 每个桶是 WindowWrap<MetricBucket>,包装了一个 MetricBucket 对象。
  • MetricBucket 内部使用 LongAdder 累加 PASS、BLOCK、SUCCESS、EXCEPTION 计数,以及 RT 总和、最小 RT 等。

当前时间所在的桶通过 currentWindow() 获取。如果当前桶过期(时间戳 + 窗口长度 < 当前时间),则重置桶数据并移动到新位置。滑动窗口的"滑动"体现在计算总量时,会累加当前窗口内的所有桶(当前桶和上一个不活动的桶,取决于时间范围),例如要计算过去 1 秒的 QPS,就会累加所有未被淘汰的桶。

MetricBucket 累加通过 addPass(int n) 等方法完成,内部 LongAdder.add(n)。RT 的统计通过 addRt(long rt) 累加,计算平均 RT 时用 rtTotal 除以 successCount

核心代码片段(简化):

java 复制代码
public long pass() {
    data.currentWindow(); // 滚动到当前窗口
    long sum = 0;
    for (WindowWrap<MetricBucket> wrap : data.list()) {
        sum += wrap.value().pass();
    }
    return sum;
}

这种设计的优势是完全无锁(LongAdder),统计开销极低,且支持秒级精度。

四、FlowSlot 与限流算法实现

FlowSlot 负责基于配置的流控规则进行限流。规则通过 FlowRuleManager 加载,支持按资源、流控模式(直接、关联、链路)以及流控效果(快速失败、Warm Up、排队等待)进行判断。

流控核心类为 FlowRuleChecker,它持有多个 TrafficShapingController 的实现类,根据 controlBehavior 选择不同的限流算法:

  • 快速失败:DefaultController ------ 基于滑动窗口的 QPS 或线程数比较。
  • Warm Up:WarmUpController ------ 令牌桶预热算法。
  • 排队等待:RateLimiterController ------ 漏桶算法(精确限速)。

4.1 DefaultController(快速失败)

实现最简单:从 StatisticNode 获取当前 QPS 或线程数,与规则中的 count 阈值比较,若超过则抛出 FlowException。对于 QPS 模式,直接读取 node.passQps();线程数模式读取 node.curThreadNum()

4.2 WarmUpController(预热)

基于 Guava 的 SmoothWarmingUp 思想实现了一个令牌桶。核心参数:

  • count:稳定后的 QPS 阈值。
  • warmUpPeriodSec:预热时间(秒)。
  • coldFactor:冷启动因子,默认为 3,表示初始阈值只有稳定阈值的 1/3。

内部维护 storedTokens(当前令牌数)、lastFilledTime(上次填充时间)。每次请求到来时,先根据时间差补充令牌(速率逐步增加),然后尝试扣除一个令牌。若令牌不足,则直接阻塞(实际上 Sentinel 的实现中预热模式并不支持排队,令牌不足时抛出 FlowException)。

关键代码逻辑(简化):

java 复制代码
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    long currentTime = TimeUtil.currentTimeMillis();
    syncToken(currentTime); // 根据时间填充令牌
    if (storedTokens > warningToken) {
        // 预热期,令牌消耗速率较慢
        long oldStoredTokens = storedTokens;
        storedTokens -= acquireCount;
        if (storedTokens < 0) {
            storedTokens = oldStoredTokens;
            return false;
        }
    } else {
        // 稳定期
        storedTokens -= acquireCount;
        if (storedTokens < 0) {
            return false;
        }
    }
    return true;
}

其中 syncToken 会根据当前时间和预热斜率计算应该补充的令牌数,令牌生成速率从低到高逐渐增加。

4.3 RateLimiterController(排队等待)

使用漏桶思想,通过 latestPassedTime 记录最近一次请求通过的时间。计算下一次请求允许通过的时间:latestPassedTime + costTime(其中 costTime = 1.0 / count * 1000 ms)。如果该时间大于当前时间,说明请求需要等待,若等待时间超过 maxQueueingTimeMs(用户配置的最大排队超时),则拒绝;否则,休眠等待到允许时间后放行,并更新 latestPassedTime

java 复制代码
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    long currentTime = TimeUtil.currentTimeMillis();
    long costTime = Math.round(1.0 / count * 1000); // 每个请求的间隔
    long expectedTime = costTime + latestPassedTime.get();
    if (expectedTime <= currentTime) {
        // 无需等待,直接通过
        latestPassedTime.set(currentTime);
        return true;
    } else {
        long waitTime = costTime + latestPassedTime.get() - currentTime;
        if (waitTime > maxQueueingTimeMs) {
            return false;
        }
        // 等待
        try {
            Thread.sleep(waitTime);
        } catch (InterruptedException e) {
            return false;
        }
        latestPassedTime.addAndGet(costTime);
        return true;
    }
}

注意这里使用了 AtomicLonglatestPassedTime 保证线程安全,但实际可能存在并发竞争,导致实际通过速率略高于配置值。不过 Sentinel 默认容忍这种轻微的超量,认为其在实际场景中可接受。

五、DegradeSlot 与熔断降级实现

DegradeSlot 依赖 DegradeRuleManager 加载的熔断规则。熔断器状态由 CircuitBreaker 接口管理,针对不同的熔断策略有不同的实现:

  • SlowRequestCircuitBreaker:慢调用比例。
  • ExceptionCircuitBreaker:异常比例和异常数(内部通过 LeapArray 统计异常和慢请求)。

每种 CircuitBreaker 都维护了一个状态机(CLOSED、OPEN、HALF-OPEN)。状态转换的逻辑在 tryPass() 方法中:

  • StatisticNode 获取当前滑动窗口内的统计数据。
  • 根据策略判断是否达到熔断阈值(例如异常比例超过 errorRatio)。
  • 如果是 CLOSED 状态且达到阈值,则转为 OPEN,记录熔断开始时间。
  • 如果是 OPEN 状态,检查是否超过 timeWindow,若超过则转为 HALF-OPEN,允许试探请求。
  • HALF-OPEN 状态下,若试探请求失败,重新 OPEN;成功则 CLOSED。

SlowRequestCircuitBreaker 的特殊之处在于,它检查慢请求的比例,而慢请求的判断是在 StatisticSlot 中比较 RT 是否超过 maxAllowedRt 完成的。

代码结构(简化):

java 复制代码
public boolean tryPass(Context context) {
    if (state == State.CLOSED) {
        // 检查是否触发熔断
        if (isOverThreshold()) {
            state = State.OPEN;
            nextRetryTimestamp = TimeUtil.currentTimeMillis() + rule.getTimeWindow() * 1000;
            return false;
        }
        return true;
    } else if (state == State.OPEN) {
        if (TimeUtil.currentTimeMillis() >= nextRetryTimestamp) {
            state = State.HALF_OPEN;
            return true; // 允许一个试探请求
        }
        return false;
    } else { // HALF_OPEN
        // 后续通过探活回调改变状态
        return true;
    }
}

探活成功/失败由 onSuccess/onFailure 方法回调,在 StatisticSlot 中调用。

六、系统自适应保护 SystemSlot

SystemSlot 检查系统级规则,规则由 SystemRuleManager 加载。它基于操作系统的指标(Load、CPU 使用率)、JVM 线程数、平均 RT、入口 QPS 等做出判断。

指标获取方式:

  • Load:通过 OperatingSystemMXBean.getSystemLoadAverage()(仅 Linux 有效)。
  • CPU 使用率:Sentinel 通过 SentinelConfig 自己采集(利用 ManagementFactory.getOperatingSystemMXBean() 和线程睡眠计算相对 CPU 时间)。
  • 平均 RT、入口 QPS:取自全局入口 ClusterNode 的统计数据。
  • 线程数:Thread.activeCount()

当任一指标超过规则设定的阈值时,SystemSlot 抛出 SystemBlockException。需要注意的是系统规则是全局性的,不区分资源。

七、热点参数限流 ParamFlowSlot

ParamFlowSlot 依靠 ParamFlowRuleManager 加载的规则,对带有参数的资源调用进行细粒度控制。其核心挑战在于统计维度爆炸:每个资源 + 参数索引 + 参数值都需要独立的计数器。

Sentinel 采用 LRU 淘汰的 ConcurrentLinkedHashMap(基于 com.googlecode.concurrentlinkedhashmap)存储参数统计信息,key 为参数值,value 为 ParameterMetric,内部同样使用滑动窗口统计 QPS。

当请求进入时,获取指定索引的参数值,在对应资源的 ParameterMetric 中查找该参数的 CacheMap,累加计数,并根据规则中的特定值阈值或全局阈值决定是否限流。如果没有命中特定值,则使用通用阈值 count

LRU 策略防止内存无限增长,当参数值基数极大时,不活跃的参数会被自动淘汰,但这也意味着对于长尾参数,流量可能不受限。

八、初始化与资源调用入口

Sentinel 的初始化通过 InitExecutor.doInit() 完成,它会利用 SPI 加载所有 InitFunc 实现并执行。常见的 InitFunc 实现包括:

  • CommandCenterInitFunc:启动 Dashboard 通信的 HTTP Server(默认 Netty)。
  • MetricCallbackInitFunc:注册 Metric 回调。
  • DefaultClusterClientInitFunc:集群流控客户端初始化(可选)。

资源调用入口是 SphU.entry(),内部逻辑:

  1. 获取或创建 Context(通过 ContextUtil)。
  2. 通过 CtSph.lookProcessChain(resourceWrapper) 获取或构建 SlotChain
  3. 调用 chain.entry(context, resourceWrapper, count, prioritized, args) 触发整条链。
  4. 返回 Entry,业务代码执行后需调用 entry.exit() 触发 StatisticSlot 中的 exit 逻辑(记录 RT 等)。

九、与 Spring Cloud 及微服务生态的集成细节

Sentinel 为 Spring Cloud 提供了自动配置模块,其中 SentinelWebAutoConfiguration 会注册一个 SentinelWebInterceptor(或 SentinelWebFluxFilter),拦截所有 Web 请求,自动创建资源和上下文。

对于 Feign,SentinelFeignAutoConfiguration 会通过 Feign.builder()contract 构建代理时,插入 SentinelInvocationHandler,实现降级回退。

对于 Gateway,SentinelGatewayAutoConfiguration 注册 SentinelGatewayFilter,并将路由 ID 作为资源名。

这些自动配置大部分利用了 Spring 的 BeanPostProcessor 和 AOP 机制,将 Sentinel 的防护能力透明化。

十、总结

Sentinel 的内部实现围绕 SlotChain 责任链展开,通过 SPI 保证组件可插拔;滑动窗口统计使用无锁 LongAdder 提供高性能指标采集;限流算法覆盖快速失败、令牌桶预热和漏桶排队,满足不同场景;熔断降级通过状态机维护探活逻辑;系统自适应保护基于 OS 指标兜底。掌握这些底层机制,有助于在极端流量场景下准确理解 Sentinel 的行为,以及进行合理的规则配置和扩展。