简单理解 Sentinel 滑动窗口实现原理


theme: serene-rose

1. 引言

Hi,你好,我是有清

对于刚经历过双 11 的电商人来说,限流这个词肯定在 10.24 的晚 20.00 点被提起过

限流作为保护我们系统不被流量冲垮的手段之一,建议每个电商人深入了解学习,什么,你不是电商人,那你也得了解一下,不然怎么在金三银四和面试官大胆对线

目前市面上比较流行的流量治理框架是 Sentinel,在本文中我们先复习一下常见八股-限流算法有哪些,然后再理解一下 Sentinel 的是如何使用滑动窗口

2. 常见限流算法

2.1. 令牌桶

令牌桶顾名思义,具有一个桶存放着令牌,系统会以恒定的速率往桶里放令牌,拿到令牌的请求才可进行后续操作,如果你没有拿到,sorry,你的请求将被抛弃,如图所示

无标题-2023-08-07-1113

无标题-2023-08-07-1113

我们可以借助 Guava 的 RateLimiter 来实现令牌桶,优点在于使用令牌桶放过的流量比较均匀,有利于保护系统不被流量冲垮;当然令牌桶的弊端在于,对于持续的峰值流量无法应对。由于令牌桶算法是以恒定速率添加令牌,当持续时间内产生大量请求时,可能无法及时获取到足够的令牌,导致请求被拒绝

2.2. 漏桶

漏桶算法,我们可以理解为存在一个水龙头持续往桶里滴水,然后这个桶可以匀速往外滴水,平移到我们的项目实践中,即我们可以维护一个有界队列作为漏桶,用来承接进来的网络请求,系统均匀处理队列中的网络请求,一旦队列满了,就触发限流策略,如图所示 漏桶

漏桶算法的弊端在于无法处理突如其来的大流量,假设我们当前处理的速率为 1000 qps ,桶容量 5000 ,现在来了一波持续 10s 的 2000 qps,那么后几秒的网络请求将都会被抛弃

2.3. 固定窗口

固定窗口算法即系统会维护一个计数器,在固定的时间段内,流量进来,计数器计数,一旦超过上限则进行限流相关拒绝策略,在下一个窗口计数器又将会被置零,如图所示

固定

固定

固定窗口的算法弊端在于:我们统一假设当前的窗口限制为 1000 qps 的流量,窗口间隔为 5s。第一个弊端在于边界问题:在第5 s 和第 6 s,分别进来了1000qps 流量,相当于窗口切换的 0.1 s 内,系统接受了 2000 qps 的流量,很容易,piaji,系统挂了;第二个问题在于流量突发的情况,在第一秒进来了 1001 的 qps,那么在 4 - 5 s 的时间内,系统流量都将被限制,带给用户的感觉就是:这个系统怎么这么垃圾

2.4. 滑动窗口

滑动窗口算法,其实就是为了解决固定窗口的弊端,大窗口依然不变,但是大窗口内会分为 n 个小窗口,每个小窗口内维护计数器,大窗口随着时间的移动,不断抛弃和拾取小窗口,从而达到限流的目的。

Snipaste<em>2023-11-05</em>11-35-34

Snipaste2023-11-0511-35-34

滑动窗口依然无法避免边界问题,并且小窗口数需要开发者进行仔细的考量

3. 滑动窗口核心实现类

铺垫了这么久,终于要进入正戏了

Sentinel 目前采取的就是滑动窗口算法,根据上文的介绍我们来一起分析一下滑动窗口的核心实现类有哪些

  • LeapArray:leap 四级单词,务必掌握,这个单词意思是间隔,leapArray 即为间隔数组,我们可以简单理解为一个大窗口,大窗口可以包含小窗口,小窗口的数量为 sampleCount 、间隔时间大小 windowLengthInMs ,都是由此数据结构控制,再来一个数学公式: sampleCount = intervalInMs / windowLengthInMs

提问 intervalInMs 代表什么含义?

  • WindowWrap:wrap 四级单词,务必掌握,包裹,整体单词意思为 窗口包装类,理解为一个小窗口
  • Metric:继续来学习单词,这个不知道是几级单词因为我也不会,理解为 指标,该接口用来标识一些指标信息,诸如 qps、rt、tps 等等
  • ArrayMetric:已经有聪明的同学开始抢答了,该类意为数组指标,即我们滑动窗口的核心实现类,对,就是男一号
  • MetricBucket:指标桶,用来滑动窗口中实际统计数据

todo:补充一个 uml 类图

4. 滑动窗口实现原理

