你所不知道的Sentinel核心源码剖析,来吧!抓紧出坑!
前言
之前介绍了Sentinel基本的应用,以及对Sentinel的改造;今天老顾来介绍Sentinel的源码,可以让我们对Sentinel机制会更加深入的了解。
我们知道可以通过Sentinel控制台进行降级限流的规则设置,也可以通过Api的方式进行设置,之前文章介绍过通过Api方式进行降级限流设置。
本质上Sentinel控制台进行设置的,最终也是通过Api进行设置的。
到底针对哪些请求/方法进行规则限制,Sentinel提供两种埋点方式:
1)try-catch 方式(通过 SphU.entry(...)),用户在 catch 块中执行异常处理 / fallback。
Entry entry = null;
try {
entry = SphU.entry(KEY); //定义执行名称
//todo 业务代码
System.out.println("entry ok...");
} catch (BlockException e1) {
// 降级、限流异常
// todo fallback处理
} catch (Exception e2) {
// 业务异常 exception
} finally {
if (entry != null) {
entry.exit();
}
}
2)if-else 方式(通过 SphO.entry(...)),当返回 false 时执行异常处理 / fallback
Entry entry = null;
if (SphO.entry(KEY)) {
//todo 业务代码
System.out.println("entry ok");
} else {
// 降级、限流异常
// todo fallback处理
}
针对不同的应用,Sentinel提供了不同的adapter适配器
不同adapter最主要就是要实现埋点,本质就是用上面的埋点Api,只要引入对应的adapter就能够达到基本常用的埋点了,不需要我们自行去定义了。
工作原理
在Sentinel里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个Entry对象。
Entry可以通过对主流框架的适配自动创建(就是上面说的adapter),也可以通过注解的方式或调用 SphU API 显式创建。
slot插槽
Entry创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责,例如默认情况下会创建一下8个插槽:
NodeSelectorSlot负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
ClusterBuilderSlot则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
LogSlot就是打印异常日志
StatisticSlot则用于记录、统计不同纬度的 runtime 指标监控信息;
AuthoritySlot则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
SystemSlot则通过系统的状态,例如 load1 等,来控制总的入口流量
FlowSlot则用于根据预设的限流规则以及前面slot统计的状态,来进行流量控制;
DegradeSlot则通过统计信息以及预设的规则,来做熔断降级;
注意:这里的插槽链都是一一对应资源名称的
对应的插槽Sentinel源码配置
上面介绍的插槽是Sentinle重要的概念,还有一个重要的概念node,我们来说明一下。
Node节点
node中保存了资源的实时统计数据,例如:passQps,blockQps,rt等实时数据。正是有了这些统计数据后,sentinel才能进行限流、降级等一系列的操
作。
node是一个接口,他有一个实现类:StatisticNode,但是StatisticNode本身也有两个子类,一个是DefaultNode,另一个是ClusterNode,DefaultNode又有一个子类叫EntranceNode。
其中entranceNode是每个上下文的入口,该节点是直接挂在root下的,是全局唯一的,每一个context都会对应一个entranceNode。另外defaultNode是记录当前调用的实时数据的,每个defaultNode都关联着一个资源和clusterNode,有着相同资源的defaultNode,他们关联着同一个clusterNode。
Metric
metric是sentinel中用来进行实时数据统计的度量接口,node就是通过metric来进行数据统计的。而metric本身也并没有统计的能力,他也是通过Window来进行统计的。
Metric有一个实现类:ArrayMetric,在ArrayMetric中主要是通过一个叫WindowLeapArray的对象进行窗口统计的
滑动窗口
我们下面看看Sentinel核心的数据统计是怎么做的,如何达到高性能的统计?核心就是利用了滑动时间窗口的巧妙的设计。
时间窗口是用WindowWrap对象表示的,其属性如下
sentinel时间基准由tick线程来做,每1ms更新一次时间基准,逻辑如下:
sentinel默认有每秒和每分钟的滑动窗口,对应的LeapArray类型,它们的初始化逻辑是:
protected int windowLengthInMs; // 单个滑动窗口时间值
protected int sampleCount; // 滑动窗口个数
protected int intervalInMs; // 周期值(相当于所有滑动窗口时间值之和)
public LeapArray(int sampleCount, int intervalInMs) {
this.windowLengthInMs = intervalInMs / sampleCount;
this.intervalInMs = intervalInMs;
this.sampleCount = sampleCount;
this.array = new AtomicReferenceArray<WindowWrap<T>>(sampleCount);
}
Sentinel提供了2个维度,一个是秒级别、一个分钟级别
针对每秒滑动窗口,windowLengthInMs=500,sampleCount=2,intervalInMs=1000
针对每分钟滑动窗口,windowLengthInMs=1000,sampleCount=60,intervalInMs=60000
对应代码:
currentTimeMillis时间基准(tick线程)每1ms更新一次,通过currentWindow(timeMillis)方法获取当前时间点对应的WindowWrap对象,然后更新对应的各种指标,用于做限流、降级时使用。
画图理解
我们拿每秒维度举个例子,
初始的时候arrays数组中只有一个窗口(可能是第一个,也可能是第二个),每个时间窗口的长度是500ms,这就意味着只要当前时间与时间窗口的差值在500ms之内,时间窗口就不会向前滑动。当前窗口current window还指向Arrays的第一个窗口。例如,假如当前时间走到300或者500时,当前时间窗口current window仍然是相同的那个
时间继续往前走,当超过500ms时,时间窗口就会向前滑动到下一个,这时就会更新当前窗口的开始时间(windowStart):
时间继续往前走,只要不超过1000ms,则当前窗口不会发生变化:
当时间继续往前走,当前时间超过1000ms时,就会再次进入下一个时间窗口,此时arrays数组中的窗口将会有一个失效,会有另一个新的窗口进行替换:
以此类推随着时间的流逝,时间窗口也在发生变化,在当前时间点中进入的请求,会被统计到当前时间对应的时间窗口中。计算qps时,会用当前采样的时间窗口中对应的指标统计值除以时间间隔,就是具体的qps。具体的代码在StatisticNode中:
上面用图的方式介绍了,滑动窗口时间。这边在提供一份网上模拟滑动窗口给的代码:
public static void main(String[] args) throws
InterruptedException {
int windowLength = 500;
int arrayLength = 2;
calculate(windowLength,arrayLength);
Thread.sleep(100);
calculate(windowLength,arrayLength);
Thread.sleep(200);
calculate(windowLength,arrayLength);
Thread.sleep(200);
calculate(windowLength,arrayLength);
Thread.sleep(500);
calculate(windowLength,arrayLength);
Thread.sleep(500);
calculate(windowLength,arrayLength);
Thread.sleep(500);
calculate(windowLength,arrayLength);
Thread.sleep(500);
calculate(windowLength,arrayLength);
Thread.sleep(500);
calculate(windowLength,arrayLength);
}
private static void calculate(int windowLength,int arrayLength){
long time = System.currentTimeMillis();
long timeId = time/windowLength;
long currentWindowStart = time - time % windowLength;
int idx = (int)(timeId % arrayLength);
System.out.println("time="+time+",currentWindowStart="+currentWindowStart+",timeId="+timeId+",idx="+idx);
}
这里假设时间窗口的长度为500ms,数组的大小为2,当前时间作为输入参数,计算出当前时间窗口的timeId、windowStart、idx等值。执行上面的代码后,将打印出如下的结果:
可以看出来,currentWindowStart每增加500ms,timeId就加1,这时就是时间窗口发生滑动的时候。
总结
介绍到这里,关于Sentinel的基本实现原理都讲了,具体怎么代码实现,小伙伴们可以去看源码调试看看。到了这里我们已经介绍了Sentinel很多相关的知识了。
那是不是我们就可以用到生产环境呢?老顾告诉大家还少一个重要的东西,没了这个东西还是不能在生产环境应用自如,具体是什么东西呢?下篇文章老顾继续介绍。谢谢!!!