上篇文章中,我们了解了sentinel是如何构造资源调用链的,以及每种Slot的具体作用,其中最重要的一个Slot非StatisticSlot莫属,因为他做的事是其他所有的Slot的基础。包括各种限流,熔断的规则,都是基于StatisticSlot统计出来的结果进行规则校验的。本篇文章我将深入研究下sentinel是如何进行qps等指标的统计的,首先要确定的一点是,sentinel是基于滑动时间窗口来实现的。

化整为零

我们已经知道了Slot是从第一个往后一直传递到最后一个的,且当信息传递到StatisticSlot时,这里就开始进行统计了,统计的结果又会被后续的Slot所采用,作为规则校验的依据。我们先来看一段非常熟悉的代码,就是StatisticSlot中的entry方法:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {try {// 触发下一个Slot的entry方法fireEntry(context, resourceWrapper, node, count, args);// 如果能通过SlotChain中后面的Slot的entry方法,说明没有被限流或降级// 统计信息node.increaseThreadNum();node.addPassRequest();// 省略部分代码} catch (BlockException e) {context.getCurEntry().setError(e);// Add block count.node.increaseBlockedQps();// 省略部分代码throw e;} catch (Throwable e) {context.getCurEntry().setError(e);// Should not happennode.increaseExceptionQps();// 省略部分代码throw e;}
}

上面的代码注释写的已经很清晰了,简单的来说,StatisticSlot中就是做了三件事:

  • 1.通过node中的当前的实时统计指标信息进行规则校验
  • 2.如果通过了校验,则重新更新node中的实时指标数据
  • 3.如果被block或出现了异常了,则重新更新node中block的指标或异常指标

从上面的代码中可以很清晰的看到,所有的实时指标的统计都是在node中进行的。这里我们拿qps的指标进行分析,看sentinel是怎么统计出qps的,这里可以事先透露下他是通过滑动时间窗口来统计的,而滑动窗口就是本篇文章的重点。

DefaultNode和ClusterNode

我们可以看到 node.addPassRequest() 这段代码是在fireEntry执行之后执行的,这意味着,当前请求通过了sentinel的流控等规则,此时需要将当次请求记录下来,也就是执行 node.addPassRequest() 这行代码,现在我们进入这个代码看看。具体的代码如下所示:

@Override
public void addPassRequest() {super.addPassRequest();this.clusterNode.addPassRequest();
}

首先我们知道这里的node是一个 DefaultNode 实例,这里特别补充一个 DefaultNodeClusterNode 的区别:

DefaultNode:保存着某个resource在某个context中的实时指标,每个DefaultNode都指向一个ClusterNode

ClusterNode:保存着某个resource在所有的context中实时指标的总和,同样的resource会共享同一个ClusterNode,不管他在哪个context中

StatisticNode

好了,知道了他们的区别后,我们再来看上面的代码,其实都是执行的 StatisticNode 对象的 addPassRequest 方法。进入这个方法中看下具体的代码:


private transient Metric rollingCounterInSecond = new ArrayMetric(1000 / SampleCountProperty.sampleCount, IntervalProperty.INTERVAL);private transient Metric rollingCounterInMinute = new ArrayMetric(1000, 2 * 60);@Override
public void addPassRequest() {rollingCounterInSecond.addPass();rollingCounterInMinute.addPass();
}

Metric

从代码中我们可以看到,具体的增加pass指标是通过一个叫 Metric 的接口进行操作的,并且是通过 ArrayMetric 这种实现类,现在我们在进入 ArrayMetric 中看一下。具体的代码如下所示:

private final WindowLeapArray data;public ArrayMetric(int windowLength, int interval) {this.data = new WindowLeapArray(windowLength, interval);
}@Override
public void addPass() {WindowWrap<Window> wrap = data.currentWindow();wrap.value().addPass();
}

LeapArray和Window

本以为在ArrayMetric中应该可以看到具体的统计操作了,谁知道又出现了一个叫 WindowLeapArray 的类,不过从名字上看有点 「窗口」 的意思了。继续跟代码,发现 wrap.value().addPass() 是执行的 wrap 对象所包装的 Window 对象的 addPass 方法,这里就是最终的增加qps中q的值的地方了。进入 Window 类中看一下,具体的代码如下:

private final LongAdder pass = new LongAdder();
private final LongAdder block = new LongAdder();
private final LongAdder exception = new LongAdder();
private final LongAdder rt = new LongAdder();
private final LongAdder success = new LongAdder();public void addPass() {pass.add(1L);
}
public void addException() {exception.add(1L);
}
public void addBlock() {block.add(1L);
}
public void addSuccess() {success.add(1L);
}
public void addRT(long rt) {this.rt.add(rt);// Not thread-safe, but it's okay.if (rt < minRt) {minRt = rt;}
}

看到这里是不是就放心了,原来 Window 是通过 LongAdder 来保存各种指标的值的,看到 LongAdder 是不是立刻就想到 AtomicLong 了?但是这里为什么不用 AtomicLong ,而是用 LongAdder 呢?主要是 LongAdder 在高并发下有更好的吞吐量,代价是花费了更多的空间,典型的以空间换时间。

完整的流程

分析到这里我们已经把指标统计的完整链路理清楚了,可以用下面这张图来表示整个过程:

add-pass-request.png

有人可能会问了,你不是要分析滑动窗口的吗?搞了半天只画了一张图,而且图上还多了一个 timeId 之类的东西,这个根本没在上面出现过。

好了,现在我们就可以来分析具体的滑动窗口了,这里的 timeId 是用来表示一个 WindowWrap 对象的时间id。为什么要用 timeId 来表示呢?我们可以看到每一个 WindowWrap 对象由三个部分组成:

  • windowStart: 时间窗口的开始时间,单位是毫秒
  • windowLength: 时间窗口的长度,单位是毫秒
  • value: 时间窗口的内容,在 WindowWrap 中是用泛型表示这个值的,但实际上就是 Window

我们先大致的了解下时间窗口的构成,后面会再来分析 timeId 的作用。首先一个时间窗口是用来在某个固定时间长度内保存一些统计值的虚拟概念。有了这个概念后,我们就可以通过时间窗口来计算统计一段时间内的诸如:qps,rt,threadNum等指标了。

继续深入

我们再回到 ArrayMetric 中看一下:

private final WindowLeapArray data;public ArrayMetric(int windowLength, int interval) {this.data = new WindowLeapArray(windowLength, interval);
}

首先创建了一个 WindowLeapArray 对象,看一下 WindowLeapArray 类的代码:

public class WindowLeapArray extends LeapArray<Window> {public WindowLeapArray(int windowLengthInMs, int intervalInSec) {super(windowLengthInMs, intervalInSec);}
}

该对象的构造方法有两个参数:

  • windowLengthInMs :一个用毫秒做单位的时间窗口的长度
  • intervalInSec ,一个用秒做单位的时间间隔,这个时间间隔具体是做什么的,下面会分析。

然后 WindowLeapArray 继承自 LeapArray ,在初始化 WindowLeapArray 的时候,直接调用了父类的构造方法,再来看一下父类 LeapArray 的代码:

public abstract class LeapArray<T> {// 时间窗口的长度protected int windowLength;// 采样窗口的个数protected int sampleCount;// 以毫秒为单位的时间间隔protected int intervalInMs;// 采样的时间窗口数组protected AtomicReferenceArray<WindowWrap<T>> array;/*** LeapArray对象* @param windowLength 时间窗口的长度,单位:毫秒* @param intervalInSec 统计的间隔,单位:秒*/public LeapArray(int windowLength, int intervalInSec) {this.windowLength = windowLength;// 时间窗口的采样个数,默认为2个采样窗口this.sampleCount = intervalInSec * 1000 / windowLength;this.intervalInMs = intervalInSec * 1000;this.array = new AtomicReferenceArray<WindowWrap<T>>(sampleCount);}
}

可以很清晰的看出来在 LeapArray 中创建了一个 AtomicReferenceArray 数组,用来对时间窗口中的统计值进行采样。通过采样的统计值再计算出平均值,就是我们需要的最终的实时指标的值了。

可以看到我在上面的代码中通过注释,标明了默认采样的时间窗口的个数是2个,这个值是怎么得到的呢?我们回忆一下 LeapArray 对象创建,是通过在 StatisticNode 中,new了一个 ArrayMetric ,然后将参数一路往上传递后创建的:

private transient Metric rollingCounterInSecond = new ArrayMetric(1000 / SampleCountProperty.sampleCount,IntervalProperty.INTERVAL);

SampleCountProperty.sampleCount 的默认值是2,所以第一个参数 windowLengthInMs 的值是 500ms,那么1秒钟是1000ms,每个时间窗口的长度是500ms,也就是说总共分了两个采样的时间窗口。

现在继续回到 ArrayMetric.addPass() 方法:

@Override
public void addPass() {WindowWrap<Window> wrap = data.currentWindow();wrap.value().addPass();
}

获取当前Window

我们已经分析了 wrap.value().addPass() ,现在只需要分析清楚 data.currentWindow() 具体做了什么,拿到了当前时间窗口就可以 了。继续深入代码,最终定位到下面的代码:

@Override
public WindowWrap<Window> currentWindow(long time) {long timeId = time / windowLength;// Calculate current index.int idx = (int)(timeId % array.length());// Cut the time to current window start.long time = time - time % windowLength;while (true) {WindowWrap<Window> old = array.get(idx);if (old == null) {WindowWrap<Window> window = new WindowWrap<Window>(windowLength, time, new Window());if (array.compareAndSet(idx, null, window)) {return window;} else {Thread.yield();}} else if (time == old.windowStart()) {return old;} else if (time > old.windowStart()) {if (addLock.tryLock()) {try {// if (old is deprecated) then [LOCK] resetTo currentTime.return resetWindowTo(old, time);} finally {addLock.unlock();}} else {Thread.yield();}} else if (time < old.windowStart()) {// Cannot go through here.return new WindowWrap<Window>(windowLength, time, new Window());}}
}

初次看到这段代码时,可能会觉得有点懵,但是细细的分析一下,实际可以把他分成以下几步:

  • 1.根据当前时间,算出该时间的timeId,并根据timeId算出当前窗口在采样窗口数组中的索引idx
  • 2.根据当前时间算出当前窗口的应该对应的开始时间time,以毫秒为单位
  • 3.根据索引idx,在采样窗口数组中取得一个时间窗口old
  • 4.循环判断知道获取到一个当前时间窗口
    • 4.1.如果old为空,则创建一个时间窗口,并将它插入到array的第idx个位置,array上面已经分析过了,是一个 AtomicReferenceArray
    • 4.2.如果当前窗口的开始时间time与old的开始时间相等,那么说明old就是当前时间窗口,直接返回old
    • 4.3.如果当前窗口的开始时间time大于old的开始时间,则说明old窗口已经过时了,将old的开始时间更新为最新值:time,下个循环中会在步骤4.2中返回
    • 4.4.如果当前窗口的开始时间time小于old的开始时间,实际上这种情况是不可能存在的,因为time是当前时间,old是过去的一个时间

上面的代码有个比较容易混淆的地方,就是计算出来的当前时间窗口的开始时间,没有使用一个新的变量来表示,而是直接用time来表示。

另外timeId是会随着时间的增长而增加,当前时间每增长一个windowLength的长度,timeId就加1。但是idx不会增长,只会在0和1之间变换,因为array数组的长度是2,只有两个采样时间窗口。至于为什么默认只有两个采样窗口,个人觉得因为sentinel是比较轻量的框架。时间窗口中保存着很多统计数据,如果时间窗口过多的话,一方面会占用过多内存,另一方面时间窗口过多就意味着时间窗口的长度会变小,如果时间窗口长度变小,就会导致时间窗口过于频繁的滑动。

经过分析,加上注释,并将表示当前窗口开始时间的time变量,重命名成其他变量,使得代码更具可读性,调整后的代码如下:

@Override
public WindowWrap<Window> currentWindow(long time) {// time每增加一个windowLength的长度,timeId就会增加1,时间窗口就会往前滑动一个long timeId = time / windowLength;// Calculate current index.// idx被分成[0,arrayLength-1]中的某一个数,作为array数组中的索引int idx = (int)(timeId % array.length());// Cut the time to current window start.long currentWindowStart = time - time % windowLength;while (true) {// 从采样数组中根据索引获取缓存的时间窗口WindowWrap<Window> old = array.get(idx);// array数组长度不宜过大,否则old很多情况下都命中不了,就会创建很多个WindowWrap对象if (old == null) {// 如果没有获取到,则创建一个新的WindowWrap<Window> window = new WindowWrap<Window>(windowLength, currentWindowStart, new Window());// 通过CAS将新窗口设置到数组中去if (array.compareAndSet(idx, null, window)) {// 如果能设置成功,则将该窗口返回return window;} else {// 否则当前线程让出时间片,等待Thread.yield();}// 如果当前窗口的开始时间与old的开始时间相等,则直接返回old窗口} else if (currentWindowStart == old.windowStart()) {return old;// 如果当前时间窗口的开始时间已经超过了old窗口的开始时间,则放弃old窗口// 并将time设置为新的时间窗口的开始时间,此时窗口向前滑动} else if (currentWindowStart > old.windowStart()) {if (addLock.tryLock()) {try {// if (old is deprecated) then [LOCK] resetTo currentTime.return resetWindowTo(old, currentWindowStart);} finally {addLock.unlock();}} else {Thread.yield();}// 这个条件不可能存在} else if (currentWindowStart < old.windowStart()) {// Cannot go through here.return new WindowWrap<Window>(windowLength, currentWindowStart, new Window());}}
}

看图理解

为了更好的理解,下面我用几幅图来描述下这个过程。

slide-window-1.png

初始的时候arrays数组中只有一个窗口(可能是第一个,也可能是第二个),每个时间窗口的长度是500ms,这就意味着只要当前时间与时间窗口的差值在500ms之内,时间窗口就不会向前滑动。例如,假如当前时间走到300或者500时,当前时间窗口仍然是相同的那个:

slide-window-2.png

时间继续往前走,当超过500ms时,时间窗口就会向前滑动到下一个,这时就会更新当前窗口的开始时间:

slide-window-3.png

时间继续往前走,只要不超过1000ms,则当前窗口不会发生变化:

slide-window-4.png

当时间继续往前走,当前时间超过1000ms时,就会再次进入下一个时间窗口,此时arrays数组中的窗口将会有一个失效,会有另一个新的窗口进行替换:

slide-window-5.png

以此类推随着时间的流逝,时间窗口也在发生变化,在当前时间点中进入的请求,会被统计到当前时间对应的时间窗口中。计算qps时,会用当前采样的时间窗口中对应的指标统计值除以时间间隔,就是具体的qps。具体的代码在StatisticNode中:

@Override
public long totalQps() {return passQps() + blockedQps();
}@Override
public long blockedQps() {return rollingCounterInSecond.block() / IntervalProperty.INTERVAL;
}@Override
public long passQps() {return rollingCounterInSecond.pass() / IntervalProperty.INTERVAL;
}

到这里就基本上把滑动窗口的原理分析清楚了,还有不清楚的地方,最好能够借助代码继续分析下,最好的做法就是debug,这里贴一下笔者在分析 currentWindow 方法时采取的测试代码:

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等值。执行上面的代码后,将打印出如下的结果:

time=1540629334619,currentWindowStart=1540629334500,timeId=3081258669,idx=1
time=1540629334721,currentWindowStart=1540629334500,timeId=3081258669,idx=1
time=1540629334924,currentWindowStart=1540629334500,timeId=3081258669,idx=1
time=1540629335129,currentWindowStart=1540629335000,timeId=3081258670,idx=0
time=1540629335633,currentWindowStart=1540629335500,timeId=3081258671,idx=1
time=1540629336137,currentWindowStart=1540629336000,timeId=3081258672,idx=0
time=1540629336641,currentWindowStart=1540629336500,timeId=3081258673,idx=1
time=1540629337145,currentWindowStart=1540629337000,timeId=3081258674,idx=0
time=1540629337649,currentWindowStart=1540629337500,timeId=3081258675,idx=1

可以看出来,windowStart每增加500ms,timeId就加1,这时就是时间窗口发生滑动的时候。

Sentinel 原理:滑动窗口相关推荐

  1. Sentinel 原理-全解析

    系列文章 Sentinel 原理-调用链 Sentinel 原理-滑动窗口 Sentinel 原理-实体类 Sentinel 实战-限流篇 Sentinel 实战-控制台篇 Sentinel 实战-规 ...

  2. Sentinel 滑动窗口实现原理 侵删

    要实现限流.熔断等功能,首先要解决的问题是如何实时采集服务(资源)调用信息.例如将某一个接口设置的限流阔值 1W/tps,那首先如何判断当前的 TPS 是多少?Alibaba Sentinel 采用滑 ...

  3. Sentinel源码解析之滑动窗口

    原创不易,转载请注明出处 文章目录 前言 1.sentinel滑动窗口实现原理 2.sentinel使用滑动窗口都统计啥 3.滑动窗口源码实现 3.1 MetricBucket 3.2 WindowW ...

  4. 微服务限流及熔断一:四种限流算法(计数器算法、滑动窗口算法、令牌限流算法、漏桶限流算法)

    引言 本篇内容根据<spring cloud alibaba 微服务原理与实战>中内容摘取,希望和大家分享限流的思想,本篇不涉及代码层面的实现. 限流的目的 目的:通过限制并发访问数或者限 ...

  5. 网络通信-滑动窗口协议-SWP

    本篇主要讲解:网络通信-滑动窗口协议-SWP 参考链接1 参考链接2 参考链接3 目录 文章目录 网络通信-滑动窗口协议-SWP TCP可靠传输 窗口介绍 窗口和缓存的关系 功能作用 窗口大小 工作原 ...

  6. sentinel 时间窗口_Sentinel滑动窗口算法

    在前面搞清楚了Sentinel的使用后,大致理了一下Sentinel的责任链,搞清楚了这个,基本就已经梳理清楚sentinel-core模块的大部分内容,顺着这条链路可以继续梳理很多东西. 知其然.知 ...

  7. Sentinel限流及其滑动窗口算法

    Sentinel限流及其滑动窗口算法 Sentinel的限流原理 滑动时间窗口算法 Sentinel的限流原理 限流效果,对应有DefaultController快速失败 WarmUpControll ...

  8. TCP滑动窗口(Sliding Window)原理

    ---------------- 版权声明:本文为CSDN博主「CQ小子」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明. 原文链接:https://blog.csd ...

  9. TCP滑动窗口原理终于清楚了!

    我们在学习计算机网络的时候,遇到很多知识点.即便是背的滚瓜烂熟,让你去辨别知识点背后的深层逻辑的时候,可能就手足无措了. 比如小邱去面A公司的时候就被问到: 事实上,这个问题很大程度弥补我计算机网络的 ...

最新文章

  1. Forbid consumer 192.168.85.1 access service com.sharearn.dubbo.romote.TestService from registry
  2. Datagridview 去掉单元格焦点虚框 和设置单元格不能被选中
  3. iOS tableViewCell自适应高度 第三发类库
  4. Java集合篇:HashSet
  5. java动态添加属性_java – 动态添加对象的属性
  6. 容器和容器镜像的区别,您真的了解吗
  7. ios 下拉放大 上拉缩小_为啥鞠婧祎发量这么多?截图放大十倍她的“发缝”,网友:真密集...
  8. feiq肆无忌惮欺压同行
  9. POJ 3172 (认真读题的题)
  10. slice,Array.prototype.slice,Array.protyotype.slice.call
  11. 10本畅销全球的技术经典,这次整个大的
  12. ATL 线程触发事件解决方案
  13. ubuntu 修改ip后,老ip仍然存在的问题
  14. 苹果app代码行数统计
  15. c语言单片机编程教学大纲,单片机应用技术[C语言]教学大纲.doc
  16. hadoop大数据工程师、数据开发工程师、数据仓库工程师 面试题目分享
  17. 完美解决Idea unable to access git 错误
  18. xp php mysql_WindowsXP+IIS+PHP5+MySQL5+Zend+GD库+phpMyAdmin+PHPW
  19. Android Studio App开发之下载管理器DownloadManager中显示、轮询下载进度、利用POST上传文件讲解及实战(附源码)
  20. 基于89C51单片机的智能小车——07.语音识别切换寻迹避障跟随并Oled显示

热门文章

  1. intel bsf指令
  2. 词向量之词袋模型(BOW)详解
  3. 敲门代码python_适用于Linux笔记本电脑的基于敲门的命令
  4. # 设置防火墙白名单
  5. java启动100线程_Java-多线程
  6. 极进网络将收购斑马技术的无线局域网业务并获蓝筹客户
  7. 麒麟系统安装clickhouse
  8. ZENCART首页显示 Featured Categorie的方法
  9. 云服务器文件777权限,云服务器目录权限设置
  10. 保护电路系列之过流过压过温保护