在核心类中我们认识了 ArrayMetric 。接下来我们就围绕着 ArrayMetric 展开说明 Sentinel 的限流实现原理

4.1. ArrayMetric 构造

我们先看下如何构造 ArrayMetric

```

    public ArrayMetric(int sampleCount, int intervalInMs) {         this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);     }

    public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) {         if (enableOccupy) {             this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);         } else {             this.data = new BucketLeapArray(sampleCount, intervalInMs);         }     } ```

ArrayMetric 提供了两个构造方法,我们先来看一下参数,sampleCount 在上文提到的即为小窗口数,继续搬出我们的公式:sampleCount = intervalInMs / windowLengthInMs,intervalInMs 即每个大窗口的间隔时间,enableOccupy 意为是否允许抢占,即是否允许抢占下一个窗口的资源,允许的话,构造的子类即为 OccupiableBucketLeapArray,否则为 BucketLeapArray,具体二者区别我们下文再展开

在 ArrayMetric 方法中,无论是 pass、rt 还是 block ,都需要获取当前小窗口信息 ,调用的方法为 data.currentWindow();

/**  * Get the bucket at current timestamp.  *  * @return the bucket at current timestamp  */ public WindowWrap<T> currentWindow() {     return currentWindow(TimeUtil.currentTimeMillis()); }

通过注释我们可以看出,该方法是根据当前时间戳,获取小窗口信息

继续点进下一步实现类之前,我们可以先想一下,如果是我们去写这样一个获取小窗的方法,我们会怎么实现?

是不是需要取获取到当前时间戳命中的窗口下标?如果其他线程已经创建过同等的时间戳窗口是否可以直接复用?如果当前时间戳大于之前已经生成的窗口的时间戳,如何处理?

4.2. 获取当前窗口

带着这几个问题,我们继续看下源码

 public WindowWrap<T> currentWindow(long timeMillis) {         if (timeMillis < 0) {             return null;         }         int idx = calculateTimeIdx(timeMillis);         long windowStart = calculateWindowStart(timeMillis);         while (true) {             WindowWrap<T> old = array.get(idx);             if (old == null) {                 WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));                 if (array.compareAndSet(idx, null, window)) {                     return window;                 } else {                     Thread.yield();                 }             } else if (windowStart == old.windowStart()) {                 return old;             } else if (windowStart > old.windowStart()) {                 if (updateLock.tryLock()) {                     try {                         return resetWindowTo(old, windowStart);                     } finally {                         updateLock.unlock();                     }                 } else {                         Thread.yield();                 }             } else if (windowStart < old.windowStart()) {                  return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));             }         }     }

首先生成窗口下标

```    int idx = calculateTimeIdx(timeMillis);

    private int calculateTimeIdx(/@Valid/ long timeMillis) {         long timeId = timeMillis / windowLengthInMs;         // Calculate current index so we can map the timestamp to the leap array.         return (int)(timeId % array.length());     } ```

来,继续做数学题,timeMillis 意味当前时间戳,windowLengthInMs 小窗间隔时间,假设当前时间戳为 666,小窗间隔时间为 200,看图 👇

Snipaste<em>2023-11-04</em>16-34-19

Snipaste2023-11-0416-34-19

接下来计算小窗的起始时间

protected long calculateWindowStart(/*@Valid*/ long timeMillis) {     return timeMillis - timeMillis % windowLengthInMs; }

小窗的起始时间计算的方法其实很简单了 666 - 666 % 200 = 600 ,对照上图,一目了然

接下来分成三种情况,我们一一来讨论一下

  • 不存在旧窗口

这种情况,比较简单,我们直接生成新窗口即可,此处采取了 CAS 来进行窗口生成,保证线程一致

WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis)); if (array.compareAndSet(idx, null, window)) {     // Successfully updated, return the created bucket.     return window; } else {     // Contention failed, the thread will yield its time slice to wait for bucket available.     Thread.yield(); }

  • 命中旧窗口

这种情况就更简单了,直接返回旧窗口即可

  • 当前时间戳大于旧窗口时间戳

这种情况是当 A 线程生成小窗的时候时间戳命中了 B1 窗口,此时 B 线程的时间戳命中 B5 窗口,即当前窗口就为 B5,需要进行窗口重置,我们来看代码

``` if (updateLock.tryLock()) {     try {         // Successfully get the update lock, now we reset the bucket.         return resetWindowTo(old, windowStart);     } finally {         updateLock.unlock();     } } else {     // Contention failed, the thread will yield its time slice to wait for bucket available.     Thread.yield(); }

    @Override     protected WindowWrap resetWindowTo(WindowWrap w, long startTime) {         // Update the start time and reset value.         w.resetTo(startTime);         w.value().reset();         return w;     } ```

这边可以看到处理的方式就是将当前的时间的起始时间和统计值全部进行重置处理

其实还有一种情况,就是当前时间小于旧窗口的起始时间,但是一般不存在这种情况,我们不进行讨论

4.3. 获取上一个窗口

获取上一个窗口的实现类中,同样是取去计算窗口的下标,但是计算下标的时候传入的不是当前的时间戳,而是减去一个小窗间隔的时间戳

``` public WindowWrap getPreviousWindow(long timeMillis) {     if (timeMillis < 0) {         return null;     }     int idx = calculateTimeIdx(timeMillis - windowLengthInMs);     timeMillis = timeMillis - windowLengthInMs;     WindowWrap wrap = array.get(idx);

    if (wrap == null || isWindowDeprecated(wrap)) {         return null;     }

    if (wrap.windowStart() + windowLengthInMs < (timeMillis)) {         return null;     }

    return wrap; } ```

4.4. 窗口是否废弃

如果当前的时间减去窗口的起始时间大于一整个大窗口的时间,即该窗口已失效

public boolean isWindowDeprecated(long time, WindowWrap<T> windowWrap) {     return time - windowWrap.windowStart() > intervalInMs; }

4.5. OccupiableBucketLeapArray

LeapArray 的重点方法我们都分析完毕了,我们看下子类针对于这些方法是否有进行重写

4.5.1. 构造方法

其实在上文我们已经看到过构造 OccupiableBucketLeapArray 需要 sampleCount 和 intervalInMs,但其实真正构造 OccupiableBucketLeapArray,还会去构造一个 FutureBucketLeapArray 对象,该对象也是继承 LeapArray,结合上文说的抢占的意思,可以推测出这是一个未来时间窗口的 LeapArray

``` private final FutureBucketLeapArray borrowArray;

public OccupiableBucketLeapArray(int sampleCount, int intervalInMs) {     // This class is the original "CombinedBucketArray".     super(sampleCount, intervalInMs);     this.borrowArray = new FutureBucketLeapArray(sampleCount, intervalInMs); } ```

4.5.2. newEmptyBucket

newEmptyBucket 顾名思义,用来创建一个新的空的小窗,它作用的地方在于我们获取当前窗口的时候,看个图

这边的实现也很简单,借助到我们在构造函数的时候生成的 borrowArray,如果 borrowArray 存在当前时间戳的数据,则直接拿到 borrowArray 中的计数数据

``` public MetricBucket newEmptyBucket(long time) {     MetricBucket newBucket = new MetricBucket();

    MetricBucket borrowBucket = borrowArray.getWindowValue(time);     if (borrowBucket != null) {         newBucket.reset(borrowBucket);     }

    return newBucket; } ```

4.5.3. resetWindowTo

重置窗口在上文中我们已经介绍过了,在该类实现中,其实也就是判断是否 borrowArray 是否存在数据,存在的话,需要加上 borrowArray 中的通过线程数

``` protected WindowWrap resetWindowTo(WindowWrap w, long time) {     // Update the start time and reset value.     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; } ```

4.6. 滑动流程

接下来我们整体看一下限流流程

首先我们假设构造小窗数量为 2,小窗间隔时间为 500 ms 的 LeapArray

Snipaste<em>2023-11-05</em>11-36-46

Snipaste2023-11-0511-36-46

当时间戳通过 currentWindow 命中 windowWrap-1,构造窗口,当时间戳命中 windowWrap-2,构造窗口,这边会看构造的是 OccupiableBucketLeapArray 亦或是BucketLeapArray

当时间往下走,大于 1s,可能时间戳又再次命中 windowWrap-1,此时就需要 resetWindowTo,同样针对不同的实现类有不同的方法

这就是滑动窗口在 Sentinel 的运用,easy 哇!

4.7 总结

滑动窗口的实现原理就是在于窗口的构造与判断,其实整体流程还是相对来说比较简单,主要就是理解其运用的数据结构,本文其实没有针对 BucketLeapArray 展开说明,感兴趣的小伙伴可以自己去扒拉一下源码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/174331.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

应急响应练习1

目录 1. 提交攻击者的IP地址 2. 识别攻击者使用的操作系统 3. 找出攻击者资产收集所使用的平台 4. 提交攻击者目录扫描所使用的工具名称 5. 提交攻击者首次攻击成功的时间&#xff0c;格式&#xff1a;DD /MM/YY:HH:MM:SS 6. 找到攻击者写入的恶意后门文件&#xff0c;提…

黑群晖断电导致存储空间已损毁修复记录

黑群晖断电2次,担心的事情还是发生了,登录后提示存储空间已损毁...... 开干!! 修复方式: 1.使用SSH登录到群晖,查看相关信息 # 登录后先获取最高权限 root@DiskStation:~# sudo -i # 检测存储池状态 root@DiskStation:~# cat /proc/mdstat Personalities : [linear] […

Java 算法篇-深入了解单链表的反转(实现:用 5 种方式来具体实现)

&#x1f525;博客主页&#xff1a; 小扳_-CSDN博客 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 单链表的反转说明 2.0 单链表的创建 3.0 实现单链表反转的五种方法 3.1 实现单链表反转 - 循环复制&#xff08;迭代法&#xff09; 3.2 实现单链表反转 - 头插法 3…

卷积神经网络(1)

目录 卷积 1 自定义二维卷积算子 2 自定义带步长和零填充的二维卷积算子 3 实现图像边缘检测 4 自定义卷积层算子和汇聚层算子 4.1 卷积算子 4.2 汇聚层算子 5 学习torch.nn.Conv2d()、torch.nn.MaxPool2d()&#xff1b;torch.nn.avg_pool2d()&#xff0c;简要介绍使用方…

数据结构-堆和二叉树

目录 1.树的概念及结构 1.1 树的相关概念 1.2 树的概念 1.3 树的表示 1.4 树在实际中的应用&#xff08;表示文件系统的目录树结构&#xff09; 2.二叉树的概念及结构 2.1 概念 2.2 特殊的二叉树 2.3 二叉树的存储 3.堆的概念及结构 4.堆的实现 初始化堆 堆的插入…

计算机组成原理:大而快——层次化存储

原文链接www.xiaocr.fun/index.php/2023/11/14/计算机组成原理大而快-层次化存储/ 引言 关于两种局部性 时间局部性&#xff1a;如果某个数据被访问&#xff0c;那么在不久的将来它可能再次被访问空间局部性&#xff1a;如果某个数据项被访问&#xff0c;与它相邻的数据项可…

Qt文档阅读笔记-Fetch More Example解析

Fetch More Example这个例子说明了如何在视图模型上添加记录。 这个例子由一个对话框组成&#xff0c;在Directory的输入框中&#xff0c;可输入路径信息。应用程序会载入路径信息的文件信息等。不需要按回车键就能搜索。 当有大量数据时&#xff0c;需要对视图模型进行批量增…

剑指 Offer 07. 重建二叉树

title: 剑指 Offer 07. 重建二叉树 tags: 二叉树递归 categories:算法剑指 Offer 题目描述 输入某二叉树的前序遍历和中序遍历的结果&#xff0c;请构建该二叉树并返回其根节点。 假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 示例 1: Input: preorder [3,9,…

Spring 常见面试题

1、Spring概述 1.1、Spring是什么? Spring是一个轻量级Java开发框架,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题Spring最根本的使命是解决企业级应用开发的复杂性&#xff0c;即简化Java开发。这些功能的底层都依赖于它的两个核心特性&#xff0c;也就是…

清华镜像源地址,适用于pip下载速度过慢从而导致下载失败的问题

清华地址 https://pypi.tuna.tsinghua.edu.cn/simple下载各种各样的包的指令模板 pip install XXX -i https://pypi.tuna.tsinghua.edu.cn/simple这样就行了&#xff0c;XXX代表的是你将要下载的包名称。 比如&#xff1a; pip install opencv-python -i https://pypi.tuna.…

nginx安装搭建

下载 免费开源版的官方网站&#xff1a;nginx news Nginx 有 Windows 版本和 Linux 版本&#xff0c;但更推荐在 Linux 下使用 Nginx&#xff1b; 下载nginx-1.14.2.tar.gz的源代码文件&#xff1a;wget http://nginx.org/download/nginx-1.14.2.tar.gz 我的习惯&#xff0…

NLP领域的突破催生大模型范式的形成与发展

当前的大模型领域的发展&#xff0c;只是范式转变的开始&#xff0c;基础大模型才刚刚开始改变人工智能系统在世界上的构建和部署方式。 1、大模型范式 1.1 传统思路&#xff08;2019年以前&#xff09; NLP领域历来专注于为具有挑战性的语言任务定义和设计系统&#xff0c…