sentinel中StatisticSlot数据采集的原理

news2024/9/27 7:18:27

StatisticSlot数据采集的原理

时间窗口

固定窗口

在固定的时间窗口内,可以允许固定数量的请求进入;超过数量就拒绝或者排队,等下一个时间段进入, 如下图

  • 时间窗长度划分为1秒

  • 单个时间窗的请求阈值为3
    在这里插入图片描述

上述存在一个问题, 假如9:18:04:333-9:18:05:000产生了2个请求, 9:18:05:000-9:18:05:333产生了3个请求, 那么也就是说9:18:04:333-9:18:05:333这一秒内产生5个请求, 正常来说这里已经超出了阈值
在这里插入图片描述

但是由于是固定窗口, 也就是这里只能9:18:04:000-9:18:05:000, 9:18:05:000-9:18:06:000这样子去处理, 所以实际上打达不到我们的要求的

滑动窗口

滑动窗口诞生的原因就在于解决固定窗口那个致命的问题,为什么我说固定窗口的问题是致命的?因为我们系统限流的目的是要在任意时间都能应对突然的流量暴增,也就是说我的系统最大在1s内能够处理请求3,但如果使用固定窗口的算法,就会造成在9:18:04:333-9:18:05:333之间的请求无法限流,从而严重的话会导致服务雪崩

滑动窗口如下图

  • 时间窗长度划分为1秒, 并且是滑动的

  • 单个时间窗的请求阈值为3
    在这里插入图片描述

如果要判断请求是否能正常通过, 那么就要把当前时间点作为终点, 统计前1秒内的请求数, 判断请求数是否达到阈值, 如果没有达到阈值就放行, 如果达到阈值了就通过

上边的问题是是解决了, 但是存在一些性能问题, 假设请求落在9:18:05:333, 往前移动1s距离, 那么就是以9:18:04:333作为起点, 统计9:18:04:333-9:18:05:333之间的请求数, 当请求落在9:18:05:633, 那么就要统计9:18:04:633-9:18:05:633之间的请求数, 发现9:18:04:633-9:18:05:333之间的数据上一次的时候就出现过了, 但是这里又得重新统计, 也就说每移动一次窗口, 那么都要重新统计重复区域的请求数量, 从而导致浪费大量系统资源, 如下图, 黄色区域为重复统计区域
在这里插入图片描述

出现了问题, 就要解决

我们需要引入更加细粒度化的计算, 也就是说需要增加子时间窗口, 那么这里引入的子时间窗口, 我们称为样本窗口

  1. 样本窗口的长度必须小于滑动窗口长度,因为如果样本窗口等于滑动窗口长度的话,就和固定窗口没啥区别了
  2. 通常情况下滑动窗口的长度是样本窗口的整数倍,比如:10 * 样本窗口 = 1 个滑动窗口
  3. 每个样本窗口在到达终点时间时,会统计本样本窗口中的流量数据并且记录下来,用于复用
  4. 当一个请求到达时,系统会首先统计当前请求时间点所在的样本窗口内的流量数据。接着,系统会检查在当前请求时间点之前的滑动窗口中的样本窗口,将它们的统计数据进行求和。如果这个求和值没有超出事先设定的阈值,请求将会被允许通过。然而,如果求和值超过了阈值,系统会触发限流措施,拒绝该请求的访问

假设

  • 时间窗长度为1s
  • 一个时间窗内包含三个样本窗口
  • 阈值为30

那么10:00:00:000-10:00:01:000内请求数为10 + 5 + 10 = 25 < 30, 所以大胆放行
在这里插入图片描述

那么10:00:00:333-10:00:01:333内请求数为5 + 10 + 7= 22 < 30, 继续放行
在这里插入图片描述

那么10:00:00:666-10:00:01:666内请求数为10 + 7 + 30= 47 > 30, 这里就要限流了

在这里插入图片描述

限流
在这里插入图片描述

那么10:00:01:000-10:00:02:000内请求数为7 + 30 + 7= 40 > 30, 这里就要限流了
在这里插入图片描述

那么10:00:01:333-10:00:02:333内请求数为30 + 7 + 34 = 71 > 30, 这里就要限流了
在这里插入图片描述

ps: 样本窗口的数量影响着滑动窗口算法的精度,依然有时间片的概念,无法根本解决临界点问题

数据统计

底层数据结构

