限流是保障服务高可用的形式之一,尤其是在微服务架构中,对接口或资源进行限流能够无效地保障服务的可用性和稳定性。
之前的我的项目中应用的限流措施次要是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