Sentinel-Core流程分析

3次阅读

共计 8156 个字符,预计需要花费 21 分钟才能阅读完成。

 上次介绍了 Sentinel 的基本概念,并在文章的最后介绍了基本的用法。这次将对用法中的主要流程和实现做说明,该部分主要涉及到源码中的 sentinel-core 模块。

1.token 获取

  如上为 token 获取的主流程,首先会先获取线程的上下文对象 Context,然后根据 ResourceName 查找对应的处理槽链,获得 SlotChain 后,生成该次调用动作的 Entry 对象,该对象会关联对应 SlotChain。内部会调用 SlotChain 的 entry 方法,让 entry 动作进入每个槽,后续需要调用 Entry 的 exit 方法,让 exit 动作进入 SlotChain 的每个槽。

  其中第三步生成的 Entry 对象为 CtEntry 对象,其模型上是一个链表,会将每次 entry 动作生成的 Entry 对象串联起来

  如上图,每 new 一个 CtEntry,都会传入 context 对象。由于每次操作会将当前 Entry 赋值 context 的 curEntry,每次 new 一次时,会检查该属性,如果为空,则是第一个节点,直接复制给 curEntry;如果非空,则该值为上一个节点,将该值复制给当前值的 parent,并将该值的 child 指向当前节点。做完这些动作后将 context 的 curEntry 指向当前节点。具体过程如上图示。

  执行 entry.exit,内部会判断 context.curEntry 是否是执行时的 entry,此举是为了控制 exit 顺序保持后进先出。如果判断不通过,说明不是按照后进先出的顺序执行 exit,会从执行的 entry 开始,到根节点逐个进行 exit,并抛出异常。如果判断通过,则调用对应的 SlotChain 执行 exit,并更改 context.curEntry,将其指向当前节点的父节点,但不解除 Entry 链的关系。

2. 查找处理槽链

  执行时会先从本地的缓存中查找是否已经有该资源对应的处理槽链,如果没有,则重新新生成一个。新加载时,使用 SPI,查找系统提供的 SlotChainBuilder 实现,若有除默认的 DefaultSlotChainBuilder 之外的实现在,则使用第一个,否则使用默认的 Builder。默认的 Builder 提供的处理槽链如下

3. 执行处理槽链

  槽的处理过程如下:

  ProcessorSlotChain 为一个链表,执行 slot 的 entry 方法会进入到 Slot 的内部,在内部可以通过 fireEntry 执行链表中下一个 slot 的 entry 方法 (如果存在)。如上,在 fireEntry 之前和之后可以有每个 slot 自己的处理逻辑,从而形成了类似过滤器链的结构。同理,exit 过程也类似

3.1. NodeSelectorSlot

  负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;该动作发生在 fireEntry 动作前。

  如下代码将构建出相对应的调用路径:

  执行 SphU.entry 时会先获取线程上下文对应的 Context,如果没有则新增一个。对于 node1C,直接调用 SphU.entry,会自动生成一个默认的 Context,内部会调用 ContextUtil.enter,并设置 EntranceNode(sentinel_default_context),然后将该 EntranceNode 接入到虚拟 EntranceNode(machine-root) 的子节点列表中。对于 node2A 和 node3A,由于调用了 ContextUtil.enter,相当于显示指定了 Context,并设置了 EntraceNode(entrance1) 和 EntraceNode(entrance2)。SphU.entry 在没有指定 EntryType 时,将设置 EntryType 为 OUT。
实现代码如下:

  实现上,由于同一个资源共享同一个 ProcessorSlotChain 对象,因而不同 Context 调用同一个资源时会使用到同一个 NodeSelectorSlot 对象。代码中直接使用 ContextName 相当于是使用了 ResourceName-ContextName 进行判断。为了在对应的 Context 下构建调用链,内部维护了一个 Map<String,DefaultNode>,其中 key 为对应线程上下文的 ContextName,value 为该上下文调用链中的各个 Node。对于第一次访问的资源,会在对应的 Context 链下新增一个 Node,并将该节点做为子节点链接到链上最近访问的那个节点上,从而完成调用链的构建。对于重复出现的资源,只会使用第一次出现的顺序。在该 slot 获得的 Node 节点将传入后续各个槽进行处理。

