关于微服务:Sentinel-是如何做限流的

33次阅读

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

限流是保障服务高可用的形式之一,尤其是在微服务架构中,对接口或资源进行限流能够无效地保障服务的可用性和稳定性。

之前的我的项目中应用的限流措施次要是 Guava 的 RateLimiter。RateLimiter 是基于令牌桶流控算法,应用非常简单,然而性能绝对比拟少。

而当初,咱们有了一种新的抉择,阿里提供的 Sentinel。

Sentinel 是阿里巴巴提供的一种限流、熔断中间件,与 RateLimiter 相比,Sentinel 提供了丰盛的限流、熔断性能。它反对控制台配置限流、熔断规定,反对集群限流,并能够将相应服务调用状况可视化。

目前曾经有很多我的项目接入了 Sentinel,而本文次要是对 Sentinel 的限流性能做一次具体的剖析,至于 Sentinel 的其余能力,则不作深究。

一、总体流程

先来理解一下总体流程:

(援用于 Sentinel 官网)

下面的图是官网的图,

从设计模式上来看,典型的的责任链模式。内部申请进来后,要通过责任链上各个节点的解决,而 Sentinel 的限流、熔断就是通过责任链上的这些节点实现的。

从限流算法来看,Sentinel 应用滑动窗口算法来进行限流。要想深刻理解原理,还是得从源码上动手,上面,间接进入 Sentinel 的源码浏览。

二、源码浏览

1.  源码浏览入口及总体流程

读源码先得找到源码入口。咱们常常应用 @ SentinelResource 来标记一个办法,能够将这个被 @ SentinelResource 标记的办法看成是一个 Sentinel 资源。因而,咱们以 @ SentinelResource 为入口,找到其切面,看看切面拦挡后所做的工作,就能够明确 Sentinel 的工作原理了。间接看注解 @SentinelResource 的切面代码(SentinelResourceAspect)。

能够清晰的看到 Sentinel 的行为形式。进入 SentinelResource 切面后,会执行 SphU.entry 办法,在这个办法中会对被拦挡办法做限流和熔断的逻辑解决。

如果触发熔断和限流,会抛出 BlockException,咱们能够指定 blockHandler 办法来解决 BlockException。而对于业务上的异样,咱们也能够配置 fallback 办法来解决被拦挡办法调用产生的异样。

所以,Sentinel 熔断限流的解决次要是在 SphU.entry 办法中,其次要解决逻辑见下图源码。

可见,在 SphU.entry 办法中,Sentinel 实现限流、熔断等性能的流程能够总结如下:

  • 获取 Sentinel 上下文(Context);
  • 获取资源对应的责任链;
  • 生成资源调用凭证(Entry);
  • 执行责任链中各个节点。

接下来,围绕这几个方面,对 Sentinel 的服务机制做一个零碎的论述。

2. 获取 Sentinel 上下文(Context)

Context,顾名思义,就是 Sentinel 熔断限流执行的上下文,蕴含资源调用的节点和 Entry 信息。

来看看 Context 的特色:

  • Context 是线程持有的,利用 ThreadLocal 与以后线程绑定。

  • Context 蕴含的内容

这里就引出了 Sentinel 的三个比拟重要的概念:Conetxt,Node,Entry。这三个类是 Sentinel 的外围类,提供了资源调用门路、资源调用统计等信息。

Context

Context 是以后线程所持有的 Sentinel 上下文。

进入 Sentinel 的逻辑时,会首先获取以后线程的 Context,如果没有则新建。当工作执行结束后,会革除以后线程的 context。Context 代表调用链路上下文,贯通一次调用链路中的所有 Entry。

Context 维持着入口节点(entranceNode)、本次调用链路的 以后节点(curNode)、调用起源(origin)等信息。Context 名称即为调用链路入口名称。

Node

Node 是对一个 @SentinelResource 标记的资源的统计包装。

Context 中记录本以后线程资源调用的入口节点。

咱们能够通过入口节点的 childList,能够追溯资源的调用状况。而每个节点都对应一个 @SentinelResource 标记的资源及其统计数据,例如:passQps,blockQps,rt 等数据。

Entry

Entry 是 Sentinel 中用来示意是否通过限流的一个凭证,如果能失常返回,则阐明你能够拜访被 Sentinel 爱护的前方服务,否则 Sentinel 会抛出一个 BlockException。

另外,它保留了本次执行 entry() 办法的一些根本信息,包含资源的 Context、Node、对应的责任链等信息,后续实现资源调用后,还须要更具取得的这个 Entry 去执行一些善后操作,包含退出 Entry 对应的责任链,实现节点的一些统计信息更新,革除以后线程的 Context 信息等。

3.  获取 @SentinelResource 标记资源对应的责任链

