Sentinel(四)工作原理和源码解析总结

发布时间 2023-09-15 10:14:31作者: Tod4

Sentinel工作原理和源码解析


1 工作原理简介

  • Sentinel在使用上是通过注解@SentinelResource来实现的对资源的流控保护的,本质是通过AOP的方式来实现的流控方法增强,底层是通过SentinelResourceAspect指定切入点为注解,然后通过环绕通知的方式获取注解传来的资源名称,然后调用Sentinel责任链完成方法的织入,如果通过责任链则说明通过流控,就可以将请求数据统计到相应数据中(比如增加通过的线程数据或者QPS),最后执行目标方法,以此实现对目标资源的流控保护
  • Sentinel责任链是将不同的Slot按照顺序串在一起(责任链模式),从而将不同的功能组合在一起,系统会为每个受保护的资源都创建一套SlotChain,Sentinel的整个架构分为两个部分,一部分的Slot用于数据统计,另一部分Slot则使用统计的数据做具体的流控,只有同过所有的Slot才能算通过流控
  • 然后在整个责任链的实际执行中,还使用了Sentinel Context上下文来存储入口节点(entranceNode)、本次调用链路的 curNode、调用来源(origin)等信息,Context的本质是一个ThreadLocal,因此
    • 一方面可以实现在同一个责任链的调用过程不需要重复获取上面的信息,方便共用信息的获取
    • 另一方面如果没有通过Slot的检查,则会将当前Context上下文赋值为一个Context的子类NullContext由此来判断是否通过责任链的检查

2 Sentinel的SlotChain责任链

​ 在 Sentinel 里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责,例如:

用于数据统计的Slot

  • NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级
  • ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据
  • StatisticSlot 则用于记录、统计不同纬度的 runtime实时指标监控信息

使用统计的数据进行具体流控的Slot

  • ParamFlowSlot:对应的是热点参数限流部分,
  • FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;对应流控规则部分
  • AuthoritySlot根据配置的黑白名单调用来源信息,来做黑白名单控制,对应的是授权规则部分;
  • DegradeSlot通过统计信息以及预设的规则,来做熔断降级,对应降级规则部分
  • SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量,对应系统规则部分;

3 Sentinel Context上下文

  • 在整个责任链的实际执行中,使用了Sentinel Context上下文来存储入口节点(entranceNode)、本次调用链路的 curNode、调用来源(origin)等信息,Context的本质是一个ThreadLocal,因此

    • 一方面可以实现在同一个责任链的调用过程不需要重复获取上面的信息,方便共用信息的获取

    • 另一方面如果没有通过Slot的检查,则会将当前Context上下文赋值为一个Context的子类NullContext由此来判断是否通过责任链的检查

  • 一个context的生命周期可以包含多个资源操作,Context的生命周期的最后一个资源在exit的时候会清理Context,也意味着该Context的生命周期结束了

4 Sentinel的初始化准备工作:上下文和责任链的创建和遍历

  • Sentinel在使用上是通过注解@SentinelResource来实现的对资源的流控保护的,本质是通过AOP的方式来实现的流控方法增强,底层是通过SentinelResourceAspect指定切入点为注解,然后通过环绕通知的方式获取注解传来的资源名称,然后调用Sentinel责任链完成方法的织入,如果通过责任链则说明通过流控,就可以执行目标方法,以此实现对目标资源的流控保护
  • 在调用责任链的时候,首先会从ThreadLocal获取context上下文,如果Context是Context的子类NullContext类型,则表示当前系统的Context数量超出了阈值,也就是没有通过责任链的检查,直接返回一个无需做规则检测的资源操作对象,如果当前线程没有绑定context,则创建一个并将其放入ThreadLocal
  • 得到context上下文后,还需要查找资源对应的SlotChain责任链,如果不存在则需要根据当前资源进行创建
  • 得到资源对应的SlotChain责任链之后,开始依次遍历责任链中的Slot,即可完成Slot对应的数据收集以及流控功能的实现