3.2.ClusterBuilderSlot

  用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;该动作发生在 fireEntry 动作前。

  上面的例子经过该 slot 后将新增如下 ClusterNode 节点

  上面说过,由于同一个资源共享同一个 ProcessorSlotChain 对象,因而不同 Context 调用同一个资源时会使用到同一个 NodeSelectorSlot 对象,为了统计该种资源的 Cluster 信息,直接使用一个 ClusterNode 节点表示即可。

 实现上,ClusterBuilderSlot 还持有一个静态的 ClusterNodeMap,用于缓存所有资源的 ClusterNode 信息。当经过该 slot 时,会判断 Map 中是否有该资源的节点信息,没有则新建一个。

  上面还有一段内容是设置节点的 Origin 信息节点的内容。如下图,ClusterNode 统计了同一种资源的统计信息,而不区分不同的 Context 来源,内部使用 originCountMap 区分不同的来源的统计情况。

 对于默认的 sentinel_default_context,其 orgin 设置为空 (""),因而 Cluster 没有该 Context 的 Origin 信息

3.3.LogSlot

  用于打印日志,在发生限流或者降级时输出特定日志;该动作发生在 fireEntry 动作后。

3.4.StatisticSlot

  用于记录、统计不同纬度的 runtime 指标监控信息;该动作发生在 fireEntry 动作后。

  该 Slot 的动作发生在 fireEntry 后,根据上面 SlotChain 执行图,该动作会在后续 Slot 检查执行后再执行。后续检查包括了权限检查,系统指标,用户自定义的限流和降级规则。

  如下图,该 Slot 的动作如下:

  若成功经过后续各个 Slot 的检查,相当于获得了 token,则会更新统计信息,包括增加线程数(ThreadNum),增加通过请求数(PassRequest),涉及的节点包括:

  1. 当前节点;当前节点的 Origin 节点(若存在)
  2. 全局 Entrance_Node 节点(若当前节点类型为 EntryType.IN);执行后将调用 onPass 回调函数。

  
  其他情况如图示,包括:

  1. 在获取 token 设置了优先策略,等待超时抛出 PriorityWaitExeption(注:为什么只增加 ThreadNum 但不增加 PassRequest,却执行了 onPass 回调函数?这里 PriorityWatitException 并不是 BlockException,抛出 PriorityWaitException 时,该请求已经获取了令牌,可以执行后续的操作,只是不在当前窗口,这点后续会说明)
  2. 后续 Slot 规则不通过,抛出 BlockException
  3. 发生其他异常,这时候会设置当前节点的 Error 值,为 exit 动作做判断

  退出的动作如下,该行为发生在 fireExit 前,用于统计成功时的响应时间,减去获取 token 时的线程数,增加成功请求数(SuccessRequest)

  具体的统计方法,后续会对 StatisticNode 做说明。

3.5.SystemSlot

  通过系统的状态,例如 load1 等,来控制总的入口流量;

  检查当前系统指标是否正常,只检查入口流量节点。包括全局 QPS,全局线程数,全局平均响应时间,系统负载,CPU 负载。

3.6.AuthoritySlot

  根据配置的黑白名单和调用来源信息,来做黑白名单控制;

3.7.FlowSlot

  用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;

  首先会根据规则设定的模式,选择处理方式,有 Local 和 Cluster 两种,这里先介绍 Local 方式。

  Local 方式时,先选择统计数据的节点,再根据设定的限流器获取 token,达到限流的目的。

3.7.1 选择统计数据的节点

  这一步将根据给定设置的应用范围,和限流策略来选择对应的节点。

  这边先介绍默认的应用范围和限流策略,分为:

  1. 应用范围:default,other
  2. 限流策略:DIRECT,RELATE,CHAIN

  选择时,将根据调用方 LimitApp 来选择对应的节点。若规则作用于 origin 上且除 default 和 other 外, 如果是 DIRECT 策略,返回 origin 节点;若是作用于 default 上,如果是 DIRECT 策略,返回 Cluster 节点;若是作用于 other 上,如果是 DIRECT 策略,返回 origin 节点。上述其他情况的选择过程都是相同的,即:如果资源名为空,返回空;如果是 RELATE 策略,使用 ClusterNode 节点数据;如果是 CHIAN 策略,且当前节点名同规则名一致,使用当前节点数据,否则返回为空,详情可以看代码。

  获得数据节点后,便可以使用规则中指定的限流器校验节点的数据,以获取 token。

