Alibaba Sentinel熔断降级/限流框架不完全解析
关于Alibaba Sentinel熔断降级/限流框架
阿里在集群防御、熔断限流方向所做可圈可点。 其中 github: Sentinel 在其中扮演着很重要的角色。 相对于类似的框架如 Hystrix(多得多的文档), 更加符合中国互联网工程师的思维习惯。
接下来的内容如果觉得啰嗦可以直接跳到
Sentinel架构介绍
小节
类似的防御体系:
- 美团的github: OCTO更像是集全公司的力量, 由上而下的要求全公司接入的、集中治理的平台。
- 滴滴对熔断、限流上更多的每个独立的团队自行发展, 各成体系。
以及其他企业的预案方案,想要轻松接入都不容易。 想要最低成本的接入熔断、限流,Sentinel更合适。
本文将持续迭代。
这里有对熔断、限流的包含源码解析的详解: Alibaba Sentinel 限流、熔断实现详解
一段话的介绍
预案
对可预期的问题, 例如 依赖的某服务不可用、网络可能不稳定、DB服务器不稳定、下星期要进行大规模抢购、可能有人恶意爬取 等这些不符合当前业务运行规则的。
设计一个处理方案, 来规避这些问题。
一般处理方案(简单列举):
- 两地三中心
- 服务器紧急扩容(参考微博)
- 代码中添加
if else
语句切换请求来源 - ···
通过建立一些针对可预期的问题的处理方案, 最低限度的保证自己系统的主体服务可用性
。
限流
通过压测测试出当前系统的最大负载能力,然后配置超过这个负载能力的请求全部丢弃、或者排队等待。
在长链路调用中,以最低承受单点为基准, 超过这个单点的负载能力的请求全部丢弃、或者排队等待。
通过限制调用方对自己的调用,起到保护自己系统的效果
。
熔断
在Sentinel中, 熔断往往跟降级靠在一起(我更加认为降级本就是预案的一部分,限流是预案、熔断也是预案,熔断却不一定都是降级)。
所有的对外部的调用都认为是不可控的。
其中部分调用是必须依赖的, 部分调用是可以被替代的(如查缓存改查DB)。
当我们对外部的请求发生异常, 例如超时、抛出异常, 且超过一定的阈值。这个时候我们可以理解为外部依赖不可用,多次请求也只是在浪费时间。这个时候可以禁止访问外部、或者直接返回默认值。
通过限制自己对外部系统的调用, 起到节约响应时间、维护链路稳定的作用
。
Alibaba Sentinel建立的用处就是在针对服务上的熔断与限流。此Sentinel不同于Redis的Sentinel。
它提供一个客户端SDK, 通过编写熔断、限流规则, 起到熔断、限流的作用。
同时它提供一个服务端(非必须), 能够更好的监控客户端运行状态,同时免编码的直接下发规则给客户端。
我基于Sentinel做的一个对Hello World的限流(看不到文件可直接下载):
Sentinel使用方式
部署Sentinel的客户端
参考Github介绍 Sentinel新手指南 以及 如何使用
大致这么几部:
- 引入依赖(
pom
/sentinel-core.jar
) - 代码编写植入
- 定义规则(支持本地配置规则, 使用Sentinel Dashboard配置规则)。
相对来说Sentinel的接入非常轻便简单了。但是遇到每一段代码都植入Sentinel给定的代码, 也是很麻烦, 因此可以参考小节
基于Sentinel所做易用性拓展
参考官方demo:
// 代码编写植入
public static void main(String[] args) {
// 配置规则.
initFlowRules();
while (true) {
// 1.5.0 版本开始可以直接利用 try-with-resources 特性
try (Entry entry = SphU.entry("HelloWorld")) {
// 被保护的逻辑
System.out.println("hello world");
} catch (BlockException ex) {
// 处理被流控的逻辑
System.out.println("blocked!");
}
}
}
// 定义规则(支持本地配置规则, 使用Sentinel Dashboard配置规则)。
private static void initFlowRules(){
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("HelloWorld");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// Set limit QPS to 20.
rule.setCount(20);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
部署Sentinel的服务端
服务端非必须, 主要提供规则配置和下发。 对简化编码有很大的帮助。
参考Github介绍 Sentinel 控制台
服务端UI功能:
名称 | 概述 | 用法 | 其他 |
---|---|---|---|
实时监控 | 采集当前资源的限流信息 | 观察 | |
簇点链路 | 把代码中植入的资源上报 | 统一熔断、限流 | 自动上报功能,很棒 |
流控规则 | 限流 | 根据资源, 支持访问资源的QPS和访问资源的线程数限流 | 一般是限制 QPS |
降级规则 | 熔断降级 | 根据资源,请求超时、异常、数超阈值熔断 | 异于hystrix,时间窗口后直接恢复 |
热点规则 | 未知 | 未知 | 未知 |
系统规则 | 未知 | 未知 | 未知 |
授权规则 | 未知 | 未知 | 未知 |
集群流控 | 见名知意 | - | - |
机器列表 | 见名知意 | - | - |
Sentinel架构介绍
对Sentinel的介绍, 仅限于客户端(服务端并不是必须的产品)。参考源码 Sentinel 的
sentinel-core
Sentinel中的核心概念resource
(资源), 定义为被Sentinel
保护的所有的事物。
所有的预案操作都是依据资源而定的。
sentinel-core
直观代码分层:
➜ sentinel-core git:(master) tree -d
.
└── src
└── main
└── java
└── com
└── alibaba
└── csp
└── sentinel
├── annotation
├── cluster
│ ├── client
│ ├── log
│ └── server
├── concurrent
├── config
├── context
├── eagleeye
├── init
├── log
├── metric
│ └── extension
│ └── callback
├── node
│ └── metric
├── property
├── slotchain
├── slots
│ ├── block
│ │ ├── authority
│ │ ├── degrade
│ │ └── flow
│ │ └── controller
│ ├── clusterbuilder
│ ├── logger
│ ├── nodeselector
│ ├── statistic
│ │ ├── base
│ │ ├── data
│ │ └── metric
│ │ └── occupy
│ └── system
├── spi
└── util
└── function
如上树形包结构, 重点关注这几个包:
包名 | 概述 | 其他 |
---|---|---|
cluster | 在客户端自成集群中 限流生效 | 后续专门讲 |
node | 数据节点, 存储SDK基于Resource运行时的包括RT、异常数等数据 | - |
solt | 插件,限流、熔断、监控都是基于插件来的 | 非常重要 |
slotchain | 每次对资源的调用都需要走一次插件链 | 责任链式solt |
context | 上下文载体 | 基于 ThreadLocal |
Sentinel Chain
Sentinel 以插件的形式,将功能打包在客户端SDK中。
插件可以构成一条单链(责任链模式
)。限流、熔断、日志等功能, 都被打包在链条中。其中的每一个节点, 就是
solt
。
详见com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot
。
通过next
指向下一个需要处理的节点。
通常用到的solt
:
名 | 概述 | 其他 |
---|---|---|
NodeSelectorSlot | 维护一条调用链(Sentinel代码嵌套调用)。 链首维护着一个 DefaultNode 。构成其它信息树的根节点。 | 这个类很重要,但是也很简单。就是维护一个调用树。 |
ClusterBuilderSlot | 类似NodeSelectorSlot ,维护的是ClusterNode 存储着调用次数、线程数等信息。 主要用于限流处 FlowSlot | - |
LogSlot | 在后续插件抛异常之后记一笔 | 记录异常 |
StatisticSlot | 在统计信息中记一笔 | 后续熔点、限流数据的的来源 |
SystemSlot | 基于整个应用的Load、QPS等限制 | - |
AuthoritySlot | 映射处理规则中的黑白名单 | - |
FlowSlot | 限流控制 | 可直接参阅源码 |
DegradeSlot | 熔断控制 | 可直接参阅源码 |
Node
看接口定义, 它就是一个存储程序运行时的节点。
com.alibaba.csp.sentinel.node.Node
节点 | 作用 |
---|---|
StatisticNode | 资源统计(计算规则), 其它Node的父类。 solt 依赖StatisticNode 持有的数据工作 |
ClusterNode | 该节点中保存了资源的总体的运行时统计信息,包括rt,线程数,qps等等, 相同的资源会全局共享同一个ClusterNode,以便用于全局QPS、线程数限制 |
DefaultNode | 该节点持有指定上下文中指定资源的统计信息(NodeSelectorSlot 创建),当在同一个上下文( 嵌套Sentinel )中次调用entry方法时,该节点可能会创建有一系列的子节点。每个DefaultNode中会关联一个ClusterNode 他们俩的区别在于 DefaultNode 更多的关注于当前Context的实时指标,而 ClusterNode 更多的关注当前资源在所有的Context的指标(主要用这个) |
EntranceNode | 该节点在创建context 的时候即被创建出来, 在一次调用链上下文中往往是头部节点 |
Sentinel执行流程图(我是重点,点我)
针对
Sentinel
所推荐的基础语法, 对其实现细致做解剖分析:
try {
entry = SphU.entry("HelloWorld");
// 资源中的逻辑.
System.out.println("hello world");
} catch (BlockException e1) {
System.out.println("blocked!");
} finally {
if (entry != null) {
entry.exit();
}
}
整体的逻辑相对简单。是对一个责任链的调用
重点在于整体数据链路的串联, 以及每个Solt的处理细则。
功能实现解析
限流功能
Sentinel的限流实现比较简单, 支持集群限流(单讲)和单机限流。实现方式委托给了包
com.alibaba.csp.sentinel.slots.block.flow.controller
下的实现。 分别支持快速失败(DefaultController
)、Warm up(热加载限流,WarmUpController
)、排队等待(RateLimiterController
)三种模式。
.
它隶属于FlowSlot
插件
快速失败模式下的限流实现很简单, 通过获取ClusterNode
中记录的当前每秒钟通过的请求数(clusterNode
), 加上当前请求需要的请求数(一般是1), 如果大于规则的设定值, 直接抛出异常,请求丢弃。 被限流。
Warm up 下的实现类似于Guava的限流实现, 采取 令牌桶
的方式。
- 从配置好规则开始, 限流大小从0逐步增长至预设限流大小。
- 每次执行限流判断的时候, 从令牌桶中重新判断是否需要灌入令牌。
- 和快速失败模式一样, 如果已经消费的令牌数加上需要的令牌数多于可取令牌数。 不被限流。
排队等待 模式很显然, 一般情况都会想到漏桶
算法。 参见 Github 匀速器。
- 排队等待采取了一种猜测等待时间的方法。
- 如果一个请求来了, 需要排队, 若预计等待时间过了依旧不满足限流条件, 自然放行。
- 而预计等待时间在限流条件下, 还会等待超时, 则自然直接拒绝掉。否则休眠之后放行。
- 排队等待的将支持后续排队, 被限流的程序则不再会被执行。
想看详细的介绍?
这里有包含源码解析的详解: Alibaba Sentinel 限流、熔断实现详解
熔断功能
熔断功能的实现就更简单了。
熔断提供三种熔断模式:RT(超时时间), 异常比例(异常数在所有请求数中的占比),异常数(具体阈值)
.
它隶属于DegradeSlot
插件
它也都是依据于ClusterNode
提供的信息做的熔断。
RT 模式 还在写。
想看详细的介绍?
这里有包含源码解析的详解: Alibaba Sentinel 限流、熔断实现详解
为什么用时间窗口(Node计算规则实现)
这段主要描述的内容是
Sentinel
怎么精确的控制每一秒的。
此处 https://juejin.im/post/5c3607b5e51d4542253faec3 有一个非常详细的介绍, 欢迎参阅。
如下简单介绍:
滑动窗口 Sentinel 的时间统计, 全放在了滑动窗口中去执行。依次委托步骤为:
Node(StatisticNode) -> Metric(ArrayMetric) -> WindowWrap(BucketLeapArray) -> MetricBucket -> LongAdder
滑动窗口如下解释:
- 按秒滑动: 每秒长
1000ms
, 分两个窗口, 则每个窗口500ms
。 - 按分滑动: 每分长
60000ms
, 分六十个窗口, 则每个窗口1000ms
。
仅以每秒为例:
任意一个时间点, 一定落在一秒的前500ms
, 或者落在后500ms
(当面毫秒
-当前毫秒 % 500
)。
滑动窗口
, 通过当前时间点,一定知道自己位于哪个窗口中(LeapArray#currentWindow(long)
),起始时间点是多少。
.
由上, 所有的统计(加、减)都可以基于时间窗口。
具体case:
后面计算限流的时候, 限定每秒QPS=N
。 则只需要知道前500ms
加上后500ms
的请求数M
, 加上当前这次请求需要的请求数X
(每个窗口中的LongAdder
, 数据, 以空间换时间, CAS累加成本太高), 是不是大于预设值N
就好了。
而每次做QPS累加, 是StatisticSlot
在后续Slot
计算未抛出异常之后才执行的。每次获取QPS,都需要得到当前的时间窗口(需要得到锁)
那回归正题, 为什么使用时间窗口呢, 秒级的时间窗口又为什么是2个呢?以我看来, 原因如下:
- 请求的到来并不是均匀的, 可能在一秒的
前500ms
根本没有请求。这个时候计算每秒的QPS需要由后500ms
+下一秒的前500ms
。 - 时间窗口的数量起码得能被时间长度(
1s=1000ms
/1m=60000ms
)整除吧。- 并发环境中,时间窗口间隔越小, 当前线程位于窗口分界点的概率就越高。
- 并发环境中,改修当前时间点所处的窗口,是需要加锁的,窗口越多,加锁越频繁。
- 窗口多到一定程度,所有线程全花在计算时间窗口上去了。
客户端与服务端通讯
可靠性分析
本文对可靠性着手的点有可用性、稳定性、高能效。所有的CASE都基于单台机器承受QPS100上限、单次任务执行200ms计算(模拟我部门生产环境)
点会很多, 遇见一个提一个。
可靠性:
- Sentinel能精准的限流么? 不可以(详见Sentinel高并发下限流不准)
- Sentinel在限流上可能错误阻断流量么? 会 (详见Sentinel线程池高并发下阻断流量)
高能效:
- Sentinel是有性能损失的。 甚至在高负载环境下, 可能达到5ms(数据基于生产环境混布,同机器有吃CPU的服务)
这个小结的目的是分析出在使用Sentinel上的时候遇见的坑。
实际生产活动中, 一般的企业维持100QPS的可能性并不高。还是并发环境的可能性就更低了。
Sentinel依然是一个非常优秀的, 可以服务于生产环境的预案工具。