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 的单个节点,如FlowSlot、DegradeSlot等。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() 方法中。对于每个资源首次被访问时,会触发链的创建。
创建流程:
- 调用
SlotChainProvider.newSlotChain()。 - 通过
SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault()加载SlotChainBuilder,默认实现为DefaultSlotChainBuilder。 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;
}
}
注意这里使用了 AtomicLong 的 latestPassedTime 保证线程安全,但实际可能存在并发竞争,导致实际通过速率略高于配置值。不过 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(),内部逻辑:
- 获取或创建
Context(通过ContextUtil)。 - 通过
CtSph.lookProcessChain(resourceWrapper)获取或构建SlotChain。 - 调用
chain.entry(context, resourceWrapper, count, prioritized, args)触发整条链。 - 返回
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 的行为,以及进行合理的规则配置和扩展。