3.7.2 根据限流器获取 token

  系统提供的限流器包括:

  1. 默认限流器:直接拒绝

    默认限流器采用直接拒绝的方式,若当前已经被获取的 token 数和需要的 token 数大于设定的规则,则直接拒绝,支持按线程数和 QPS 来算。当按 QPS 算时,还支持优先模式,允许参照之前的使用情况,在一定期望时间内,从后续时间借用令牌,保证当前请求能够通过。支持优先模式能够充分利用系统的资源,尽可能多的接受请求,防止请求被不必要的拦截

  2. 预热限流器:参考 guava,提供带预热 / 冷启动功能的令牌桶方法

    参考 guava 的预热限流器,按照请求数来计算。Token 个数不是一开始就达到设定的上限,而是有个预热的过程,在预热的时间内,token 的生成速度是固定的,当超过该时间后将根据可用 token 数,调整速率,使之增加到设定的值。这样做能够有效的防止突发流量穿透到后台服务。

  3. 恒定速率限流器:根据预设的速率进行恒定控制

    根据设定的 QPS,可以得到每个 token 所需的恒定时间,对于所需的 token 数,能够预估所需的时间。若该等待时间大于最大等待时间,则拒绝,否则更新上次通过时间(该规则在上个请求已经获取 token 后的预期时间),加上该次需要的时间,并让该次请求进行等待。能够保证 token 的获取速率是平滑恒定的,达到削峰填谷,防止通过的请求扎堆在窗口的前段。

  4. 预热的恒定速率限流器:前期同预热限流器相同,过了预热时间后,将按照恒定的速率获取 token。
3.8.DegradeSlot

  则通过统计信息以及预设的规则,来做熔断降级;

  降级的流程如上:

  1. 流程进来时,如果该规则(资源)已经处于降级状态,则直接返回校验不通过
  2. 若不处于降级状态,则获取该资源的 ClusterNode 节点数据,按照设定的降级类型进行判断:

    1) 根据响应时间判断:若该资源的平均响应时间小于规则的设定值,则重置不通过的次数为 0,并返回 true;如不通过的次数小于默认值,则返回 true;否则进入降级流程

    2) 根据异常率判断:若该资源异常次数小于默认值或者异常率小于设定值,则返回 true;否则进入降级流程

    3) 根据异常次数判断:若异常次数小于设定值,则返回 true;否则进入降级流程

  3. 降级流程:将资源状态置为降级,并开启定时器,该定时器将在规则设定的时间后执行,执行时将重置该规则指定的资源降级状态

4.Node

4.1 滑动窗口模型

  Sentinel 底层采用高性能的滑动窗口数据结构 LeapArray 来统计实时的秒级指标数据,可以很好地支撑写多于读的高并发场景。

  LeapArray 主要包括如下属性:

  滑动窗口模型如下:

  LeapArray 的主体实现如下(去除原有备注,换上自己的备注):  

  将 array 抽象为一个环,则上面的流程可以按如下图看:

  主体流程为:

  1. 计算所给时间所在窗口索引号:i
  2. 计算所给时间所在窗口开始时间:ws
  3. 根据索引号 i 获取现有窗口对象:window
  4. 根据现有窗口对象:window,判断是否需要更新当前窗口

    1) 如果现有窗口对象为空,则初始化当前窗口

    2) 如果现有窗口的开始时间同 ws 一致,则说明现有窗口还未过期,继续使用当前窗口

    3) 如果现有窗口的开始时间小于 ws,说明现有窗口已经过期,需要更新该窗口

4.2 StatisticNode

  StatisticNode 为 Sentinel 实现监控统计的必要组件,该组件能够实现不同粒度,不同维度的数据监控统计。

  上图为 Statistic 必要组件的组成:

  1. LeapArray:滑动窗口模型,实现不同时间粒度的滑动窗口行为,LeapArray 为抽象类
  2. BucketLeapArray:LeapArray 的实现类,主要实现方法

    1) newEmptyBucket:初始化窗口的动作,这里初始化时设置了包装类 MetricBucket

    2) resetWindowTo:更新窗口的动作,这里重置了窗口的开始时间,以及重置了窗口中包装类的值

  3. MetricBucket:存储着各个统计维度的计数,统计的维度为 MetricEvent 枚举值
  4. MetricEvent:统计维度,包括

    1) PASS:获得令牌的计数

    2) BLOCK:未获得令牌的计数

    3) EXCEPTION:发生异常的计算

    4) SUCCESS:获得令牌并成功归还的计数

    5) RT:请求时间

  5. Metric:统计接口,按接口译,可以理解为“可统计的”,聚合了采集统计信息以及生成统计信息的方法。采集统计信息为各个维度的 add 方法,生成统计信息使用 windows 和 details 方法,返回 MetricNode 包含各统计信息

    1) windows:返回现在 LeapArray 中的统计信息,以 MetricBucket 形式

    2) details:返回现在 LeapArray 中的统计信息,以 MetricNode 的形式

  6. MetricNode:Bean,各属性为统计信息的值,相当于当前系统的一个镜像数据,将计数数据按照维度进行运算过。
  7. ArrayMetric:Metric 的实现类,内部使用 LeapArray 作为滑动窗口统计各个窗口的数据
  8. StatisticNode:统计节点,内部使用 1 秒和 1 分钟的滑动窗口来统计数据,同时还会记录当前的线程数

    1) rollingCounterInSecond:sampleCounter 为 2,intervalInMs 为 1 秒的滑动窗口

    2) rollingCounterInMinute:sampleCounter 为 60,intervalInMs 为 1 分钟的滑动窗口

  9. ClusterNode : 继承自 StatisticNode,对于某一个资源的全局统计
  10. DefaultNode:继承自 StatisticNode,对于某一个资源在相应上下文中的实现,保存了一个指向 ClusterNode 的引用。另外还保存了子节点列表,当在同一个 context 下多次调用 SphU.entry 不同资源时会创建子节点

  主要调用流程如下:

  流程到最后,将累加 MetricBucket 中维护的各维度计数数据。这些数据可以在调用时转为 MetricNode 提供格式化数据。

