流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:
除了流量控制以外,及时对调用链路中的不稳定因素进行熔断也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,可能会导致请求发生堆积,进而导致级联错误。
Sentinel
和 Hystrix
的原则是一致的: 当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。
在限制的手段上,Sentinel
和 Hystrix
采取了完全不一样的方法。
Hystrix
通过 线程池隔离
的方式,来对依赖(在 Sentinel 的概念中对应资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本(过多的线程池导致线程数目过多),还需要预先给各个资源做线程池大小的分配。
Sentinel
对这个问题采取了两种手段:
https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5
在 Sentinel
里面,所有的资源都对应一个资源名称(resourceName
),每次资源调用都会创建一个 Entry
对象。Entry
可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain
),这些插槽有不同的职责,例如:
Sentinel 将 ProcessorSlot
作为 SPI 接口进行扩展(1.7.2 版本以前 SlotChainBuilder 作为 SPI),使得 Slot Chain 具备了扩展的能力。您可以自行加入自定义的 slot 并编排 slot 间的顺序,从而可以给 Sentinel 添加自定义的功能。
下面介绍一下各个 slot 的功能。
这个 slot 主要负责收集资源的路径,并将这些资源的调用路径以树状结构存储起来,用于根据调用路径进行流量控制。
ContextUtil.enter("entrance1", "appA");
Entry nodeA = SphU.entry("nodeA");
if (nodeA != null) {
nodeA.exit();
}
ContextUtil.exit();
上述代码通过 ContextUtil.enter()
创建了一个名为 entrance1
的上下文,同时指定调用发起者为 appA
;接着通过 SphU.entry()
请求一个 token
,如果该方法顺利执行没有抛 BlockException
,表明 token
请求成功。
以上代码将在内存中生成以下结构:
machine-root
/
/
EntranceNode1
/
/
DefaultNode(nodeA)
注意:每个 DefaultNode 由资源 ID 和输入名称来标识。换句话说,一个资源 ID 可以有多个不同入口的 DefaultNode。
ContextUtil.enter("entrance1", "appA");
Entry nodeA = SphU.entry("nodeA");
if (nodeA != null) {
nodeA.exit();
}
ContextUtil.exit();
ContextUtil.enter("entrance2", "appA");
nodeA = SphU.entry("nodeA");
if (nodeA != null) {
nodeA.exit();
}
ContextUtil.exit();
machine-root
/ \
/ \
EntranceNode1 EntranceNode2
/ \
/ \
DefaultNode(nodeA) DefaultNode(nodeA)
此插槽用于构建资源的 ClusterNode
以及调用来源节点。ClusterNode
保持某个资源运行统计信息(响应时间、QPS、block 数目、线程数、异常数等)以及调用来源统计信息列表。调用来源的名称由 ContextUtil.enter(contextName,origin)
中的 origin
标记。
StatisticSlot
是 Sentinel 的核心功能插槽之一,用于统计实时的调用数据。
Sentinel 底层采用高性能的滑动窗口数据结构 LeapArray
来统计实时的秒级指标数据,可以很好地支撑写多于读的高并发场景。
这个 slot 主要根据预设的资源的统计信息,按照固定的次序,依次生效。如果一个资源对应两条或者多条流控规则,则会根据如下次序依次检验,直到全部通过或者有一个规则生效为止:
这个 slot 主要针对资源的平均响应时间(RT)以及异常比率,来决定资源是否在接下来的时间被自动熔断掉。
这个 slot 会根据对于当前系统的整体情况,对入口资源的调用进行动态调配。其原理是让入口的流量和当前系统的预计容量达到一个动态平衡。
注意系统规则只对入口流量起作用(调用类型为 EntryType.IN
),对出口流量无效。可通过 SphU.entry(res, entryType)
指定调用类型,如果不指定,默认是EntryType.OUT
。
Sentinel
的核心骨架,将不同的 Slot
按照顺序串在一起(责任链模式
),从而将不同的功能(限流、降级、系统保护)组合在一起。slot chain
其实可以分为两部分:统计数据构建部分(statistic)和判断部分(rule checking)。
核心结构:
目前的设计是 one slot chain per resource,因为某些 slot 是 per resource 的(比如 NodeSelectorSlot)。
Context
代表调用链路上下文,贯穿一次调用链路中的所有 Entry。Context 维持着入口节点(entranceNode)、本次调用链路的 curNode、调用来源(origin)等信息。Context 名称即为调用链路入口名称。
Context 维持的方式:通过 ThreadLocal 传递,只有在入口 enter 的时候生效。由于 Context 是通过 ThreadLocal 传递的,因此对于异步调用链路,线程切换的时候会丢掉 Context,因此需要手动通过 ContextUtil.runOnContext(context, f) 来变换 context。
每一次资源调用都会创建一个 Entry。Entry 包含了资源名、curNode(当前统计节点)、originNode(来源统计节点)等信息。
CtEntry 为普通的 Entry,在调用 SphU.entry(xxx) 的时候创建。特性:Linked entry within current context(内部维护着 parent 和 child)
需要注意的一点:CtEntry 构造函数中会做调用链的变换,即将当前 Entry 接到传入 Context 的调用链路上(setUpEntryFor)。
资源调用结束时需要 entry.exit()。exit 操作会过一遍 slot chain exit,恢复调用栈,exit context 然后清空 entry 中的 context 防止重复调用。
Sentinel 里面的各种种类的统计节点:
StatisticNode:最为基础的统计节点,包含秒级和分钟级两个滑动窗口结构。
DefaultNode:链路节点,用于统计调用链路上某个资源的数据,维持树状结构。
ClusterNode:簇点,用于统计每个资源全局的数据(不区分调用链路),以及存放该资源的按来源区分的调用数据(类型为 StatisticNode)。特别地,Constants.ENTRY_NODE 节点用于统计全局的入口资源数据。
EntranceNode:入口节点,特殊的链路节点,对应某个 Context 入口的所有调用数据。Constants.ROOT 节点也是入口节点。
构建的时机:
EntranceNode 在 ContextUtil.enter(xxx) 的时候就创建了,然后塞到 Context 里面。
NodeSelectorSlot:根据 context 创建 DefaultNode,然后 set curNode to context。
ClusterBuilderSlot:首先根据 resourceName 创建 ClusterNode,并且 set clusterNode to defaultNode;然后再根据 origin 创建来源节点(类型为 StatisticNode),并且 set originNode to curEntry。
几种 Node 的维度(数目):
ClusterNode 的维度是 resource
DefaultNode 的维度是 resource * context,存在每个 NodeSelectorSlot 的 map 里面
EntranceNode 的维度是 context,存在 ContextUtil 类的 contextNameNodeMap 里面
来源节点(类型为 StatisticNode)的维度是 resource * origin,存在每个 ClusterNode 的 originCountMap 里面。
StatisticSlot
是 Sentinel 最为重要的类之一,用于根据规则判断结果进行相应的统计操作。
entry 的时候:依次执行后面的判断 slot。每个 slot 触发流控的话会抛出异常(BlockException 的子类)。若有 BlockException 抛出,则记录 block 数据;若无异常抛出则算作可通过(pass),记录 pass 数据。
exit 的时候:若无 error(无论是业务异常还是流控异常),记录 complete(success)以及 RT,线程数-1。
记录数据的维度:线程数+1、记录当前 DefaultNode 数据、记录对应的 originNode 数据(若存在 origin)、累计 IN 统计数据(若流量类型为 IN)。
时间窗算法:
假设在一个时间窗内的QPS为100,当一个请求过来时,会先判断当前的时间窗内的阈值有没有超出,如果没有超出则允许通过,否则拒绝请求。所以由图中可知,第一个和第二个中的请求可以正常通过,但是第三个时间窗中会有20个请求无法通过。
时间窗算法存在的问题:
在10t - 16t中通过了10个请求,10t - 20t之间通过50个;
在20t - 26t中通过了60个请求,26t - 30t中通过了20个请求;
虽然在第一个时间窗和第二个时间窗中都没有超出阈值,但是在16t - 26t 时间内通过了110个请求,超出了单位时间窗内的阈值。
滑动时间窗算法:
时间窗不是固定的,而是在当前时间往前推一个单位时间作为一个时间窗,判断当前时间窗内的阈值是否超出。
滑动时间窗算法的问题:
当两个时间窗的时间间隔很短的时候,会造成一大段重复时间段的重复统计。造成资源浪费