一、Sentinel简介
Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
- 丰富的应用场景:秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
- 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等
有关Sentinel的详细介绍以及和Hystrix的区别可以自行网上检索,推荐一篇文章:mp.weixin.qq.com/s/Q7Xv8cypQ…
本次主要使用了Sentinel的降级、限流、系统负载保护功能
二、Sentinel关键技术源码解析
无论是限流、降级、负载等控制手段,大致流程如下:
•StatisticSlot 则用于记录、统计不同维度的 runtime 指标监控信息
•责任链依次触发后续 slot 的 entry 方法,如 SystemSlot、FlowSlot、DegradeSlot 等的规则校验;
•当后续的 slot 通过,没有抛出 BlockException 异常,说明该资源被成功调用,则增加执行线程数和通过的请求数等信息。
关于数据统计,主要会牵扯到 ArrayMetric、BucketLeapArray、MetricBucket、WindowWrap 等类。
项目结构
以下主要分析core包里的内容
2.1注解入口
2.1.1 Entry、Context、Node
SphU门面类的方法出参都是Entry,Entry可以理解为每次进入资源的一个凭证,如果调用SphO.entry()或者SphU.entry()能获取Entry对象,代表获取了凭证,没有被限流,否则抛出一个BlockException。
Entry中持有本次对资源调用的相关信息:
•createTime:创建该Entry的时间戳。
•curNode:Entry当前是在哪个节点。
•orginNode:Entry的调用源节点。
•resourceWrapper:Entry关联的资源信息。
Entry是一个抽象类,CtEntry是Entry的实现,CtEntry持有Context和调用链的信息
Context的源码注释如下,
kotlin
复制代码
This class holds metadata of current invocation
Node的源码注释
sql
复制代码
Holds real-time statistics for resources
Node中保存了对资源的实时数据的统计,Sentinel中的限流或者降级等功能就是通过Node中的数据进行判断的。Node是一个接口,里面定义了各种操作request、exception、rt、qps、thread的方法。
在细看Node实现时,不难发现LongAddr的使用,关于LongAddr和DoubleAddr都是java8 java.util.concurrent.atomic里的内容,感兴趣的小伙伴可以再深入研究一下,这两个是高并发下计数功能非常优秀的数据结构,实际应用场景里需要计数时可以考虑使用。
关于Node的介绍后续还会深入,此处大致先提一下这个概念。
2.2 初始化
2.2.1 Context初始化
在初始化slot责任链部分前,还执行了context的初始化,里面涉及几个重要概念,需要解释一下:
可以发现在Context初始化的过程中,会把EntranceNode加入到Root子节点中(实际Root本身是一个特殊的EntranceNode),并把EntranceNode放到contextNameNodeMap中。
之前简单提到过Node,是用来统计数据用的,不同Node功能如下:
•Node:用于完成数据统计的接口
•StatisticNode:统计节点,是Node接口的实现类,用于完成数据统计
•EntranceNode:入口节点,一个Context会有一个入口节点,用于统计当前Context的总体流量数据
•DefaultNode:默认节点,用于统计一个资源在当前Context中的流量数据
•ClusterNode:集群节点,用于统计一个资源在所有Context中的总体流量数据
ini
复制代码
protected static Context trueEnter(String name, String origin) { Context context = contextHolder.get(); if (context == null) { Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap; DefaultNode node = localCacheNameMap.get(name); if (node == null) { if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { setNullContext(); return NULL_CONTEXT; } else { LOCK.lock(); try { node = contextNameNodeMap.get(name); if (node == null) { if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { setNullContext(); return NULL_CONTEXT; } else { node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null); // Add entrance node. Constants.ROOT.addChild(node); Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1); newMap.putAll(contextNameNodeMap); newMap.put(name, node); contextNameNodeMap = newMap; } } } finally { LOCK.unlock(); } } } context = new Context(node, name); context.setOrigin(origin); contextHolder.set(context); } return context; }
2.2.2 通过SpiLoader默认初始化8个slot
每个slot的主要职责如下:
•NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
•ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
•StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;
•FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
•AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
•DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级;
•SystemSlot 则通过系统的状态,例如 集群QPS、线程数、RT、负载 等,来控制总的入口流量;
2.3 StatisticSlot
2.3.1 Node
深入看一下Node,因为统计信息都在里面,后面不论是限流、熔断、负载保护等都是结合规则+统计信息判断是否要执行
从Node的源码注释看,它会持有资源维度的实时统计数据,以下是接口里的方法定义,可以看到totalRequest、totalPass、totalSuccess、blockRequest、totalException、passQps等很多request、qps、thread的相关方法:
csharp
复制代码
/** * Holds real-time statistics for resources. * * @author qinan.qn * @author leyou * @author Eric Zhao */ public interface Node extends OccupySupport, DebugSupport { long totalRequest(); long totalPass(); long totalSuccess(); long blockRequest(); long totalException(); double passQps(); double blockQps(); double totalQps(); double successQps(); …… }
2.3.2 StatisticNode
我们先从最基础的StatisticNode开始看,源码给出的定位是:
less
复制代码
The statistic node keep three kinds of real-time statistics metrics: metrics in second level ({@code rollingCounterInSecond}) metrics in minute level ({@code rollingCounterInMinute}) thread count
StatisticNode只有四个属性,除了之前提到过的LongAddr类型的curThreadNum外,还有两个属性是Metric对象,通过入参已经属性命名可以看出,一个用于秒级,一个用于分钟级统计。接下来我们就要看看Metric
java
复制代码
// StatisticNode持有两个Metric,一个秒级一个分钟级,由入参可知,秒级统计划分了两个时间窗口,窗口程度是500ms private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT, IntervalProperty.INTERVAL); // 分钟级统计划分了60个时间窗口,窗口长度是1000ms private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false); /** * The counter for thread count. */ private LongAdder curThreadNum = new LongAdder(); /** * The last timestamp when metrics were fetched. */ private long lastFetchTime = -1;
ArrayMetric只有一个属性LeapArray,其余都是用于统计的方法,LeapArray是sentinel中统计最基本的数据结构,这里有必要详细看一下,总体就是根据timeMillis去获取一个bucket,分为:没有创建、有直接返回、被废弃后的reset三种场景。
java
复制代码
//以分钟级的统计属性为例,看一下时间窗口初始化过程 private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false); public LeapArray(int sampleCount, int intervalInMs) { AssertUtil.isTrue(sampleCount > 0, "bucket count is invalid: " + sampleCount); AssertUtil.isTrue(intervalInMs > 0, "total time interval of the sliding window should be positive"); AssertUtil.isTrue(intervalInMs % sampleCount == 0, "time span needs to be evenly divided"); // windowLengthInMs = 60*1000 / 60 = 1000 滑动窗口时间长度,可见sentinel默认将单位时间分为了60