龙空技术网

你了解滑动时间窗口吗?Sentinel核心源码剖析

老顾聊技术 5476

前言:

如今兄弟们对“滑动窗口基本原理”大概比较关怀,朋友们都想要分析一些“滑动窗口基本原理”的相关资讯。那么小编同时在网上搜集了一些关于“滑动窗口基本原理””的相关内容,希望兄弟们能喜欢,你们快快来了解一下吧!

目录前言工作原理滑动窗口总结前言

之前介绍了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很多相关的知识了。

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

推荐阅读

Sentinel全局Feign默认熔断降级策略的思考

你所不知道的头部参数传递的坑,来吧!抓紧出坑

5分钟让你理解K8S必备架构概念,以及网络模型(一)

5分钟让你理解K8S必备架构概念,以及网络模型(二)

5分钟让你理解K8S必备架构概念,以及网络模型(三)

大厂如何基于binlog解决多机房同步mysql数据(一)?

大厂如何基于binlog解决多机房同步mysql数据(二)?

基于binlog的canal组件有哪些使用场景(三)?

基于binlog日志之canal企业应用及高可用原理(四)?

可用于大型应用的微服务生态灰度发布如何实现?

一线大厂级别公共Redis集群监控,细化到每个项目实例

Sharding-jdbc的实战入门之水平分表(一)

Sharding-Jdbc之水平分库和读写分离(二)

标签: #滑动窗口基本原理