StatisticSlot.entry() 中的 node.addPassRequest(count)方法

public void addPassRequest(int count) {
    // 增加当前入口的DefaultNode中的数据
    super.addPassRequest(count);
    // 增加当前资源的 ClusterNode 中的全局统计数据
    this.clusterNode.addPassRequest(count);
}


@Override
public void addPassRequest(int count) {
    // 为滑动计数器增加本次请求
    rollingCounterInSecond.addPass(count);
    rollingCounterInMinute.addPass(count);
}

rollingCounterInSecond 就是一个真正保存数据的计量器,数据类型为 ArrayMetric,也就是说 Sentinel 在统计数据上采取的是一个名为 ArrayMetric 的 Java 类,如下

// 定义了一个使用数组保存数据的计量器,样本窗口数量为2(SAMPLE_COUNT),时间窗口长度为1000ms(INTERVAL)
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT, IntervalProperty.INTERVAL);

那么 ArrayMetric 类又是如何存储数据的呢?

// 这是一个使用数组保存数据的计量器类,数据就保存在data中
public class ArrayMetric implements Metric {
    // 真正存储数据的地方
    private final LeapArray<MetricBucket> data;
}


public abstract class LeapArray<T> {
    // 样本窗口的长度
    protected int windowLengthInMs;
    // 一个时间窗口包含的样本窗口数量,公式 intervalInMs / windowLengthInMs,也就是时间窗口长度 / 样本窗口长度
    protected int sampleCount;
    // 时间窗口长度
    protected int intervalInMs;
    // 也是时间窗口长度,只是单位为s
    private double intervalInSecond;

    // WindowWrap : 样本窗口类
    // 这是一个数组
    // 这里的泛型T实际类型为 MetricBucket
    protected final AtomicReferenceArray<WindowWrap<T>> array;
}

LeapArray 类似于一个样本窗口管理类,而真正的样本窗口类是 WindowWrap<T>,对于样本窗口的概念我们肯定不陌生了,其包含:单个样本窗口的长度、样本窗口的开始时间戳,如下所示:

// 样本窗口类,泛型T为MetricBucket
public class WindowWrap<T> {
    // 单个样本窗口的长度
    private final long windowLengthInMs;

    // 样本窗口的起始时间戳
    private long windowStart;

    // 当前样本窗口的统计数据,类型为 MetricBucket
    private T value;
}

关于泛型真实类型为 MetricBucket 也很简单,可以从前面代码 ArrayMetric#LeapArray<MetricBucket> 得出。接下来就看真实类型 MetricBucket 是个什么东西

// 统计数据的封装类
public class MetricBucket {
    // 统计的数据真实存放在LongAdder里
    // 但是为什么要数组?直接用LongAdder+1不就行了?因为统计的数据是多维度的,这些维度类型在MetricEvent枚举中。
    private final LongAdder[] counters;

    private volatile long minRt;
}

这里有一个巧妙的设计,就是为什么用LongAdder[]而不是用LongAdder,正是因为统计的数据是多维度的,比如:统计通过的 QPS、统计失败的 QPS 等,因此设计成数组,我们就可以将不同类型放到不同的数组下标里

关系如下图
在这里插入图片描述

addPass()方法

@Override
public void addPassRequest(int count) {
    // 为滑动计数器增加本次请求
    rollingCounterInSecond.addPass(count);
    rollingCounterInMinute.addPass(count);
}

public void addPass(int count) {
    // 获取当前时间点所在的样本窗口
    WindowWrap<MetricBucket> wrap = data.currentWindow();
    // 将当前请求的计数量添加到当前样本窗口的统计数据中
    wrap.value().addPass(count);
}

其主要分为三部分:

  1. 当前时间所在的样本窗口如果还没创建,则需要初始化。
  2. 若当前样本窗口的起始时间与计算出的样本窗口起始时间相同,则说明这两个是同一个样本窗口,直接获取就行。
  3. 若当前样本窗口的起始时间大于计算出的样本窗口起始时间,说明计算出来的样本窗口已经过时了,需要将原来的样本窗口替换为新的样本窗口。数组的环形数组,不是无限长的,比如存 1s,1000 个样本窗口,那么下 1s 的 1000 个时间窗口会覆盖上一秒的。
  4. 若当前样本窗口的起始时间小于计算出的样本窗口起始时间,一般不会出现,因为时间不会倒流,除非人为修改系统时间导致时钟回拨
