前言
笔记仅用来记录sentinel限流降级的底层逻辑,不涉及如何整合spring cloud知识,所以只针对sentinel-core包内的源码进行讲解。你需要提前了解如下知识:
- 多线程下的线程安全问题
- 什么是ThreadLocal
- 责任链设计模式
- 什么是滑动窗口算法
- sentinel底层实现的官方文档
环境准备
- 创建一个maven项目
- 引入sentinel的核心依赖:
xml
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.6</version>
</dependency>
- 创建一个测试类SentinelTest:
- 在initFlowRules()中定义了限流规则:针对app_A的调用,如果QPS>20则限流
- 模拟两个应用(app_A、app_B)通过HTTP的方式并发调用/sentinelTest/user/info。
- 调用成功则日志打印pass,否则打印block
java
package sentinel;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.context.ContextUtil;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class SentinelTest {
private static final String USER_INFO_API = "/sentinelTest/user/info";
private static final String APP_A = "app_A";
private static final String APP_B = "app_B";
private static final Executor EXECUTOR_FOR_HTTP = new ThreadPoolExecutor(10, 20, 1000, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<>(180));
private static final String INVOKE_TYPE_HTTP = "HTTP";
private static final Executor EXECUTOR_FOR_RPC = new ThreadPoolExecutor(3, 5, 1000, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<>(100));
private static final String INVOKE_TYPE_RPC = "RPC";
private static void initFlowRules(){
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setLimitApp(APP_A);
rule.setResource(USER_INFO_API);
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// Set limit QPS to 20.
rule.setCount(20);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
public static void main(String[] args) {
// 配置规则.
initFlowRules();
int i = 0;
AtomicInteger COUNTER_FRO_APP_A = new AtomicInteger(0);
AtomicInteger COUNTER_FRO_APP_B = new AtomicInteger(0);
while (i++ < 100) {
EXECUTOR_FOR_HTTP.execute(() -> userInfo(COUNTER_FRO_APP_A, INVOKE_TYPE_HTTP, APP_A));
EXECUTOR_FOR_HTTP.execute(() -> userInfo(COUNTER_FRO_APP_B, INVOKE_TYPE_HTTP, APP_B));
}
}
public static String userInfo(AtomicInteger counter, String contextName, String origin) {
ContextUtil.enter(contextName, origin);
try (Entry entry = SphU.entry(USER_INFO_API)) {
// 被保护的逻辑
System.out.printf("请求次数[%s],应用[%s]通过[%s]调用资源[%s],pass%n", counter.addAndGet(1), origin, contextName, USER_INFO_API);
} catch (BlockException ex) {
// 处理被流控的逻辑
System.out.printf("请求次数[%s],应用[%s]通过[%s]调用资源[%s],block%n", counter.addAndGet(1), origin, contextName, USER_INFO_API);
}
ContextUtil.exit();
return "user";
}
}
- 执行结果
- 结果解析
- 应用app_A在第21次调用时,被block了,说明限流规则生效了,执行结果:符合预期
- 应用app_B在第40次调用时,依然能pass,说明没被限流,执行结果:符合预取
sentinel关键类
度量指标
LongAdder
名称:Long类型数据的加减器 作用:能线程安全地对Long类型数据进行加减操作,是各个指标的基础存储单元。 描述:比如记录某一资源的访问成功次数success,底层就是LongAdder类型的。LongAdder.class通过继承juc包下的Striped64.class,能线程安全地对Long类型数据进行加减操作,因此,当并发访问某一接口时,能保证统计数据的准确性。
MetricBucket
名称:度量桶、指标桶 作用:统计资源在一段时间内运行数据 描述:比如统计1000ms内某一资源的访问的通过次数、阻塞次数、成功次数、异常次数、资源的RT,数据就存储在一个MetricBucket对象中。MetricBucket类通过维护内部一个LongAdder数组,来记录各个指标在一段时间内的数量,定义的指标请看MetricEvent。
LeapArray<?>
名称:LeapArray 作用:可以使用滑动窗口算法对数据进行计数。 描述:LeapArray是一个抽象类,定义了sentinel中的基础统计指标。通过定义窗口总长度(ms)、间隔长度(ms)、采样数量,内部实现了滑动窗口算法的逻辑 实现类:
- BucketLeapArray
- OccupiableBucketLeapArray
- SimpleErrorCounterLeapArray
- SlowRequestLeapArray
- .......
Metric
名称:度量、指标 作用:定义了一个资源的运行指标,包括:成功数量、报错数量、通过数量、阻塞数量...... 描述:Metic是一个接口,定义了资源的调用指标。它的实现类ArrayMetric内部持有LeapArray对象,可以理解为LeapArray进一步封装,可以很简单地获取资源的当前运行指标,如:成功数量、报错数量、通过数量、阻塞数量等........ 实现类:ArrayMetric
类之间地关系图
ArrayMetric内部持有一个BucketLeapArray对象,BucketLeapArray内部持有一个MetricBucket数组,而MetricBucket持有一个LongAdder数组。总的来说,它们之间的关系如图所示:
举个例子,来理解这个数据结构:以200ms作为间隔,统计接口A在1s内请求的pass数、block数、exception数。那么接口A会对应一个ArrayMetric对象,也就是对应一个BucketLeapArray对象;一个BucketLeapArray对应5个MetricBucket对象,每个MetricBucket记录接口A在200ms内的运行数据;一个MetricBucket对应3个LongAdder对象,它们分别pass数、block数、exception数。
资源节点
Node
名称:资源节点 作用:用于记录和存储资源的实时统计数据 描述:Node接口定义一个资源的实时运行数据,比如pass数、block数、exception数等。什么是资源呢?在后端服务中,一个/user/info接口、一个facade接口都可以是资源,一个Node对象可以理解为一个后端接口在一段时间内的运行状态。 实现类:
- StatisticNode
- DefaultNode
- EntranceNode
- ClusterNode
- DefaultNode
StatisticNode实现了Node接口,具备了统计能力,所谓统计能力是指StatisticNode对象提供了方法,可以轻松的修改、读取资源的运行数据,方便判断资源是否过载、是否需要限流。DefaultNode、EntranceNode、ClusterNode都继承自StatisticNode,所以它们都具备统计能力。之所以要区分不同类型的Node,是因为这样可以从不同粒度统计资源的运行状态,看完下面的类之间的关系图,就很好理解资源和各种Node之间的关系了。
类之间的关系图
如何理解资源和各种Node之间的关系呢,举个例子,服务中有个UserManager提供了查询用户信息的queryUser()方法,对外分别提供了http接口以及rpc接口,当然我们可以将这两个接口定义成两个资源,但实际底层都是调用的queryUser()方法,所以定义成一个资源可能更恰当,这个时候如何区分来源是http和rpc呢?可以在http接口中调用 ContextUtil.enter("http", origin),在rpc接口调用 ContextUtil.enter("rpc", origin),在queryUser()方法中调用SphU.entry("queryUser"),这时候在sentinel内部,资源和各种Node之间的关系是:
Root_Node可以理解为是特殊的Node对象,一个应用只有一个Root_Node。由于我们分别调用ContextUtil.enter("http", origin)、ContextUtil.enter("rpc", origin)来区分不同来源,所以在Root_Node下面存在两个EntranceNode对象分别表示http和rpc调用方式,这两个EntranceNode对象分别指向两个不同的DefualtNode对象,它们的名字都是queryUser,分别记录queryUser()在http和rpc调用方式的实时运行数据,而这两个同名的DefaultNode都会指向一个名为queryUser的ClusterNode对象,ClusterNode对象记录了queryUser总的运行数据。这样组织Node对象,可以做到从不同粒度对queryNode()做限流降级,比如我可以只针对rpc调用方式的queryUser()做限流,也可以针对所有querUser()的调用做限流。 进一步来说,假设UserManger还存在一个queryDepatment()方法,对外只提供了http接口,那这个时候Node在sentinel中的组织方式是:
在介绍资源与各种node的关系时,我们提到StatisticNode具有统计数据的能力,但没解释它是怎么实现数据统计的,这里涉及到Node和Metric的关系。在指标度量中已经介绍了ArrayMetric(Metric的子类)能线程安全地记录一段时间内的运行数据,StatisticNode对象内部包就含两个ArrayMetric对象分别用来记录资源在1秒和1分钟内地运行数据。
处理器插槽
ProcessorSlot
名称:处理器插槽 描述:定义了sentinel实现限流和降级的底层接口,各实现类负责特定的功能模块,比如NodeSelectorSlot负责构建DefaultNode的映射关系,并根据contextname获取对应的DefaultNode、ClusterBuilderSlot负责构建ClusterNode的映射关系、StatisticSlot负责数据统计...... 实现类:
- AbstractLinkedProcessorSlot
- NodeSelectorSlot
- ClusterBuilderSlot
- LogSlot
- StatisticSlot
- AuthoritySlot
- SystemSlot
- FlowSlot
- DegradeSlot
- ProcessorSlotChain
- DefaultProcessorSlotChain
ProcessorSlotChain比较特殊,它虽然继承自AbstractLinkedProcessorSlot,但它并不像NodeSelectorSlot一样负责特定的模块功能,而仅仅定义了两个接口方法addFirst、addLast,让子类DefaultProcessorSlotChain实现如何在处理器插槽链的前后面增加处理器插槽,也就是说DefaultProcessorSlotChain的作用是按顺序将ProcessorSlot的实现类按链表的方式组装,DefaultProcessorSlotChain内部记录链表的first和last节点。
在sentinel中,一个资源只对应一个DefaultProcessorSlotChain对象,还是以UserManager为例,我们把它的queryUser()和queryDepatment()定义成资源,那么它们的映射关系是:
规则及其管理器
Rule
作用:存储降级或限流规则,还具备根据Node的运行数据判断是否需要限流或降级 实现类:
- AbstractRule
- FlowRule
- DegradeRule
- SystemRule
- AuthorityRul
FlowRuleManager
名称:限流规则管理器 描述:负责载入和读取限流规则,FlowSlot内部将用到。
DegradeRuleManager
名称:降价规则管理器 描述:负责载入和读取降级规则,DegredeSlow内部将用到。
其他
Context
名称:表示上下文 描述:存在一个ThreadLocal contextHolder,每个线程都会有自己的一个Context对象,如果没有就会创建,创建时会将contextName、EntranceNode对象、origin等信息保存起来,为后续ProcessorSlotChain使用。