资源对应的责任链是限流逻辑具体执行的中央,采纳的是典型的责任链模式。

先来看看默认的的责任链的组成:

 

默认的责任链中的解决节点包含 NodeSelectorSlot、ClusterBuilderSlot、StatisticSlot、FlowSlot、DegradeSlot 等。调用链(ProcessorSlotChain)和其中蕴含的所有 Slot 都实现了 ProcessorSlot 接口,采纳责任链的模式执行各个节点的解决逻辑,并调用下一个节点。

每个节点都有本人的作用,前面将会看到这些节点具体是干什么的。

此外,雷同资源(@SentinelResource 标记的办法)对应的责任链是统一的。也就是说,每个资源对应一条独自的责任链,能够看下源码中资源责任链的获取逻辑:先从缓存获取,没有则新建。

4. 生成调用凭证 Entry

生成的 Entry 是 CtEntry。其结构参数包含资源包装(ResourceWrapper)、资源对应的责任链以及以后线程的 Context。

能够看到,新建 CtEntry 记录了以后资源的责任链和 Context,同时更新 Context,将 Context 的以后 Entry 设置为本人。能够看到,CtEntry 是一个双向链表,构建了 Sentinel 资源的调用链路。

5. 责任链的执行

接下来就进入了责任链的执行。责任链和其中的 Slot 都实现了 ProcessorSlot,责任链的 entry 办法会顺次执行责任链各个 slot,所以上面就进入了责任链中的各个 Slot。为了突出重点,这次本文只钻研与限流性能无关的 Slot。

5.1 NodeSelectorSlot — 获取以后资源对应 Node,构建节点调用树

此节点负责获取或者构建以后资源对应的 Node,这个 Node 被用于后续资源调用的统计及限流和熔断条件的判断。同时,NodeSelectorSlot 还会实现调用链路构建。来看源码:

相熟的代码格调。咱们晓得一个资源对应一个责任链。每个调用链中都有 NodeSelectorSlot。NodeSelectSlot 中的 node 缓存 map 是非动态变量,所以 map 只对以后这个资源共用,不同的资源对应的 NodeSelectSlot 及 Node 的缓存都是不一样的,资源和 Node 缓存 map 的关系可见下图。

所以 NodeSelectorSlot 的的作用是:

  • 在资源对应的调用链执行时,获取以后 context 对应的 Node,这个 Node 代表着这个资源的调用状况。
  • 将获取到的 node 设为以后 node,增加到之前的 node 前面,造成树状的调用门路。(通过 Context 中的以后 Entry 进行)
  • 触发下一个 Slot 的执行。

这里有个很乏味的问题,就是咱们在责任链的 NodeSelectorSlot 中获取资源对应的 Node 时,为什么用的是 Context 的 name,而不是 SentinelResource 的 name 呢?

首先,咱们晓得一个资源对应一条责任链。然而进入一个资源调用的 Context 却可能是不同的。如果应用资源名来作为 key,获取对应的 Node,那么通过不同 context 进来的调用办法获取到的 Node 就都是同一个了。所以通过这种形式,能够将雷同 resource 对应的 node 按 Context 辨别开。

举个例子,Sentinel 性能的实现不仅仅能够通过 @SentinelResource 注解办法来实现,也能够通过引入相干依赖(sentinel-dubbo-adapter),利用 Dubbo 的 Filter 机制间接对 DUBBO 接口进行爱护。咱们来比拟 @SentinelResource 和 Dubbo 形式生成 Context 的区别:

@SentinelResource

生成的 context 的 name 是:sentinel\_default\_context。所有资源对应的 Context 都是这个值。

Dubbo Filter 形式

生成的 context 的 name 是 Dubbo 的接口限定名或者办法限定名。

如果呈现嵌套在 Dubbo Filter 形式上面的其余 SentinelResource 的资源调用,那么这些资源调用的就会就会呈现不同的 Context。

所以有这样一种状况,不同的 dubbo 接口进来,这些 dubbo 接口都调用了同一个 @SentinelResource 标记的办法,那么这个办法对应的 SentinelReource 的在执行时对应的 Context 就是不同的。

另一个问题是,既然资源按 Context 分出了不同的 node,那咱们想看资源总数统计是怎么办呢?这就波及到 ClusterNode 了。具体可见 ClusterBuilderSlot。

5.2 ClusterBuilderSlot — 聚合雷同资源不同 Context 的 Node

此节点负责聚合雷同资源不同 Context 对应的 Node,以供后续限流判断应用。

能够看到,ClusterNode 的获取是以资源名为 key。ClusterNode 将会成为以后 node 的一个属性,次要目标是为了聚合同一个资源不同 Context 状况下的多个 node。默认的限流条件判断就是根据 ClusterNode 中的统计信息来进行的。