public WindowWrap<T> currentWindow(long timeMillis) {
    if (timeMillis < 0) {
        return null;
    }

    // 计算当前时间所在的样本窗口index,也就是样本窗口的下标,即在计算数组LeapArray中的下标
    int idx = calculateTimeIdx(timeMillis);
    // Calculate current bucket start time.
    // 计算当前样本窗口的开始时间点
    long windowStart = calculateWindowStart(timeMillis);

    while (true) {
        // 获取到当前时间所在的样本窗口
        WindowWrap<T> old = array.get(idx);
        // 代表当前时间所在的样本窗口没有,需要创建
        if (old == null) {
            /*
             *     B0       B1      B2    NULL      B4
             * ||_______|_______|_______|_______|_______||___
             * 200     400     600     800     1000    1200  timestamp
             *                             ^
             *                          time=888
             *            		bucket为空, 所以新建并更新
             *
             * 如果旧的bucket不存在,那么我们在windowStart创建一个新的bucket,然后尝试通过CAS操作
             * 更新循环数组。只有一个线程可以成功更新,而其他线程争夺这个时间片
             */
            // 创建一个时间窗口
            WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
            // CAS 将新建窗口放入LeapArray
            if (array.compareAndSet(idx, null, window)) {
                // 更新成功,返回创建的bucket
                return window;
            } else {
                // 获取锁失败, 线程将放弃其时间片以等待可用的bucket
                Thread.yield();
            }
        } else if (windowStart == old.windowStart()) { // 若当前样本窗口的起始时间与计算出的样本窗口起始时间相同,则说明这两个是同一个样本窗口,直接获取就行
            /*
             *     B0       B1      B2     B3      B4
             * ||_______|_______|_______|_______|_______||___
             * 200     400     600     800     1000    1200  timestamp
             *                             ^
             *                          time=888
             *            桶3的起始时间: 800,所以它是最新的
             *
             * 如果当前windowStart等于旧bucket的开始时间戳,则表示时间在bucket内,因此直接返回bucket
             */
            return old;
        } else if (windowStart > old.windowStart()) { // 若当前样本窗口的起始时间大于计算出的样本窗口起始时间。说明计算出来的样本窗口已经过时了,需要将原来的样本窗口替换为新的样本窗口。 数组的环形数组,不是无限长的,比如存1s,1000个样本窗口,那么下1s的1000个时间窗口会覆盖上一秒的
            /*
             *   (old)
             *             B0       B1      B2    NULL      B4
             * |_______||_______|_______|_______|_______|_______||___
             * ...    1200     1400    1600    1800    2000    2200  timestamp
             *                              ^
             *                           time=1676
             *          Bucket 2的startTime: 400,已弃用,应重置
             *
             * 如果旧bucket的开始时间戳落后于当前时间,则表示该bucket已弃用。我们必须将bucket重置为当前的windowStart。请注意,重置和清理操作很难是原子的,所以我们需要一个更新锁来保证bucket更新的正确性
             *
             * 更新锁是有条件的(小范围),只有当桶被弃用时才会生效,所以在大多数情况下它不会导致性能损失
             */
            if (updateLock.tryLock()) {
                try {
                    // 成功获取更新锁,现在我们重置bucket
                    // 替换老的样本窗口
                    return resetWindowTo(old, windowStart);
                } finally {
                    updateLock.unlock();
                }
            } else {
                // 获取锁失败,线程将放弃其时间片以等待可用的bucket
                Thread.yield();
            }
        } else if (windowStart < old.windowStart()) { // 若当前样本窗口的起始时间小于计算出的样本窗口起始时间,一般不会出现,因为时间不会倒流,除非人为修改系统时间(即时钟回拨)
            return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
        }
    }
}

流程图如下
在这里插入图片描述

上述使用到的数组是一个环形数组, 不是我们所谓的普通数组, 目地就是复用, 节省内存

环形数组的工作原理

现有环形数组, 长度为8, 目前已经用了6个位置
在这里插入图片描述

继续添加元素

在这里插入图片描述

继续添加元素
在这里插入图片描述

目前元素已经满了, 接着添加, 发现它把原来存在元素1的位置替换了, 换成了9
在这里插入图片描述

继续添加, 同理可得, 那么元素应该就应该替换到元素2这个位置
在这里插入图片描述

上边就是环形数组的工作原理

