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

news2024/12/23 6:12:02

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.coloradmin.cn/o/1189822.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ansible-第二天

ansible 第二天 以上学习了ping、command、shell、script模块&#xff0c;但一般不建议使用以上三个&#xff0c;因为这三个模块没有幂等性。举例如下&#xff1a; [rootcontrol ansible]# ansible test -a "mkdir /tmp/1234"[WARNING]: Consider using the file …

GitHub上的开源工业软件

github上看到一个中国人做的流体力学开源介绍&#xff0c;太牛了&#xff01; https://github.com/clatterrr/FluidSimulationTutorialsUnity 先分析一下工业仿真软件赛道 工业仿真软件的赛道和产品主要功能如下&#xff1a; 1. 工艺仿真赛道&#xff1a; - 工厂布局优化&am…

人工智能模型转ONNX 连接摄像头使用ONNX格式的模型进行推理

部署之后模型的运算基本上能快5倍。本地部署之后&#xff0c;联网都不需要&#xff0c;数据和隐私不像在网上那样容易泄露了。 模型部署的通用流程 各大厂商都有自己的推理工具。 训练的归训练&#xff0c;部署的归部署&#xff0c;人工智能也分训练端和部署端&#xff0c;每一…

派金SDK接入文档

一、接入SDK 1、将sdk文件手动导入到目标项目中&#xff0c;如下图所示&#xff1a; 2、该SDK需接入其他三方广告&#xff0c;通过pod的方式接入&#xff0c;在Profile中加入如下代码&#xff1a; pod GDTMobSDK, ~> 4.14.40pod BaiduMobAdSDK, ~> 5.313pod KSAdSDK…

pytorch中常用的损失函数

1 损失函数的作用 损失函数是模型训练的基础&#xff0c;并且在大多数机器学习项目中&#xff0c;如果没有损失函数&#xff0c;就无法驱动模型做出正确的预测。 通俗地说&#xff0c;损失函数是一种数学函数或表达式&#xff0c;用于衡量模型在某些数据集上的表现。损失函数在…

数模之线性规划

线性规划 优化类问题&#xff1a;有限的资源&#xff0c;最大的收益 例子: 华强去水果摊找茬&#xff0c;水果摊上共3个瓜&#xff0c;华强总共有40点体力值,每劈一个瓜能带来40点挑衅值,每挑一个瓜问“你这瓜保熟吗”能带来30点挑衅值,劈瓜消耗20点体力值&#xff0c;问话消耗…

Linux awk命令

除了使用 sed 命令&#xff0c;Linux 系统中还有一个功能更加强大的文本数据处理工具&#xff0c;就是 awk。 曾有人推测 awk 命令的名字来源于 awkward 这个单词。其实不然&#xff0c;此命令的设计者有 3 位&#xff0c;他们的姓分别是 Aho、Weingberger 和 Kernighan&#x…

7+差异分析+WGCNA+PPI网络,学会了不吃亏

今天给同学们分享一篇生信文章“Integrated PPI- and WGCNA-Retrieval of Hub Gene Signatures Shared Between Barretts Esophagus and Esophageal Adenocarcinoma”&#xff0c;这篇文章发表在Front Pharmacol期刊上&#xff0c;影响因子为5.6。 结果解读&#xff1a; 选定研…

【解决方案】vue 项目 npm run dev 时报错:‘cross-env‘ 不是内部或外部命令,也不是可运行的程序

报错 cross-env 不是内部或外部命令&#xff0c;也不是可运行的程序 或批处理文件。 npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! estate1.0.0 dev: cross-env webpack-dev-server --inline --progress --config build/webpack.dev.conf.js npm ERR! Exit status 1 np…

什么是final修饰 使用final修饰类、方法、变量的区别?