5.3 StatisticSlot — 资源调用统计

此节点次要负责资源调用的统计信息的计算和更新。与后面以及前面的 slot 不同,StatisticSlot 的执行时先触发下一个 slot 的执行,等上面的 slot 执行完才会执行本人的逻辑。

这也很好了解,作为统计组件,总要等熔断或者限流解决完之后能力做统计吧。上面看一下具体的统计过程。

下面这张图曾经很清晰的形容了 StatisticSlot 的数据统计的过程。能够留神一下无异样和阻塞异样的状况,次要是更新线程数、通过申请数量和阻塞申请数量。不论是 DefaultNode,还是 ClusterNode,都继承自 StatisticNode。所以 Node 的数据更新要来到 StatisticNode。

参考 Sentinel 数据统计框图,形容了 Node 统计数据更新的大体流程如下:

咱们从 StatisticNode.addPassRequest() 办法动手,以 passQps 为例,探索 StatisticNode 是如何更新通过申请的 QPS 计数的。

从源码可见,计数变量 rollingCounterInSecond 和 rollingCounterInMinute 都是 Metric,两个变量的工夫维度别离是秒和分钟。rollingCounterInSecond 和 rollingCounterInMinute 用的是 Metric 的实现类 ArrayMetric。

从 ArrayMetric 追溯上来:

统计信息都是保留到 ArrayMetric 的 data,也就是 LeapArray<MertricBucket> 中的。

LeapArray 是工夫窗口数组。根本信息包含:工夫窗口长度(ms,windowLengthInMs),取样数(也就是工夫窗口的数量,sampleCount),工夫距离(ms,intervalInMs),以及工夫窗口数组(array)。工夫窗口长度、取样数及工夫距离有上面的关系:

windowLengthInMs = intervalInMs / sampleCount

代码中 rollingCounterInSecond 应用的 intervalInMs 是 1000(ms),也就是 1s,sampleCount=2。所以,窗口时长就是 windowLengthInMs = 500ms。rollingCounterInMinute 应用的 intervalInMs 是 60 * 1000(ms),也就是 60s。sampleCount=60,所以,windowLengthInMs = 1000ms,也就是 1s。

工夫窗口数组(array)是类型是 AtomicReferenceArray,可见这是一个原子操作的的数组援用。数组元素类型是 WindowWrap<MetricBucket>。windowWrap 是对工夫窗口的一个包装,包含窗口的开始工夫(windowStart)及窗口的长度(windowLengthInMs),以及本窗口的计数器(value,类型为 MetricBucket)。窗口理论的计数是由 MetricBucket 进行的,计数信息是保留在 MetricBucket 里计数器 counters(类型为(LongAdder))。能够看一下下图计数组件的组成框图:

回到 StatisticNode.addPassRequest 办法,以 rollingCounterInSecond.addPass(count) 为例,探索 Sentinel 如何进行滑动窗口计数的。

5.3.1 获取以后工夫窗口

(1)取以后工夫戳对应的数组下标

long timeId = time / windowLength

int idx = (int)(timeId % array.length());

time 为以后工夫,windowLength 为工夫窗口长度,rollingCounterInSecond 的工夫窗口长度是 500ms。array 是单位工夫内工夫窗口的数量,rollingCounterInSecond 的单位工夫(1s)工夫窗口数是 2。timeId 是以后工夫对工夫窗口的整除。time 每减少一个 windowLength 的长度,timeId 就会减少 1,工夫窗口就会往前滑动一个。

(2)计算窗口开始工夫

窗口开始工夫 = 以后工夫(ms)- 以后工夫(ms)% 工夫窗口长度(ms)

获取的窗口开始工夫均为工夫窗口的整数倍。

(3)获取工夫窗口

首先,依据数组下标从 LeapArray 的数组中获取工夫窗口。

  • 如果获取到的工夫窗口自为空,则新建工夫窗口(CAS)。
  • 如果获取到的工夫窗口非空,且工夫窗口的开始工夫等于咱们计算的开始工夫,阐明以后工夫正好在这个工夫窗口里,间接返回该工夫窗口。
  •  如果获取到的工夫窗口非空,且工夫窗口的开始工夫小于咱们计算的开始工夫,阐明工夫窗口曾经过期(间隔上次获取工夫窗口曾经过来比拟久的场景),须要更新工夫窗口(加锁操作),将工夫窗口的开始工夫设为计算出来的开始工夫,将工夫窗口里的计数器重置为 0。
  •  如果获取到的工夫窗口非空,且工夫窗口的开始工夫大于咱们计算的开始工夫,创立新的工夫窗口。这个个别不会走进这个分支,因为阐明以后工夫曾经落后于工夫窗口了,获取到的工夫窗口是未来的工夫,那就没有意义了。