currentWindow的图文分析
old == null

这个表示环形数组都没有, 那么就创建一个环形数组, 并将元素设置进去, 这里就不画图了

windowStart > old.windowStart()

在这里插入图片描述

windowStart == old.windowStart()

在这里插入图片描述

windowStart < old.windowStart()

在这里插入图片描述

参考资料

通关 Sentinel 流量治理框架 - 编程界的小學生

10张图带你彻底搞懂限流、熔断、服务降级

分布式服务限流实战,已经为你排好坑了

服务限流详解

新来个技术总监,把限流实现的那叫一个优雅,佩服!

接口限流算法总结

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

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

相关文章

【ArcGIS微课1000例】0106:ArcGIS制作风向频率(风速)玫瑰图

文章目录 一、效果预览二、加载数据三、创建图表四、图表修饰五、保存图片一、效果预览 在ArcGIS制作的风向频率玫瑰图最终效果如下所示: 二、加载数据 加载配套实验数据包中0106.rar中的excel数据,然后右键→打开。 三、创建图表 1. 创建图表。右击打开属性表,选择表选项…

ETL数据倾斜与资源优化

1.数据倾斜实例 数据倾斜在MapReduce编程模型中比较常见&#xff0c;由于key值分布不均&#xff0c;大量的相同key被存储分配到一个分区里&#xff0c;出现只有少量的机器在计算&#xff0c;其他机器等待的情况。主要分为JOIN数据倾斜和GROUP BY数据倾斜。 1.1GROUP BY数据倾…

Web前端—浏览器渲染原理

浏览器渲染原理 浏览器渲染原理渲染时间点渲染流水线1. 解析HTML—Parse HTML2. 样式计算—Recalculate Style3. 布局—Layout4. 分层—Layer5. 绘制—Paint6. 分块—Tiling7. 光栅化—Raster8. 画—Draw完整过程 面试题1. 浏览器是如何渲染页面的&#xff1f;2. 什么是 reflow…

6.3 BP神经网络

在多层感知器被引入的同时&#xff0c;也引入了一个新的问题&#xff1a;由于隐藏层的预期输出并没有在训练样例中给出&#xff0c;隐藏层结点的误差无法像单层感知器那样直接计算得到。 为了解决这个问题&#xff0c;反向传播&#xff08;BP&#xff09;算法被引入&#xff0…

Spark DAG

Spark DAG 什么是DAG DAG 是一组顶点和边的组合。顶点代表了 RDD&#xff0c; 边代表了对 RDD 的一系列操作。 DAG Scheduler 会根据 RDD 的 transformation 动作&#xff0c;将 DAG 分为不同的 stage&#xff0c;每个 stage 中分为多个 task&#xff0c;这些 task 可以并行运…

jenkins介绍,帮助你从安装到使用jenkins

Jenkins 概述 官网地址&#xff1a;https://www.jenkins.io/zh/ 什么是 Jenkins Jenkins是一款开源 CI&CD 软件&#xff0c;用于自动化各种任务&#xff0c;包括构建、测试和部署软件。它提供了一个易于使用的图形化界面&#xff0c;可以通过配置简单的任务来实现自动化构…

ubuntu20.04安装 ffmpeg 开发环境

参考&#xff1a;参考1 一些相关软件包&#xff0c;已打包整理好&#xff0c;如下 源码包 1、安装步骤 创建安装目录 sudo mkdir -p /usr/local/ffmpeg/lib 解压源码 tar -jxf ffmpeg-4.3.2.tar.bz2 到指定ffmpeg目录进行配置 cd ffmpeg-4.3.2/ 配置&#xff1a;会报错很多…

简单使用Swagger

文章目录 1、介绍2、 使用步骤3、 常用注解 1、介绍 Swagger 是一个规范和完整的框架&#xff0c;用于生成、描述、调用和可视化 RESTful 风格的 Web 服务(https://swagger.io/)。 它的主要作用是&#xff1a; 使得前后端分离开发更加方便&#xff0c;有利于团队协作 接口的文…

银行监管报送系统介绍(五):金融统计数据大集中自动化报送系统——PBOC Report

人民银行金融统计数据大集中自动化报送系统&#xff08;简称PBOC Report&#xff09;&#xff0c;是基于现代计算机网络技术应用基础上&#xff0c;由人行总行设置金融统计数据服务器&#xff0c;建立的一个全国统一的金融统计数据库。 人行针对各银行存贷款、中间业务、网点人…

