简介
Sentinel提供了丰富的限流、熔断功能。它支持控制台配置限流、熔断规则,支持集群限流,并可以将相应服务调用情况可视化。
本文主要分析Sentinel的限流功能源码。
Sentinel会进行流量统计,执行流量控制规则。而统计数据的展示和规则的设置在 sentinel-dashboard 项目中,这是一个 Spring MVC 应用,有后台管理界面,我们通过这个管理后台和各个应用进行交互。
当然,你不一定需要 dashboard,仅仅使用 sentinel-core,它会将统计信息写入到指定的日志文件中,通过该文件内容来了解每个接口的流量情况。这时只是使用到了 Sentinel 的流量监控功能。
dashboard 应用默认不持久化数据,它的所有数据都在内存,所以 dashboard 重启意味着所有数据都会丢失。你应按需要定制化 dashboard,如至少你应该要持久化规则设置,QPS 数据非常适合存放在时序数据库中,当然如果你的数据量不大,存 MySQL 也问题不大,定期清理一下过期数据即可,因为大部分人应该不会关心一个月以前的 QPS。
Sentinel 的核心将不同 Slot 按序串在一起(责任链模式),从而将不同功能(限流、降级、系统保护)组合在一起。核心结构:
slot chain 其实可以分为两部分
统计数据构建部分(statistic)
判断部分(rule checking)
Sentinel 的数据统计
数据统计模块的内容,这样读者在后面看到相应的内容的时候心里有一些底。这节内容还是比较简单的,当然,如果你希望立马进入 Sentinel 的主流程,可以先跳过这一节。
Sentinel 的定位是流量控制,它有两个维度的控制,一个是控制并发线程数,另一个是控制 QPS,它们都是针对某个具体的接口来设置的,其实说资源比较准确,Sentinel 把控制的粒度定义为 Resource。
要做控制,首先就要先做统计,它要知道当前接口的 QPS 和并发是多少,进而判断一个新的请求能不能让它通过。
数据统计的代码在
StatisticNode
QPS 数据使用了滑动窗口:
保存最近60秒的统计信息。windowLengthInMs 故意设置为1000毫秒,表示每秒每个桶,这样我们就可以获得每秒的准确统计信息。
线程数量的计数器,即统计并发量
可知,Sentinel 统计了 秒 和 分 两个维度,现在看其实现类
ArrayMetric
Sentinel中的基本度量标准类,使用内部 BucketLeapArray。
属性
以分钟维度统计的使用来说,使用子类 BucketLeapArray 实现。
构造器
LeapArray
字段
条件(谓词)更新锁,仅在不使用当前桶时使用。
内部核心数组 array,它的长度为 60,就是有 60 个窗口,每个窗口长度为 1 秒,一分钟走完一轮。然后下一轮开启“覆盖”操作。
每个窗口是一个 WindowWrap 类实例。
添加数据的时候,先判断当前走到哪个窗口了(当前时间(s) % 60 即可),然后需要判断这个窗口是否是过期数据,如果是过期数据(窗口代表的时间距离当前已经超过 1 分钟),需要先重置这个窗口实例的数据。
统计数据同理,如统计过去一分钟的 QPS 数据,就是将每个窗口的值相加,当中需要判断窗口数据是否是过期数据,即判断窗口的 WindowWrap 实例是否是一分钟内的数据。
核心逻辑都封装在了 currentWindow(long timeMillis) 和 values(long timeMillis)方法中。
添加数据的时候,我们要先获取操作的目标窗口,也就是
分维度数据统计
currentWindow
Sentinel 在这里处理初始化和过期重置的情况
获取数据,使用的是
values
返回“有效”窗口中的数据
isWindowDeprecated
案例
红色部分的Context 代表一个调用链的入口,Context 实例设置在 ThreadLocal,所以它是跟着线程走的,如果要切换线程,需要手动切换。
ContextUtil#enter 有俩参数:
context name
调用链的入口,以区分不同调用链路,默认是
origin
调用方标识,作用
黑白名单的授权控制
统计诸如从应用 application-a 发起的对当前应用 interfaceXxx() 接口的调用,目前这个数据会被统计,但是 dashboard 中并不展示
进入 BlockException 异常分支,代表该次请求被流量控制规则限制,一般会让代码走入到熔断降级逻辑。当然,BlockException 其实有好多个子类
亦可 catch 具体子类处理。
SphU#entry 方法的参数:
- 第一个参数
标识资源,通常就是我们的接口标识,对于数据统计、规则控制等,我们一般都是在这个粒度上进行的,根据这个字符串来唯一标识,它会被包装成 ResourceWrapper 实例。 - 第二个参数
标识资源的类型 - EntryType.IN
入口流量,比如我们的接口对外提供服务,那通常就是控制入口流量
EntryType.OUT
默认就是出口流量,它的业务需要调用订单服务,像这种情况,压力其实都在订单服务,那就指定它为出口流量。
流量类型在 SystemSlot 类中用以实现自适应限流,根据系统健康状态来判断是否要限流,如果是 OUT 类型,由于压力在外部系统中,所以就不需要执行该规则。
若在一个方法中写,要注意内层的 Entry 先 exit,才能做外层的 exit,否则会抛出异常。源码角度来看,是在 Context 实例中,保存了当前的 Entry 实例。
源码解析
ContextUtil
static 代码块
这里会添加一个默认的 EntranceNode 实例。
enter
该行代码可不写,通常情况下,都不会显示设置 context。
ContextUtil.enter("user-center", "app-A");
如果不显式调用该方法,就会进入到默认 context。
然后上面的这个方法会走进 ContextUtil#trueEnter
,添加名为 “user-center” 的 EntranceNode 节点:
若不显式调用 ContextUtil#enter,那 root 就只有一个默认节点 sentinel_default_context。
context,线程执行的上下文,在 Sentinel 中对于一个新的 context name,Sentinel 会往树中添加一个 EntranceNode 实例。所以它的作用是为了区分调用链路,标识调用入口。在 sentinel-dashboard 中,我们可以很直观地看出调用链路:
SphU
entry
CtSph#entryWithPriority
lookProcessChain(resourceWrapper)
链中每一个节点是一个 Slot 实例,这个链通过 BlockException 异常来告知调用入口最终的执行情况。
Sentinel 提供了 SPI 端点,让我们可以自己定制 Builder,如添加一个 Slot 进去。
由于 SlotChainBuilder 接口设计,我们只能全局所有的 resource 使用相同的责任链配置。
按照默认的 DefaultSlotChainBuilder 生成的责任链继续源码。
对相同的 resource,使用同一责任链实例,不同 resource,使用不同责任链实例。
resource 实例根据 resource name 来判断,和线程没有关系。
参考
https://juejin.cn/post/6906302891875647495
https://github.com/alibaba/Sentinel/wiki/Sentinel-%E6%A0%B8%E5%BF%83%E7%B1%BB%E8%A7%A3%E6%9E%90
https://www.javadoop.com/post/sentinel