5.3.2 对工夫窗口的计数器进行累加

工夫窗口计数器是一个 LongAdder 数组,这个数组用于寄存通过申请数、异样申请数、阻塞申请数等数据。如下图:

其中,通过计数、阻塞计数、异样计数为执行 StatisticSlot 的 entry 办法时更新。胜利计数及响应工夫是执行 StatisticSlot 的 exit 办法时更新。其实就是别离在被拦挡办法执行前和执行后进行相应计数的更新。当然,addPass 就是在计数数组的第一个元素上进行累加。

计数数组元素类型是 LongAdder。LongAdder 是 JDK8 增加到 JUC 中的。它是一个线程平安的、比 Atomic* 系工具性能更好的 ” 计数器 ”。

5.4 FlowSlot — 限流判断

FlowSlot 是进行限流条件判断的节点。之前在 StatisticSlot 对相干资源调用做的统计,在 FlowSlot 限流判断时将会失去应用。

间接来到限流操作的外围逻辑–限流规定查看器(FlowRuleChecker):

次要的流程包含:

  • 获取资源对应的限流规定
  • 依据限流规定查看是否被限流

如果被限流,则抛出限流异样 FlowException。FlowException 继承自 BlockException。

那么 FlowSlot 查看是否限流的过程是怎么样的?

默认状况下,限流应用的节点是以后节点的 cluster node。次要剖析的限流形式是 QPS 限流。来看一下限流的要害代码(DefaultController):

  • 获取节点的以后 qps 计数;
  • 判断获取新的计数后是否超过阈值
  • 超过阈值单返回 false,示意被限流,前面会抛出 FlowException。否则返回 true,不被限流。

能够看到限流判断非常简单,只须要对 qps 计数进行查看就能够了。这归功于 StatisticSlot 做的数据统计。

5.5 责任链小结

通过下面的解说,再来看上面这张图,是不是很清晰了?

(援用于 Sentinel 官网)

NodeSelectorSlot 用于获取资源对应的 Node,并构建 Node 调用树,将 SentinelSource 的调用链路以 Node Tree 的模式组起来。ClusterBuilderSlot 为以后 Node 创立对应的 ClusterNode,聚合雷同资源对应的不同 Context 的 Node,后续的限流根据就是这个 ClusterNode。

ClusterNode 继承自 StatisticNode,记录着相应资源解决的一些统计数据。StatisticSlot 用于更新资源调用的相干计数,用于后续的限流判断应用。FlowSlot 依据资源对应 Node 的调用计数,判断是否进行限流。至此,Sentinel 的责任链执行逻辑就残缺了。

6. Sentienl 的收尾工作

无论执行胜利还是失败,或者是阻塞,都会执行 Entry.exit() 办法,来看一下这个办法。

  • 判断要退出的 entry 是否是以后 context 的以后 entry;
  • 如果要退出的 entry 不是以后 context 的以后 entry,则不退出此 entry,而是退出 context 的的以后 entry 及其所有父 entry,并抛出异样;
  • 如果要退出的 entry 是以后 context 的以后 entry(这种是失常状况),先退出以后 entry 对应的责任链的所有 slot。在这一步,StatisticSlot 会更新 node 的 success 计数和 RT 计数;
  • 将 context 的以后 entry 置为被退出的 entry 的父 entry;
  • 如果被退出 entry 的父 entry 为空,且 context 为默认 context,主动退出默认 context(革除 ThreadLocal)。
  • 革除被退出 entry 的 context 援用

7. 总结

通过浏览 Sentinel 的源码,能够很清晰的了解 Sentinel 的限流过程了,而对下面的源码浏览,总结如下:

  • 三大组件 Context、Entry、Node,是 Sentinel 的外围组件,各类信息及资源调用状况都由这三大类持有;
  • 采纳责任链模式实现 Sentinel 的信息统计、熔断、限流等操作;
  • 责任链中 NodeSelectSlot 负责抉择以后资源对应的 Node,同时构建 node 调用树;
  • 责任链中 ClusterBuilderSlot 负责构建以后 Node 对应的 ClusterNode,用于聚合同一资源对应不同 Context 的 Node;
  • 责任链中的 StatisticSlot 用于统计以后资源的调用状况,更新 Node 与其对用的 ClusterNode 的各种统计数据;
  • 责任链中的 FlowSlot 依据以后 Node 对应的 ClusterNode(默认)的统计信息进行限流;
  • 资源调用统计数据(例如 PassQps)应用滑动工夫窗口进行统计;
  • 所有工作执行结束后,执行退出流程,补充一些统计数据,清理 Context。

三、参考文献

https://github.com/alibaba/Sentinel/wiki

作者:Sun Yi

正文完
 0