Sentinel原理之责任链详解
文章目录
- 1. 基础知识
- 2. 责任链统计类Slot
- 2.1 NodeSelectorSlot
- 2.2 ClusterBuilderSlot
- 2.3 StatisticSlot
- 2.4 LeapArray滑动窗口
- 2.5 资源节点树
- 3. 责任链规则类Slot
- 3.1 SystemSlot
- 3.2 FlowSlot
- 3.3 DegradeSlot
1. 基础知识
Sentinel的核心在于其轻量级责任链插槽,主要实现类如下:
- NodeSelectorSlot:构建资源调用树,关联上下文和资源节点
- ClusterBuilderSlot:创建全局资源节点,聚合跨链路统计信息
- StatisticSlot:核心统计引擎,实时记录通过/阻塞/异常等指标,滑动窗口即在此实现
- SystemSlot:系统保护相关判断,如CPU和线程数等
- FlowSlot:流量控制,通常是配置QPS,可指定流量控制行为,如预热、快速失败和排队等策略
- DegradeSlot:熔断降级,配置基于慢调用比例、异常数量或比例配置降级条件
责任链的调用顺序是固定的,不能随意修改
调用链主要分为两种:
- 统计类Slot:如NodeSelectorSlot、ClusterBuilderSlot和StatisticSlot,用于收集资源指标,为规则判断提供数据
- 规则类Slot:基于统计数据执行控制逻辑
责任链可通过SPI机制自定义Slot,自定义的Slot需要实现ProcessorSlot接口,可使用@SpiOrder注解指定执行顺序,若未指定执行顺序在最后面
若未指定Context的名称,其默认名称为sentinel_default_context
同一个线程的Context是一样的,同一个资源名称使用的一直是同一个责任链
在资源树中,节点一共有四种类型:
- EntranceNode:继承DefaultNode,可以看成是一个资源树的根节点
- DefaultNode:继承StatisticNode,记录了资源信息、子节点信息和ClusterNode节点信息
- ClusterNode:继承StatisticNode,记录了资源名称和资源类型
- StatisticNode:继承自接口Node,内部有滑动窗口实现类ArrayMetric,分别支持秒和分钟的固定统计窗口,还有线程数量
在调用SphU.entry()方法时,若entry()直接抛出异常,Entry会自动记录BlockException,若要追踪系统普通异常,则需要依赖于Tracer.trace()方法来记录异常到Entry中以方便后续判断
调用Entry.exit()方法时会自动调用ContextUtil.exit(),前提是未显式创建Context,若显式创建了Context,则需要手动调用Context的exit()方法
调用Entry.exit()方法时,其执行步骤和entry()相反,主要是根据实际执行结果更新通过数、RT等
每次调用完entry()方法后需要调用exit()方法,否则会导致线程计数持续占用,导致误识别熔断限流,以及Entry未被释放引起内存泄漏
使用SphU.asyncEntry()时,需要在异步线程中同步调用exit()方法
2. 责任链统计类Slot
2.1 NodeSelectorSlot
资源树构建Slot,新建节点的条件:
- 线程不是同一个:核心为上下文名称不是同一个
- 资源名称不是同一个:每个资源都有独属于自己对应的Slot链
只要满足上面的任一条件就会新建一个DefaultNode节点,一个上下文对应一个EntranceNode节点
在新建DefaultNode节点时会构建各个节点的父子关系,在调用exit时从子节点回溯到父节点
2.2 ClusterBuilderSlot
同一个Slot链下该Slot中固定只有一个,和资源名称一一对应的
在该类中保存了不同资源对应的ClusterNode节点
其新增条件只有一个:资源名称不同
在该类中如果上下文的origin属性不为空,ClusterNode节点会创建origin属性对应的StatisticNode,并保存不同origin属性对应的节点
2.3 StatisticSlot
该Slot只做统计相关的事情,在entry()方法时优先执行完后面的Slot,再统计数据
entry()方法只记录DefaultNode和ClusterNode的线程数量和请求数量,如果origin属性不为空则记录StatisticNode的线程数量和请求数量
当Slot抛出异常时记录该异常,抛出异常是BlockException记录到blockError中,其它异常记录在error中,以方便exit()方法记录判断
exit()方法记录DefaultNode和ClusterNode的响应时间和异常数量(如果有的话),如果origin属性不为空则记录StatisticNode的响应时间和异常数量(如果有的话)
StatisticSlot主要操作的是StatisticNode中的秒级和分级滑动窗口,以及线程数量对象
2.4 LeapArray滑动窗口
在Sentinel的StatisticNode中,滑动窗口的实现类是ArrayMetric,该类中有LeapArray实现类,支持设置分片数量和每片时间间隔
在StatisticNode中,秒级窗口有2个分片,间隔为1000ms,每片时长500ms;分级窗口有60个分片,间隔为60000ms,每片时长1000ms
以秒级滑动窗口为例:
- 数据结构:
- LeapArray:长度=2的AtomicReferenceArray<WindowWrap> 数组
- WindowWrap:内有窗口时长windowLengthInMs、窗口开始时间windowStart和MetricBucket对象
- MetricBucket:最小响应时间minRt和LongAdder[]类型的counters对象,counters长度为6,索引对应了MetricEvent枚举对象
- 索引计算:
- sampleCount=2;windowLengthInMs=500
- 时间窗口索引公式:idx = ( timestamp / windowLengthInMs ) % sampleCount
- 窗口起始时间公式:windowStart = timestamp - timestamp % windowLengthInMs
此时假设需要均匀处理分布在100ms的100个请求,核心计算流程如下:
阶段 1:0ms → 500ms(填充第一个窗口)
- 请求分布:时间点:0ms、100ms、200ms、300ms、400ms(共 5 个请求)。
- 窗口操作:
- 所有请求的时间戳均落在 [0ms, 500ms) 区间。
- 计算索引:idx = (timestamp / 500) % 2 = 0(所有请求均指向索引 0)。
- 窗口创建与更新:
- 首个请求(0ms):
- 索引 0 为空 → 创建新窗口 WindowWrap0,windowStart=0ms。
- MetricBucket 初始化,LongAdder 数组清零。
- 请求计数:PASS 指标 +1。
- 后续请求(100ms、200ms、300ms、400ms):
- 复用 WindowWrap0,更新 MetricBucket:每次请求使 PASS 计数 +1。
- 首个请求(0ms):
- 当前窗口状态:
- 索引 0:PASS=5,其他指标(如 BLOCK)为 0。
- 索引 1:仍为空。
阶段 2:500ms → 1000ms(切换至第二个窗口)
- 请求分布:时间点:500ms、600ms、700ms、800ms、900ms(共 5 个请求)。
- 窗口操作:
- 500ms 请求:
- 计算索引:idx = (500/500)%2 = 1,windowStart=500ms。
- 索引 1 为空 → 创建 WindowWrap1,windowStart=500ms。
- PASS 计数 +1。
- 600ms–900ms 请求:
- 均落在 [500ms, 1000ms),索引 idx=1 → 复用 WindowWrap1,PASS 计数累加至 5。
- 500ms 请求:
- 当前窗口状态:
- 索引 0:PASS=5(数据保留,但窗口已过期)。
- 索引 1:PASS=5。
阶段 3:1000ms → 1500ms(复用并重置第一个窗口)
- 请求分布:时间点:1000ms、1100ms、1200ms、1300ms、1400ms(共 5 个请求)。
- 窗口操作:
- 1000ms 请求:
- 索引计算:idx = (1000/500)%2 = 0,windowStart=1000ms。
- 检查索引 0:原窗口 windowStart=0ms(已过期)→ 重置窗口:
- 清空 MetricBucket 计数器(PASS 归零)。
- 更新 windowStart=1000ms。
- 新请求计数:PASS=1。
- 1100ms–1400ms 请求:
- 复用索引 0 窗口,PASS 计数累加至 5。
- 1000ms 请求:
- 当前窗口状态:
- 索引 0:PASS=5(新周期数据)。
- 索引 1:PASS=5(保留,但将在 1500ms 后过期)。
阶段 4:后续请求(循环复用窗口)
- 模式重复:
- 每 500ms 切换一次窗口索引(0 → 1 → 0 → 1)。
- 到达窗口边界时(如 1500ms、2000ms),过期窗口被重置并复用。
- 计数分布:
- 每个 500ms 窗口均匀接收 5 个请求(共 20 个窗口覆盖 10000ms)。
- 每个窗口最终 PASS 计数均为 5。
上述流程关键机制说明:
- 窗口复用和重置:当该窗口的windowStart比计算出来的windowStart小时触发重置:
- 更新分片的windowStart
- 清空MetricBucket中的LongAdder数组数据
- 环形数组管理:通过数组长度和索引进行取模,保证索引一直在合法范围
- 技术性能优化:使用LongAdder存储指标,高并发使用时间分片减少竞争
2.5 资源节点树
资源节点树对于Sentinel而言就像JVM中的栈帧,其主要作用是记录资源入口调用链路
以下面的调用链路为例:
// 步骤1:调用资源A
try (Entry entryA = SphU.entry("ResourceA")) {// 步骤2:在资源A中调用资源Btry (Entry entryB = SphU.entry("ResourceB")) {// ... 业务逻辑}// 步骤3:在资源A中调用资源Ctry (Entry entryC = SphU.entry("ResourceC")) {// ... 业务逻辑}
}
其中每次调用SphU.entry()方法后,都会生成一个对应的资源节点,实际生产环境,调用链路具有很多分支,因此需要构建节点树来记录各个调用链路的关系。上述例子生成的资源节点树如下:
其资源对应派生节点关系:
其对应的责任链一共有三条,分别对应resourceA
、resourceB
和resourceC
。
3. 责任链规则类Slot
规则类的Slot调用顺序从前到后分别是SystemSlot、FlowSlot和DegradeSlot,其调用顺序如下图:
3.1 SystemSlot
SystemSlot的统计数据来源必须要求入口类型EntryType是IN,常用的SphU.entry(String)其默认的Entrype是OUT
当EntryType是IN时,在StatisticSlot中会把线程数量、请求数、响应时长和异常数量都添加到全局静态对象ENTRY_NODE中,其对象类型是ClusterNode
系统规则使用SystemRuleManager维护,QPS、maxThread这些规则配置都直接维护在该管理类中。若配置的系统规则有多个,只会比较后取最合适的
在SystemSlot规则判断时,便会使用全局静态对象ENTRY_NODE统计的数据和SystemRuleManager维护的系统规则阈值进行判断,若超过了配置阈值则抛出SystemBlockException异常,后续的Slot将会跳过
3.2 FlowSlot
FlowSlot的统计数据根据规则配置来源节点不同:
- StatisticNode:
- 入口配置了origin属性,FlowRule的limitApp和origin相同,strategy=DIRECT(默认)
- FlowRule的limitApp=default,strategy=DIRECT(默认)
- 入口配置了origin属性,FlowRule的limitApp=other,resource对应的limitApp!=origin
- ClusterNode:
- FlowRule的limitApp=default(默认),strategy=DIRECT(默认)
- FlowRule的strategy=RELATE,获取refResource对应的ClusterNode
- DefaultNode:
- FlowRule的strategy=CHAIN,refResource=Context名称
FlowRule使用FlowRuleManager维护规则,在FlowRuleManager加载规则时,会根据controlBehavior属性来创建对应的流量控制器:
- QPS控流:
- 慢启动:WarmUpController
- 匀速队列:RateLimiterController
- 慢启动匀速队列:WarmUpRateLimiterController
- 默认:DefaultController
- 线程数量控流:默认的DefaultController
流控器执行逻辑:
- DefaultController:直接比较统计指标和阈值,超出阈值直接抛出FlowException
- WarmUpController:基于令牌桶算法和冷启动曲线,动态调整阈值,超出阈值抛出FlowException
- RateLimiterController:基于漏桶算法控制请求频率,若频率大于配置的阈值则让其等待,等待时间大于最大等待时间则抛出FlowExceptiona,小于则线程等待时间差值
- WarmUpRateLimiterController:结合了慢启动和匀速排队的方式,使用慢启动逐步增大流量,并在增大途中或稳定后使用匀速队列控制请求速率
慢启动算法:
- 假设FlowRule配置count=100QPS,即每秒生成100令牌,warmUpPeriodInSec=10s,默认coldFactor=3
- warningToken=( warmUpPeriodInSec * count ) / (coldFactor - 1)=500
- maxToken=warningToken + (2 * warmUpPeriodInSec * count / (1 + coldFactor))=1000
- 请求到达时令牌操作:
- 桶中有令牌:消耗令牌并立即处理请求
- 桶中无令牌:拒绝请求
- 结合冷启动曲线:
- 剩余令牌数大于警戒线warningToken:冷启动阶段,根据斜率判断请求数量是否超过冷启动曲线阈值
- 剩余令牌数小于警戒线warningToken:进入常态化请求阶段
漏桶算法:
- 控制逻辑:
- count=100,即100QPS,固定间隔 = 1000 / 100 = 10ms
- 请求到达时计算预期通过时间:期望时间 = 上次通过时间 + 固定间隔
- 若当前时间 < 期望时间:
- 计算等待时间 = 期望时间 - 当前时间
- 若等待时间 > maxQueueingTimeMs拒绝请求,否则线程休眠等待时间
- 实现效果:
- 突发100个请求到达,系统按10ms/请求匀速处理(每秒100个)
- 若QPS超过阈值则根据配置时间等待或拒绝
- 实际案例
- 参数配置:
- count=100:固定间隔 = 10ms
- maxQueueingTimeMs:最大等待时间 = 5ms
- 上次通过时间 = -1ms(默认)
- 第一次调用为0ms:上次通过时间 = 0ms
- 第二次调用为6ms:
- 当前时间 = 6ms
- 期望时间 = 上次通过时间 + 固定间隔 = 0 + 10 = 10ms
- 等待时间= 期望时间 - 当前时间 = 4ms,
- 小于maxQueueingTimeMs = 5ms,等待4ms后放行
- 上次通过时间 = 上次通过时间 + 10ms = 0 + 10 = 10ms
- 第三次调用时间为14ms:
- 当前时间 = 14ms
- 期望时间 = 10 + 10 = 20ms
- 等待时间 = 20 - 14 = 6ms
- 大于maxQueueingTimeMs,拒绝
- 上次通过时间 = 10ms
- 第四次调用时间为17ms
- 当前时间 = 17ms
- 期望时间 = 10 + 10 = 20ms
- 等待时间 = 20 - 17 = 3ms,小于maxQueueingTimeMs,等待3ms
- 上次通过时间 = 10 + 10 = 20ms
- 第五次调用时间为31ms
- 当前时间 = 31ms
- 期望时间 = 20 + 10 = 30ms
- 由于当前时间 >= 期望时间,直接放行
- 上次通过时间 = 当前时间 = 31ms
- 参数配置:
3.3 DegradeSlot
负责判断熔断限流,根据预设规则控制资源访问状态,防止系统因依赖服务不稳定而崩溃
支持三种熔断策略:
- 慢调用比例:统计周期statIntervalMs内,慢调用比例 > slowRatioThreshold、总请求数 > minRequestAmount时触发,响应时间 > count配置即算作慢调用
- 异常比例:统计周期statIntervalMs内,异常比例 > count,总请求数 > minRequestAmount时触发
- 异常数量:统计周期statIntervalMs内,异常总数 > count,总请求数 > minRequestAmount时触发
熔断状态管理:
- CLOSED(关闭):熔断关闭,正常放行请求
- OPEN(打开):熔断打开,拒绝所有请求,持续timeWindow配置的时间
- HALF_OPEN(半开):熔断持续时间结束后进入该状态,试探性放1个请求,若请求正常切换为关闭,否则切换为打开
DegradeSlot原理:
- 调用entry():
- 熔断状态=CLOSE:放行请求
- 熔断状态=OPEN
- 请求时间在熔断窗口timeWindow内:拒绝请求,抛出DegradeException
- 请求时间在熔断窗口timeWindow后:仅单线程竞争,修改熔断状态为HALF_OPEN,竞争成功线程被放行,竞争失败线程被拒绝,抛出DegradeException
- 调用exit():
- 请求正常:正常退出
- 抛出BlockException:正常退出
- 抛出其它异常:使用资源对应的断路器进行后置判断
- 熔断状态=CLOSE:记录对应失败指标
- 熔断状态=HALF_OPEN:切换为OPEN状态
当熔断状态进行切换时会同步发送切换事件,可实现CircuitBreakerStateChangeObserver接口完成监听逻辑,并使用EventObserverRegistry.getInstance().addStateChangeObserver()
方法完成添加监听器
熔断规则DegradeRule被DegradeRuleManager进行管理,在新增加载规则时,会为对应的熔断规则创建对应的CircuitBreaker(断路器),在DegradeSlot中便是使用断路器来判断请求是否该熔断
断路器原理:内部都是使用滑动窗口完成时间段内的统计判断,窗口仅一个,时间窗口大小=statIntervalMs配置
断路器类型和判断逻辑:
- 慢调用断路器:
- 创建条件:grade=0
- 统计窗口指标:记录总调用次数,若响应时间大于count,慢调用次数+1
- 修改为OPEN状态:
- 窗口内总请求数 > minRequestAmount
- 慢调用次数 / 总调用次数 > slowRatioThreshold
- 修改为CLOSED状态:
- 当前状态 = HALF_OPEN
- 响应时间 > count
- 异常断路器:异常比例和异常数量使用同一个断路器进行判断
- 创建条件:grade=1或2
- 统计窗口指标:记录总调用次数,记录普通异常次数
- 状态修改为OPEN条件:
- 窗口内总请求数 > minRequestAmount
- 策略为异常比例时,errCount / totalCount > count
- 策略为异常数量时,curCount > count
- 修改为CLOSED状态:
- 当前状态 = HALF_OPEN
- 当前响应无普通异常抛出