你所不知道的Sentinel核心源码剖析,来吧!抓紧出坑!

前言

之前介绍了Sentinel基本的应用,以及对Sentinel的改造;今天来介绍Sentinel的源码,可以让我们对Sentinel机制会更加深入的了解!同时用XMind画了一张导图记录Spring Cloud Alibaba的学习笔记(源文件对部分节点有详细备注和参考资料,由于太大就没展示全部,欢迎关注我的公众号:阿风的架构笔记 后台发送【导图】拿下载链接, 已经完善更新):

我们知道可以通过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很多相关的知识了

那是不是我们就可以用到生产环境呢?告诉大家还少一个重要的东西,没了这个东西还是不能在生产环境应用自如,具体是什么东西呢?下篇文章继续介绍。谢谢!!!

看完三件事❤️


如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 阿风的架构笔记 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀
  4. 关注后回复【666】扫码即可获取架构进阶学习资料包
posted @ 2021-04-15 14:52  阿风的架构笔记  阅读(642)  评论(0编辑  收藏  举报