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

温馨提示:如果对源码不太感兴趣,可以先跳到文末,看一下滑动窗口的设计原理图,再决定是否需要阅读源码。

@

1、滑动窗口核心类图



我们先对上述核心类做一个简单的介绍,重点关注核心类的作用与核心属性(重点需要探究其核心数据结构)。

  • Metric

    指标收集核心接口,主要定义一个滑动窗口中成功的数量、异常数量、阻塞数量,TPS、响应时间等数据。
  • ArrayMetric

    滑动窗口核心实现类。
  • LeapArray

    滑动窗口顶层数据结构,包含一个一个的窗口数据。
  • WindowWrap

    每一个滑动窗口的包装类,其内部的数据结构用 MetricBucket 表示。
  • MetricBucket

    指标桶,例如通过数量、阻塞数量、异常数量、成功数量、响应时间,已通过未来配额(抢占下一个滑动窗口的数量)。
  • MetricEvent

    指标类型,例如通过数量、阻塞数量、异常数量、成功数量、响应时间等。

2、滑动窗口实现原理

2.1 ArrayMetric

滑动窗口的入口类为 ArrayMetric ,我们先来看一下其核心代码。

private final LeapArray<MetricBucket> data;   // @1
public ArrayMetric(int sampleCount, int intervalInMs) { // @2
this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
}
public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) { // @3
if (enableOccupy) {
this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
} else {
this.data = new BucketLeapArray(sampleCount, intervalInMs);
}
}

代码@1:ArrayMetric 类唯一的属性,用来存储各个窗口的数据,这个是接下来我们探究的重点。

代码@2,代码@3 该类提供了两个构造方法,其核心参数为:

  • int intervalInMs

    表示一个采集的时间间隔,例如1秒,1分钟。
  • int sampleCount

    在一个采集间隔中抽样的个数,默认为 2,例如当 intervalInMs = 1000时,抽象两次,则一个采集间隔中会包含两个相等的区间,一个区间就是滑动窗口。
  • boolean enableOccupy

    是否允许抢占,即当前时间戳已经达到限制后,是否可以占用下一个时间窗口的容量,这里对应 LeapArray 的两个实现类,如果允许抢占,则为 OccupiableBucketLeapArray,否则为 BucketLeapArray。

注意,LeapArray 的泛型类为 MetricBucket,意思就是指标桶,可以认为一个 MetricBucket 对象可以存储一个抽样时间段内所有的指标,例如一个抽象时间段中通过数量、阻塞数量、异常数量、成功数量、响应时间,其实现的奥秘在 LongAdder 中,本文先不对该类进行详细介绍,后续文章会单独来探究其实现原理。

这次,我们先不去看子类,反其道而行,先去看看其父类。

2.2 LongAdder

2.2.1 类图与核心属性



LeapArray 的核心属性如下:

  • int windowLengthInMs

    每一个窗口的时间间隔,单位为毫秒。
  • int sampleCount

    抽样个数,就一个统计时间间隔中包含的滑动窗口个数,在 intervalInMs 相同的情况下,sampleCount 越多,抽样的统计数据就越精确,相应的需要的内存也越多。
  • int intervalInMs

    一个统计的时间间隔。
  • AtomicReferenceArray<WindowWrap< T>> array

    一个统计时间间隔中滑动窗口的数组,从这里也可以看出,一个滑动窗口就是使用的 WindowWrap< MetricBucket > 来表示。

上面的各个属性的含义是从其构造函数得出来的,请其看构造函数。

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");
this.windowLengthInMs = intervalInMs / sampleCount;
this.intervalInMs = intervalInMs;
this.sampleCount = sampleCount;
this.array = new AtomicReferenceArray<>(sampleCount);
}

那我们继续来看 LeapArray 中的方法,深入探究滑动窗口的实现细节。

2.2.2 currentWindow() 详解

该方法主要是根据当前时间来确定处于哪一个滑动窗口中,即找到上图中的 WindowWrap,该方法内部就是调用其重载方法,参数为系统的当前时间,故我们重点来看一下重载方法的实现。