4.1 Context上下文的创建
  • 首先尝试从ThreadLocal中获取Context,ThreadLocal中没有Context,则尝试从本地缓存map中获取,缓存map的key为context的名称,value为EntranceNode

  • 若缓存map的size大于context的数量阈值,则直接返回NULL_CONTEXT

  • 否则根据访问资源的名称和类型封装一个入口节点EntranceNodeEntranceNode是每一个上下文Context的一个入口节点,用于统计当前context的总体流量数据,创建后放入map缓存中,并设置context的资源名称和入口节点、来源信息

    这里上下文的创建同样需要注意两个地方:

    • 为了防止在共享集合中并发创建,使用了双重检测锁DCL

    • 更新map缓存的时候,不能直接put而是采用写时复制的手段,保证共享集合变量的迭代稳定性

  • 最后放入ThreadLocal即完成了Context上下文的创建

4.2 责任链的查找与创建

责任链的查找

  • 首先尝试从一个本地map缓存中获取当前资源的SlotChain责任链,key为由名称、资源类型等封装成的资源实体,value为对应的SlotChain责任链

  • 如果缓存中不存在则直接创建一个新的责任链,然后添加到map缓存中

这里责任链的创建需要注意两个地方:

  • 为了防止在共享集合中并发创建,使用了双重检测锁DCL
  • 更新map缓存的时候,不能直接put而是采用写时复制的手段,保证共享集合变量的迭代稳定性

责任链的创建

  • 创建责任链的时候,首先通过SPI的方式创建了一个SlotChainBuilder

  • 然后调用SlotChainBuilder的build方法,再利用SPI的方式创建责任链中的所有Slot,并可以指定创建的优先级顺序,采用尾插法的方式插入到ProcessorSlotChain组成的单向链表,就得到了资源对应的一个责任链

    # Sentinel default ProcessorSlots
    com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot
    com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot
    com.alibaba.csp.sentinel.slots.logger.LogSlot
    com.alibaba.csp.sentinel.slots.statistic.StatisticSlot
    com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot
    com.alibaba.csp.sentinel.slots.system.SystemSlot
    com.alibaba.csp.sentinel.slots.block.flow.FlowSlot
    com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot
    

5 Sentinel的数据统计实现原理

​ Sentinel的数据统计主要是通过各个用于数据统计Slot中创建的Node节点来实现的,包括

  • StatisticNode:统计节点,是Node接口的实现类,用于完成数据统计
  • EntranceNode:入口节点,每一个Context都会有一个入口节点,在创建context的时候生成, 用于统计当前Context的总体流量数据
  • DefaultNode:默认节点,SelectorSlot中生成,用于统计一个资源在当前Context的流量数据
  • ClusterNode:集群节点,用于统计一个资源在所有Context中的总体资源流量

6 StatisticSlot实现数据的统计与使用

6.1 StatisticSlot实现数据的统计
  • StatisticSlot在进行数据统计的时候,首先通过StatisticNode获取当前时间点所在的样本窗口,然后将当前的请求数量添加到当前样本中指定维度PASS的统计数据中

  • StatisticSlot对数据的使用,主要是借助的滑动时间窗算法:

    • 计算当前时间所在的样本窗口在时间窗口的位置

    • 计算当前时间窗口的开始时间点

    • 获取当前时间所在的样本窗口,如果当前时间样本窗口为null,说明样本窗口不存在则需要创建,然后通过CAS方式将新建窗口放入Array

    • 若当前窗口的起始时间点等于计算出的样本窗口的起始时间,直接返回计算出的样本窗口;如果当前样本窗口的起始时间 大于 计算出的样本窗口的起始时间,说明计算出的样本窗口已经过时了,需要将原来的样本窗口替换

      这是因为滑动时间窗口的实现实际上是复用了长度为windowLengthInMs的数组,所以统计第二轮样本容量的时候,又从头开始遍历数组,但是此时数组存放着以前的统计数据,因此只要前样本窗口的起始时间 大于 计算出的样本窗口的起始时间,就说明进行的是新一轮的样本窗口统计,因此需要将原来的样本窗口替换掉

    • 替换掉老的样本窗口,1:更新样本窗口的起始时间,2:设置多维度数据清零

6.2 StatisticSlot实现统计数据的使用