4.3 LeapArray

  LeapArray 的实现包括:

  1. BucketLeapArray:每个窗口持有一个 MetricBucket,该对象存储着当前窗口内各个维度的计数值
  2. FutureLeapArray:只存储比当前时间大的窗口
  3. BorrowBucketLeapArray:持有 FutureLeapArray,支持从后续窗口中借用资源
  4. LeapArray 的默认实现上,每个窗口在 intervalInMs 内都是有效的

  如图,在某个时间 800 时,intervalInMs 内的各个窗口都是有效的,这时候计算 qps 将使用各个窗口的统计值之和。

  FutureLeapArray 重写了 isWindowDeprecated 方法,如下

  只要给定时间大于给定窗口的起始时间则算窗口失效; 当给定时间为当前时间,窗口为上一个窗口或者当前时间所在窗口时,都是失效的,只能存给定时间后的窗口。上图中,在给定的时间为 800 时,只有 1000 这个窗口是有效的。

  BorrowBucketLeapArray,主要用于在进行限流时支持有限模式,当当前的 token 不够时,允许先从后续窗口中获取 token。内部持有一个 FutureLeapArray,该窗口队列用于存储在当前时间后的窗口。通过重写 LeapArray 的 addWaiting 方法,占用指定的后续窗口计数值,再通过 currentWaiting 方法,可以获取当前时间已经占用后后续窗口多少个资源。

  因为 borrowArray 中的窗口可能在之前已经初始化或者使用过,因而,BorrowBucketLeapArray 在初始化窗口或者更新窗口时,会考虑 borrowArray 中已有的窗口数据,如下重写的 newEmptyBucket 方法和 resetWindowTo 方法。

  newEmptyBucket 方法初始化时,如果发现该时间所在的窗口已经在 FutureBucketArray 中出现过,将会使用该窗口的值。同理,restWindowTo 在更新时,如果所给时间窗口已经存在,则加上之前已经存在的计数值。

  StatisticNode 在使用时还会根据之前统计的请求,估算后续窗口的可用请求,再从后续窗口借用 token,具体实现如下:

  执行时会先计算离当前时间最远的一个有效窗口的开始值,如下图,假设当前窗口为 curr,则 earliestTime 落在 earliest 1 的开始处。然后从 earliest 1 开始,逐次增加一个窗口,以逐步根据之前的 Pass 值,估算后续可能出现的请求,如果根据之前的某个窗口的值估算出后续某个窗口存在空闲的 token,且等待时间在期望的时间内,则 hold 住当前请求,使之到达特定窗口后再继续通过。下图假设设定规则的最大 QPS 为 20,在前 3 个窗口时,每次都没有达到最大限定的最大值,则可以认为,在 curr 值后的一个窗口内也是该种情况。当 curr 突然到来 22 个请求时,根据规则将有 2 个请求被拒接掉,但根据之前窗口的情况,这两个请求可以在后续的第二个请求中完成,以此充分的利用系统的资源。

5.RuleManager

  各规则管理的模式都一致,主要用到如下 3 个组件

  1. RuleManager,具体的规则实现类,没有具体的实现接口,但是都有 loadRules 方法
  2. ProertyListener,规则监听器
  3. SentinelProperty,观察者,持有各监听器

  RuleManaer 内部持有 PropertyListener 和 SentinelProerty,并且 RuleManager 有 PropertyListener 的内部实现类

  具体流程为,初始化时 RuleManager 调用 SentinelProepety.addListener,设置监听器。SentinelProperty 会调用 PropertyListener.configLoad,完成初始化。后面调用 RuleManager.loadRules 重新更改规则时,内部调用者 SentinelProerty.updateValue,该方法会遍历 SentinelProperty 内部持有的所有 Listener,逐个执行 PropertyListner.configUpdate,从而通知到 RuleManager 规则发生了改变,以便让 RuleManager 做出处理。

个人公众号:啊驼

正文完
 0