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的代码路径:
SlotChainBuilderbuilder=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。
核心代码片段(简化):
publiclongpass(){data.currentWindow();// 滚动到当前窗口longsum=0;for(WindowWrap<MetricBucket>wrap:data.list()){sum+=wrap.value().pass();}returnsum;}这种设计的优势是完全无锁(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)。
关键代码逻辑(简化):
publicbooleancanPass(Nodenode,intacquireCount,booleanprioritized){longcurrentTime=TimeUtil.currentTimeMillis();syncToken(currentTime);// 根据时间填充令牌if(storedTokens>warningToken){// 预热期,令牌消耗速率较慢longoldStoredTokens=storedTokens;storedTokens-=acquireCount;if(storedTokens<0){storedTokens=oldStoredTokens;returnfalse;}}else{// 稳定期storedTokens-=acquireCount;if(storedTokens<0){returnfalse;}}returntrue;}其中syncToken会根据当前时间和预热斜率计算应该补充的令牌数,令牌生成速率从低到高逐渐增加。
4.3 RateLimiterController(排队等待)
使用漏桶思想,通过latestPassedTime记录最近一次请求通过的时间。计算下一次请求允许通过的时间:latestPassedTime + costTime(其中costTime = 1.0 / count * 1000 ms)。如果该时间大于当前时间,说明请求需要等待,若等待时间超过maxQueueingTimeMs(用户配置的最大排队超时),则拒绝;否则,休眠等待到允许时间后放行,并更新latestPassedTime。
publicbooleancanPass(Nodenode,intacquireCount,booleanprioritized){longcurrentTime=TimeUtil.currentTimeMillis();longcostTime=Math.round(1.0/count*1000);// 每个请求的间隔longexpectedTime=costTime+latestPassedTime.get();if(expectedTime<=currentTime){// 无需等待,直接通过latestPassedTime.set(currentTime);returntrue;}else{longwaitTime=costTime+latestPassedTime.get()-currentTime;if(waitTime>maxQueueingTimeMs){returnfalse;}// 等待try{Thread.sleep(waitTime);}catch(InterruptedExceptione){returnfalse;}latestPassedTime.addAndGet(costTime);returntrue;}}注意这里使用了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完成的。
代码结构(简化):
publicbooleantryPass(Contextcontext){if(state==State.CLOSED){// 检查是否触发熔断if(isOverThreshold()){state=State.OPEN;nextRetryTimestamp=TimeUtil.currentTimeMillis()+rule.getTimeWindow()*1000;returnfalse;}returntrue;}elseif(state==State.OPEN){if(TimeUtil.currentTimeMillis()>=nextRetryTimestamp){state=State.HALF_OPEN;returntrue;// 允许一个试探请求}returnfalse;}else{// HALF_OPEN// 后续通过探活回调改变状态returntrue;}}探活成功/失败由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 的行为,以及进行合理的规则配置和扩展。