6 SelectorSlot实现调用路径统计

  • 根据SPI创建的顺序,遍历ProcessorSlotChain单链表的第一个Slot是NodeSelectorSlot,这个Slot负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级
  • 它底层是通过创建DefaultNode,并将创建的节点添加到调用树中,来实现统计一个资源在当前Context的流量数据

7 ClusterBuilderSlot实现集群簇点构建统计

8 StatisticSlot实现监控统计

7 FolwSlot实现单机和集群流控

  • ProcessorSlotChain责任链遍历到FlowSlot的时候,首先会获取指定资源的所有流控规则,然后逐个应用流控规则,无法通过规则检测则抛出异常表示流控生效
  • 流控规则FlowRule对应在DashBoard中设置的流控规则,包括:资源名称、限流阈值、流控模式、流控效果、流控效果、排队等待的超时时间和冷启动时间、集群模式等等,其中
    • 流控模式包括直接流控、关联流控和链路流控
    • 流控效果又包括快速失败、冷启动(底层是令牌桶算法)、排队等待(底层是漏斗算法)和冷启动排队等待组合的方式
    • 限流阈值类型包括QPS限流和线程数限流
  • 在应用规则的时候,还需要判断资源的规则是否是集群模式,如果是则使用规则处理集群流控,如果不是则使用规则处理单机流控
  • 然后根据规则选择出符合规则的node,Sentinel的数据统计主要是通过各个用于数据统计Slot中创建的Node节点来实现的,比如单机流控会选择默认节点node(包含一个资源在当前context上下文的流量数据),而集群规则会选择集群节点node(包含统计一个资源在所有Context中的总体资源流量),初次之外两种模式也都会包含 入口节点node(包含当前Context的总体流量数据)等node
  • 随后使用规则和得到的node统计数据进行逐项检测,根据规则中流控效果的不同,可以分为:
    • 快速失败(底层滑动时间窗算法)
    • 冷启动(底层令牌桶算法)
    • 排队等待(底层漏斗算法)
7.1 快速失败

​ 当流控规则为快速失败的时候,底层采用的是滑动时间窗算法来实现的通过性检查

  • 首先获取当前时间窗口中已经统计的数据(需要根据规则的阈值类型判断返回QPS(时间窗通过请求数量/时间窗长度)还是线程数),若已经统计的数据与本次请求的数量和大于设置的阈值,则表示没有通过检测直接返回false
  • 否则若小于等于阈值,返回true,通过检测
7.2 冷启动
7.3 排队等待

8 DegradeSlot实现熔断、降级

image-20230903163314889
  • DegradeSlot通过统计信息以及预设的规则,来做熔断降级,对应降级规则部分

  • ProcessorSlotChain责任链遍历到DegradeSlot的时候,会首先去获取当前资源的所有熔断器,然后逐个遍历当前熔断器,如果没有通过当前熔断器,则直接抛出异常结束责任链,表示没有通过责任链的规则检测,流控生效

  • Sentinel将三种异常策略慢调用比例、异常比例和异常数封装成了两种熔断器:

    • 响应时间熔断器ExceptionCircuitBreaker,包括慢调用策略
    • 异常熔断器ResponseTimeCircuitBreaker,包括异常比和异常数策略
  • 熔断器有三种状态,是熔断器接口的一个内部枚举类实现的,包含:

    • OPEN:打开状态,会拒绝所有请求
    • HALF_OPEN:在HALF_OPEN,收到请求的时候,熔断器允许进行尝试性调用,如果调用异常,则熔断器状态变为OPEN,等待下一次的恢复时间点,如果调用成功,则熔断器状态变为变为CLOSE
    • CLOSED:关闭状态,所有请求都被允许,请求达到阈值的时候就会将熔断器状态变为OPEN
  • 因此在判断请求是否通过熔断器的时候,先判断熔断器的状态,为关闭状态则请求可以通过;熔断器状态为打开状态,则判断尝试时间点是否到达(当前时间点是否大于等于下一个时间窗的开始时间)并且根据context上下文修改熔断器从open状态变为Half_Open状态,修改成功则可以访问