关闭 Microsoft Word 2010 配置窗口

关闭 Microsoft Word 2010 配置窗口 References 出现这种问题&#xff0c;主要是安装时所用账户和目前登陆的账户不为同一个账户造成的。或者你进行过覆盖安装或是重新安装过系统&#xff0c;但是 office 的安装目录没有更改。先激活 Microsoft Office&#xff0c;然后执行下列…

如何使用OpenCV扫描图像、查找表和时间测量

返回&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;OpenCV4.9.0开源计算机视觉库核心功能&#xff08;核心模块&#xff09; ​ 编辑 目标 我们将寻求以下问题的答案&#xff1a; 如何浏览图像的每个像素&#xff1f;OpenCV 矩…

【前端Vue】HR-saas中台项目开发md文档第1篇:vuex基础-介绍,vuex基础-初始化功能【附代码文档】

HR-saas中台管理项目开发完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;vuex基础-介绍,vuex基础-初始化功能,vuex基础-state,vuex基础-mutations,vuex基础-actions,vuex基础-getters。项目课设计&#xff0c;人力资源的环境搭建vue-element-admin的了解和…

微服务篇-C 深入理解第一代微服务(SpringCloud)_V 深入理解Config分布式配置中心

原创作者&#xff1a;田超凡&#xff08;程序员田宝宝&#xff09; 版权所有&#xff0c;引用请注明原作者&#xff0c;严禁复制转载 Part 1 理论部分 1 什么是SpringCloud Config&#xff1f; 当一个系统中的配置文件发生改变的时候&#xff0c;我们需要重新启动该服务&am…

信号处理--基于FBCSP滤波方法的运动想象分类

目录 理论 工具 方法 代码获取 理论 通用空间模式 (CSP) 算法可以用来有效构建最佳空间滤波器区分&#xff0c;然后实现运动想象的数据中的脑电信号的区分。然而&#xff0c;空间滤波器性能的好坏主要取决于其工作频带。如果脑电信号没有经过滤波或者滤波的频带范围不合适…

贝尔曼最优方程【BOE】

强化学习笔记 主要基于b站西湖大学赵世钰老师的【强化学习的数学原理】课程&#xff0c;个人觉得赵老师的课件深入浅出&#xff0c;很适合入门. 第一章 强化学习基本概念 第二章 贝尔曼方程 第三章 贝尔曼最优方程 文章目录 强化学习笔记一、最优策略二、贝尔曼最优方程(BOE)三…

边缘计算【智能+安全检测】系列教程-- Jeton Agx Orin 基础环境搭建

1 .前期准备 Jetson Agx Orin 比Jetson Agx Orin Xavier的算力要高&#xff0c;性能要好通常用来做自动驾驶的AI推理&#xff0c;具体外观如下图 1.刷机软件sdkmanager&#xff1a;下载链接 NVIDIA账号需要注册&#xff0c;正常一步一步往下走就行。在ubuntu18以上的系统安…

每日一题 --- 两两交换链表中的节点[力扣][Go]

两两交换链表中的节点 题目&#xff1a;24. 两两交换链表中的节点 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。 示例 1&a…

图解Kafka架构学习笔记(三)

准备Kafka环境 这里推荐使用Docker Compose快速搭建一套本地开发环境。 以下docker-compose.yml文件用来搭建一套单节点zookeeper和单节点kafka环境&#xff0c;并且在8080端口提供kafka-ui管理界面。 version: 2.1services:zoo1:image: confluentinc/cp-zookeeper:7.3.2hos…

ARMday7作业

实现三个按键的中断&#xff0c;现象和代码 do_ipr.c #include "stm32mp1xx_gic.h" #include "stm32mp1xx_exti.h" extern void printf(const char *fmt, ...); unsigned int i 0; void do_irq(void) {//获取要处理的中断的中断号unsigned int irqnoGI…

抽取CLOB字段中XML的特定元素的VALUE值

在ORACLE数据库中&#xff0c;有时XML文件会被保存在CLOB字段中。 这时候&#xff0c;若是我们要获取此字段XML中特定元素的VALUE值&#xff0c;就需要用到xmltype 这个函数。 如下面的 XML文件&#xff0c;保存在 TABLE_A 的CLOB_K 字段&#xff0c;若是我们要获取其中的 Y…