简介: 变量成为常量&#xff0c;不允许修改 当final修饰类时&#xff0c;该类变为最终类&#xff08;或称为不可继承的类&#xff09;。不能从最终类派生子类。这样做的目的是为了防止其他类修改或扩展最终类的行为。当final修饰方法时&#xff0c;该方法成为最终方法&#xf…

Qt QtCreator调试Qt源码配置

目录 前言1、编译debug版Qt2、QtCreator配置3、调试测试4、总结 前言 本篇主要介绍了在麒麟V10系统下&#xff0c;如何编译debug版qt&#xff0c;并通过配置QtCreator实现调试Qt源码的目的。通过调试源码&#xff0c;我们可以对Qt框架的运行机制进一步深入了解&#xff0c;同时…

计算摄像技术03 - 数字感光器件

一些计算摄像技术知识内容的整理&#xff1a;感光器件的发展过程、数字感光器件结构、数字感光器件的指标。 目录 一、感光器件的发展过程 二、数字感光器件结构 &#xff08;1&#xff09;CCD结构 ① 微透镜 ② 滤光片 ③ 感光层 电荷传输模式 &#xff08;2&#xff09;CMOS结…

代码随想录算法训练营第16天|104. 二叉树的最大深度111.二叉树的最小深度222.完全二叉树的节点个数

JAVA代码编写 104. 二叉树的最大深度 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;3示例 2&#xff1a; …

API接口自动化测试

本节介绍&#xff0c;使用python实现接口自动化实现。 思路&#xff1a;讲接口数据存放在excel文档中&#xff0c;读取excel数据&#xff0c;将每一行数据存放在一个个列表当中。然后获取URL,header,请求体等数据&#xff0c;进行请求发送。 结构如下 excel文档内容如下&#x…

【vue会员管理系统】篇五之系统首页布局和导航跳转

一、效果图 1.首页 2.会员管理&#xff0c;跳转&#xff0c;跳其他页面也是如此&#xff0c;该页的详细设计会在后面的章节完善 二、代码 新增文件 components下新增文件 view下新增文件&#xff1a; 1.componets下新建layout.vue 放入以下代码&#xff1a; <template…

计算机组成原理之指令

引言 关于riscv操作数 32个寄存器 | X0~X31|快速定位数据。在riscv中&#xff0c;只对寄存器中的数据执行算术运算 2^61个存储字 | 只能被数据传输指令访问。riscv体系采用的是字节寻址。 一个寄存器是8bytes&#xff0c;64位&#xff08;double word&#xff09; 每次取的…

Python高级语法----深入asyncio:构建异步应用

文章目录 异步I/O操作示例:异步网络请求异步任务管理示例:并发执行多个任务使用异步队列示例:生产者-消费者模式在现代软件开发中,异步编程已经成为提高应用性能和响应性的关键技术之一。Python的asyncio库为编写单线程并发代码提供了强大的支持。本文将深入探讨asyncio的三…

Hadoop原理,HDFS架构,MapReduce原理

Hadoop原理&#xff0c;HDFS架构&#xff0c;MapReduce原理 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xff0c…

C++ vector 动态数组的指定元素删除

文本旨在对 C 的容器 vector 进行肤浅的分析。 文章目录 Ⅰ、vector 的指定元素删除代码结果与分析 Ⅱ、vector 在新增元素后再删除指定元素代码结果与分析 Ⅲ、vector 在特定条件下新增元素代码结果与分析 参考文献 Ⅰ、vector 的指定元素删除 代码 #include <iostream&g…

另辟蹊径者 PoseiSwap:背靠潜力叙事,构建 DeFi 理想国

前不久&#xff0c;灰度在与 SEC 就关于 ETF 受理的诉讼案件中&#xff0c;以灰度胜诉告终。灰度的胜利&#xff0c;也被加密行业看做是加密 ETF 在北美地区阶段性的胜利&#xff0c; 该事件也带动了加密市场的新一轮复苏。 此前&#xff0c;Nason Smart Money 曾对加密市场在 …