public WindowWrap<T> currentWindow(long timeMillis) {
if (timeMillis < 0) {
return null;
}
int idx = calculateTimeIdx(timeMillis); // @1
long windowStart = calculateWindowStart(timeMillis); // @2
while (true) { // 死循环查找当前的时间窗口,这里之所有需要循环,是因为可能多个线程都在获取当前时间窗口。
WindowWrap<T> old = array.get(idx); // @3
if (old == null) { // @4
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
if (array.compareAndSet(idx, null, window)) { // @5
return window;
} else {
Thread.yield();
}
} else if (windowStart == old.windowStart()) { // @6
return old;
} else if (windowStart > old.windowStart()) { // @7
if (updateLock.tryLock()) {
try {
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
Thread.yield();
}
} else if (windowStart < old.windowStart()) { // @8
return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
}
}
}

代码@1:计算当前时间会落在一个采集间隔 ( LeapArray ) 中哪一个时间窗口中,即在 LeapArray 中属性 AtomicReferenceArray <WindowWrap< T>> array 的下标。其实现算法如下:

  • 首先用当前时间除以一个时间窗口的时间间隔,得出当前时间是多少个时间窗口的倍数,用 n 表示。
  • 然后我们可以看出从一系列时间窗口,从 0 开始,一起向前滚动 n 隔得到当前时间戳代表的时间窗口的位置。现在我们要定位到这个时间窗口的位置是落在 LeapArray 中数组的下标,而一个 LeapArray 中包含 sampleCount 个元素,要得到其下标,则使用 n % sampleCount 即可。

代码@2:计算当前时间戳所在的时间窗口的开始时间,即要计算出 WindowWrap 中 windowStart 的值,其实就是要算出小于当前时间戳,并且是 windowLengthInMs 的整数倍最大的数字,Sentinel 给出是算法为 ( timeMillis - timeMillis % windowLengthInMs )。

代码@3:尝试从 LeapArray 中的 WindowWrap 数组查找指定下标的元素。

代码@4:如果指定下标的元素为空,则需要创建一个 WindowWrap 。 其中 WindowWrap 中的 MetricBucket 是调用其抽象方法 newEmptyBucket (timeMillis),由不同的子类去实现。

代码@5:这里使用了 CAS 机制来更新 LeapArray 数组中的 元素,因为同一时间戳,可能有多个线程都在获取当前时间窗口对象,但该时间窗口对象还未创建,这里就是避免创建多个,导致统计数据被覆盖,如果用 CAS 更新成功的线程,则返回新建好的 WindowWrap ,CAS 设置不成功的线程继续跑这个流程,然后会进入到代码@6。

代码@6:如果指定索引下的时间窗口对象不为空并判断起始时间相等,则返回。

代码@7:如果原先存在的窗口开始时间小于当前时间戳计算出来的开始时间,则表示 bucket 已被弃用。则需要将开始时间重置到新时间戳对应的开始时间戳,重置的逻辑将在下文详细介绍。

代码@8:应该不会进入到该分支,因为当前时间算出来时间窗口不会比之前的小。

2.2.3 isWindowDeprecated() 详解

接下来我们来看一下窗口的过期机制。

public boolean isWindowDeprecated(/*@NonNull*/ WindowWrap<T> windowWrap) {
return isWindowDeprecated(TimeUtil.currentTimeMillis(), windowWrap);
}
public boolean isWindowDeprecated(long time, WindowWrap<T> windowWrap) {
return time - windowWrap.windowStart() > intervalInMs;
}

判断滑动窗口是否生效的依据是当系统时间与滑动窗口的开始时间戳的间隔大于一个采集时间,即表示过期。即从当前窗口开始,通常包含的有效窗口为 sampleCount 个有效滑动窗口。

2.2.4 getPreviousWindow() 详解

根据当前时间获取前一个有效滑动窗口,其代码如下:

public WindowWrap<T> getPreviousWindow(long timeMillis) {
if (timeMillis < 0) {
return null;
}
int idx = calculateTimeIdx(timeMillis - windowLengthInMs); // @1
timeMillis = timeMillis - windowLengthInMs;
WindowWrap<T> wrap = array.get(idx);
if (wrap == null || isWindowDeprecated(wrap)) { // @2
return null;
}
if (wrap.windowStart() + windowLengthInMs < (timeMillis)) { // @3
return null;
}
return wrap;
}

其实现的关键点如下:

代码@1:用当前时间减去一个时间窗口间隔,然后去定位所在 LeapArray 中 数组的下标。

代码@2:如果为空或已过期,则返回 null。

代码@3:如果定位的窗口的开始时间再加上 windowLengthInMs 小于 timeMills ,说明失效,则返回 null,通常是不会走到该分支。

2.2.5 滑动窗口图示

经过上面的分析,虽然还有一个核心方法 (resetWindowTo) 未进行分析,但我们应该可以画出滑动窗口的实现的实现原理图了。



接下来对上面的图进行一个简单的说明:下面的示例以采集间隔为 1 s,抽样次数为 2。

首先会创建一个 LeapArray,内部持有一个数组,元素为 2,一开始进行采集时,数组的第一个,第二个下标都会 null,例如当前时间经过 calculateTimeIdx 定位到下标为 0,此时没有滑动窗口,会创建一个滑动窗口,然后该滑动窗口会采集指标,随着进入 1s 的后500ms,后会创建第二个抽样窗口。

然后时间前进 1s,又会定位到下标为 0 的地方,但此时不会为空,因为有上一秒的采集数据,故需要将这些采集数据丢弃 ( MetricBucket value ),然后重置该窗口的 windowStart,这就是 resetWindowTo 方法的作用。

在 ArrayMetric 的构造函数出现了 LeapArray 的两个实现类型 BucketLeapArray 与 OccupiableBucketLeapArray。

其中 BucketLeapArray 比较简单,在这里就不深入研究了, 我们接下来将重点探讨一下 OccupiableBucketLeapArray 的实现原理,即支持抢占未来的“令牌”。

3、OccupiableBucketLeapArray 详解

所谓的 OccupiableBucketLeapArray ,实现的思想是当前抽样统计中的“令牌”已耗尽,即达到用户设定的相关指标的阔值后,可以向下一个时间窗口,即借用未来一个采样区间。接下来我们详细来探讨一下它的核心实现原理。

3.1 类图



我们重点关注一下 OccupiableBucketLeapArray 引入了一个 FutureBucketLeapArray 的成员变量,其命名叫 borrowArray,即为借用的意思。

3.2 构造函数

public OccupiableBucketLeapArray(int sampleCount, int intervalInMs) {
super(sampleCount, intervalInMs);
this.borrowArray = new FutureBucketLeapArray(sampleCount, intervalInMs);
}

从构造函数可以看出,不仅创建了一个常规的 LeapArray,对应一个采集周期,还会创建一个 borrowArray ,也会包含一个采集周期。

3.3 newEmptyBucket

public MetricBucket newEmptyBucket(long time) {
MetricBucket newBucket = new MetricBucket(); // @1
MetricBucket borrowBucket = borrowArray.getWindowValue(time); // @2
if (borrowBucket != null) {
newBucket.reset(borrowBucket);
}
return newBucket;
}

我们知道 newEmptyBucket 是在获取当前窗口时,对应的数组下标为空的时会创建。

代码@1:首先新建一个 MetricBucket。

代码@2:在新建的时候,如果曾经有借用过未来的滑动窗口,则将未来的滑动窗口上收集的数据 copy 到新创建的采集指标上,再返回。

3.4 resetWindowTo

protected WindowWrap<MetricBucket> resetWindowTo(WindowWrap<MetricBucket> w, long time) {
w.resetTo(time);
MetricBucket borrowBucket = borrowArray.getWindowValue(time);
if (borrowBucket != null) {
w.value().reset();
w.value().addPass((int)borrowBucket.pass());
} else {
w.value().reset();
}
return w;
}

遇到过期的滑动窗口时,需要对滑动窗口进行重置,这里的思路和 newEmptyBucket 的核心思想是一样的,即如果存在已借用的情况,在重置后需要加上在未来已使用过的许可,就不一一展开了。

3.5 addWaiting

public void addWaiting(long time, int acquireCount) {
WindowWrap<MetricBucket> window = borrowArray.currentWindow(time);
window.value().add(MetricEvent.PASS, acquireCount);
}

经过上面的分析,先做一个大胆的猜测,该方法应该是当前滑动窗口中的“令牌”已使用完成,借用未来的令牌。将在下文给出证明。

滑动窗口的实现原理就介绍到这里了。大家可以按照上面的代码结合下图做一个理解。

思考题,大家可以画一下 OccupiableBucketLeapArray 滑动窗口的图示。这部分内容也将在我的【中间件知识星球】中与各位星友一起探讨,欢迎大家的加入。

推荐阅读:源码分析 Alibaba Sentinel 专栏。

1、Alibaba Sentinel 限流与熔断初探(技巧篇)

2、源码分析 Sentinel 之 Dubbo 适配原理


作者信息:丁威,《RocketMQ技术内幕》作者,目前担任中通科技技术平台部资深架构师,维护 中间件兴趣圈公众号,目前主要发表了源码阅读java集合、JUC(java并发包)、Netty、ElasticJob、Mycat、Dubbo、RocketMQ、mybaits等系列源码。点击链接:加入笔者的知识星球,一起探讨高并发、分布式服务架构,分享阅读源码心得。

源码分析 Alibaba sentinel 滑动窗口实现原理(文末附原理图)的更多相关文章

  1. Spring Ioc源码分析系列--@Autowired注解的实现原理

    Spring Ioc源码分析系列--@Autowired注解的实现原理 前言 前面系列文章分析了一把Spring Ioc的源码,是不是云里雾里,感觉并没有跟实际开发搭上半毛钱关系?看了一遍下来,对我的 ...

  2. vscode源码分析【九】窗口里的主要元素

    第一篇: vscode源码分析[一]从源码运行vscode 第二篇:vscode源码分析[二]程序的启动逻辑,第一个窗口是如何创建的 第三篇:vscode源码分析[三]程序的启动逻辑,性能问题的追踪 ...

  3. Mybatis源码分析--关联表查询及延迟加载原理(二)

    在上一篇博客Mybatis源码分析--关联表查询及延迟加载(一)中我们简单介绍了Mybatis的延迟加载的编程,接下来我们通过分析源码来分析一下Mybatis延迟加载的实现原理. 其实简单来说Myba ...

  4. RocketMQ源码分析之RocketMQ事务消息实现原理上篇(二阶段提交)

    在阅读本文前,若您对RocketMQ技术感兴趣,请加入 RocketMQ技术交流群 根据上文的描述,发送事务消息的入口为: TransactionMQProducer#sendMessageInTra ...

  5. zuul源码分析-探究原生zuul的工作原理

    前提 最近在项目中使用了SpringCloud,基于zuul搭建了一个提供加解密.鉴权等功能的网关服务.鉴于之前没怎么使用过Zuul,于是顺便仔细阅读了它的源码.实际上,zuul原来提供的功能是很单一 ...

  6. MyBatis源码分析(各组件关系+底层原理

    MyBatis源码分析MyBatis流程图 下面将结合代码具体分析. MyBatis具体代码分析 SqlSessionFactoryBuilder根据XML文件流,或者Configuration类实例 ...

  7. Flask源码分析二:路由内部实现原理

    前言 Flask是目前为止我最喜欢的一个Python Web框架了,为了更好的掌握其内部实现机制,这两天准备学习下Flask的源码,将由浅入深跟大家分享下,其中Flask版本为1.1.1. 上次了解了 ...

  8. Vue.js 源码分析(四) 基础篇 响应式原理 data属性

    官网对data属性的介绍如下: 意思就是:data保存着Vue实例里用到的数据,Vue会修改data里的每个属性的访问控制器属性,当访问每个属性时会访问对应的get方法,修改属性时会执行对应的set方 ...

  9. Sentinel源码解析三(滑动窗口流量统计)

    前言 Sentinel的核心功能之一是流量统计,例如我们常用的指标QPS,当前线程数等.上一篇文章中我们已经大致提到了提供数据统计功能的Slot(StatisticSlot),StatisticSlo ...

随机推荐

  1. 使用GitBook编写项目文档

    GitBook简介 GitBook 是使用 GitHub / Git 和 Markdown(或AsciiDoc)构建漂亮书籍的命令行工具(和Node.js库): GitBook 可以将您的内容作为网站 ...

  2. Python-多任务复制文件夹

    import multiprocessing import os import time def copy_file(queue, file_name, old_folder_name, new_fo ...

  3. idea(or maven) 未结束字符串字面值 非法的表达式开始

    [ERROR] *.java:[38,27] 未结束的字符串字面值 [ERROR] *.java:[38,53] 需要 ';' [ERROR] *.java:[41,19] 需要 ')' [ERROR ...

  4. H5 video 标签 详解

    昨天使用H5  video 标签 写了视频播放   本打算参考爱奇艺的代码进行修改  发现 它是动态数据  静态页面需要拆解代码 我情急之下  使用了  video   整理一下笔记   后面有人用 ...

  5. 18岁,赚到了人生中的第一个10W!

    大家好,我是九歌 今年我18岁,赚到了我人生中的第一个10W 截至2019年10月14日,我已经做了43天的公众号啦,粉丝也悄然增长到了1W8,感谢各位读者朋友给我的支持和鼓励. 相信大部分读者都是从 ...

  6. APP倒闭:你充值的钱会蒸发吗?

    有一句说到吐,但却又不得不说的话:资本大潮退去,才知道谁在裸泳.随着资本寒冬的来临,互联网上众多看起来狂飙突进的项目却呈现迅速萎靡态势.尤其是众多具有互联网元素的油卡.洗衣.保洁等成为重灾区,其中不少 ...

  7. CALL/APPLY、一些编程基础以及一些基础知识、正则

    call.apply.bind 求数组的最大值和最小值: 数组排序(SORT的原理->localeCompare实现汉字比较),取头取尾 假设法 利用APPLY传参传递的是一个数组的机制,借用M ...

  8. 万维网(WWW)

    万维网(WWW) 一.万维网概述 万维网 WWW (World Wide Web)是一个大规模的.联机式的信息储藏所. 万维网用链接的方法能非常方便地从因特网上的一个站点访问另一个站点,从而主动地按需 ...

  9. Python开发(三):字符编码,文件操作,函数

    一:三级菜单 If len(choice) == continue # 判断输入的是否为空,为空就跳出这次循环进行下次循环, exit(“bye”) :退出程序显示,bye 二:编码 最早的编码是as ...

  10. 2019年后,Java岗面试快速突击指南

    大家好.这篇文章给大家分享一下如何获得一个可以去参加面试的最小可行知识(Minimal Viable Knowledge)!我自己在就基本上靠文章中的策略在找实习的时候拿到了头条阿里的offer.所以 ...