Sentinel原理之责任链详解

文章目录

  • [1. 基础知识](#1. 基础知识)
  • [2. 责任链统计类Slot](#2. 责任链统计类Slot)
    • [2.1 NodeSelectorSlot](#2.1 NodeSelectorSlot)
    • [2.2 ClusterBuilderSlot](#2.2 ClusterBuilderSlot)
    • [2.3 StatisticSlot](#2.3 StatisticSlot)
    • [2.4 LeapArray滑动窗口](#2.4 LeapArray滑动窗口)
    • [2.5 资源节点树](#2.5 资源节点树)
  • [3. 责任链规则类Slot](#3. 责任链规则类Slot)
    • [3.1 SystemSlot](#3.1 SystemSlot)
    • [3.2 FlowSlot](#3.2 FlowSlot)
    • [3.3 DegradeSlot](#3.3 DegradeSlot)

1. 基础知识

Sentinel的核心在于其轻量级责任链插槽,主要实现类如下:

  1. NodeSelectorSlot:构建资源调用树,关联上下文和资源节点
  2. ClusterBuilderSlot:创建全局资源节点,聚合跨链路统计信息
  3. StatisticSlot:核心统计引擎,实时记录通过/阻塞/异常等指标,滑动窗口即在此实现
  4. SystemSlot:系统保护相关判断,如CPU和线程数等
  5. FlowSlot:流量控制,通常是配置QPS,可指定流量控制行为,如预热、快速失败和排队等策略
  6. DegradeSlot:熔断降级,配置基于慢调用比例、异常数量或比例配置降级条件

责任链的调用顺序是固定的,不能随意修改

调用链主要分为两种:

  1. 统计类Slot:如NodeSelectorSlot、ClusterBuilderSlot和StatisticSlot,用于收集资源指标,为规则判断提供数据
  2. 规则类Slot:基于统计数据执行控制逻辑

责任链可通过SPI机制自定义Slot,自定义的Slot需要实现ProcessorSlot接口,可使用@SpiOrder注解指定执行顺序,若未指定执行顺序在最后面

若未指定Context的名称,其默认名称为sentinel_default_context

同一个线程的Context是一样的,同一个资源名称使用的一直是同一个责任链

在资源树中,节点一共有四种类型:

  1. EntranceNode:继承DefaultNode,可以看成是一个资源树的根节点
  2. DefaultNode:继承StatisticNode,记录了资源信息、子节点信息和ClusterNode节点信息
  3. ClusterNode:继承StatisticNode,记录了资源名称和资源类型
  4. 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,新建节点的条件:

  1. 线程不是同一个:核心为上下文名称不是同一个
  2. 资源名称不是同一个:每个资源都有独属于自己对应的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。
  • 当前窗口状态
    • 索引 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。
  • 当前窗口状态
    • 索引 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。
  • 当前窗口状态
    • 索引 0:PASS=5(新周期数据)。
    • 索引 1:PASS=5(保留,但将在 1500ms 后过期)。

阶段 4:后续请求(循环复用窗口)

  • 模式重复
    • 每 500ms 切换一次窗口索引(0 → 1 → 0 → 1)。
    • 到达窗口边界时(如 1500ms、2000ms),过期窗口被重置并复用。
  • 计数分布
    • 每个 500ms 窗口均匀接收 5 个请求(共 20 个窗口覆盖 10000ms)。
    • 每个窗口最终 PASS 计数均为 5。

上述流程关键机制说明:

  1. 窗口复用和重置 :当该窗口的windowStart比计算出来的windowStart小时触发重置:
    • 更新分片的windowStart
    • 清空MetricBucket中的LongAdder数组数据
  2. 环形数组管理:通过数组长度和索引进行取模,保证索引一直在合法范围
  3. 技术性能优化:使用LongAdder存储指标,高并发使用时间分片减少竞争

2.5 资源节点树

资源节点树对于Sentinel而言就像JVM中的栈帧,其主要作用是记录资源入口调用链路

以下面的调用链路为例:

java 复制代码
// 步骤1:调用资源A
try (Entry entryA = SphU.entry("ResourceA")) {
    // 步骤2:在资源A中调用资源B
    try (Entry entryB = SphU.entry("ResourceB")) {
        // ... 业务逻辑
    }
    // 步骤3:在资源A中调用资源C
     try (Entry entryC = SphU.entry("ResourceC")) {
        // ... 业务逻辑
    }
}

其中每次调用SphU.entry()方法后,都会生成一个对应的资源节点,实际生产环境,调用链路具有很多分支,因此需要构建节点树来记录各个调用链路的关系。上述例子生成的资源节点树如下:
EntranceNode resourceA=defaultNodeA resourceB=defaultNodeB resourceC=defaultNodeC

其资源对应派生节点关系:
defaultNodeA clusterNodeA originA1 originA2 originAN statisticNodeA1 statisticNodeA2 statisticNodeAN defaultNodeB clusterNodeB originB1 originB2 originBN statisticNodeB1 statisticNodeB2 statisticNodeBN defaultNodeC clusterNodeC originC1 originC2 originCN statisticNodeC1 statisticNodeC2 statisticNodeCN

其对应的责任链一共有三条,分别对应resourceAresourceBresourceC

3. 责任链规则类Slot

规则类的Slot调用顺序从前到后分别是SystemSlot、FlowSlot和DegradeSlot,其调用顺序如下图:
系统过载 正常 触发限流 正常 熔断中 正常 请求进入 SystemSlot 抛出SystemBlockException FlowSlot 抛出FlowException DegradeSlot 抛出DegradeException 执行业务逻辑

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

负责判断熔断限流,根据预设规则控制资源访问状态,防止系统因依赖服务不稳定而崩溃

支持三种熔断策略:

  1. 慢调用比例:统计周期statIntervalMs内,慢调用比例 > slowRatioThreshold、总请求数 > minRequestAmount时触发,响应时间 > count配置即算作慢调用
  2. 异常比例:统计周期statIntervalMs内,异常比例 > count,总请求数 > minRequestAmount时触发
  3. 异常数量:统计周期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
      • 当前响应无普通异常抛出
相关推荐
Swift社区6 小时前
从 JDK 1.8 切换到 JDK 21 时遇到 NoProviderFoundException 该如何解决?
java·开发语言
DKPT7 小时前
JVM中如何调优新生代和老生代?
java·jvm·笔记·学习·spring
phltxy7 小时前
JVM——Java虚拟机学习
java·jvm·学习
seabirdssss9 小时前
使用Spring Boot DevTools快速重启功能
java·spring boot·后端
喂完待续9 小时前
【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
java·spring·spring cloud·云原生·架构·big data·序列晋升
benben0449 小时前
ReAct模式解读
java·ai
轮到我狗叫了10 小时前
牛客.小红的子串牛客.kotori和抽卡牛客.循环汉诺塔牛客.ruby和薯条
java·开发语言·算法
Volunteer Technology11 小时前
三高项目-缓存设计
java·spring·缓存·高并发·高可用·高数据量
栗子~~11 小时前
bat脚本- 将jar 包批量安装到 Maven 本地仓库
java·maven·jar