滑动时间窗口统计 QPS

news2024/11/25 11:48:21

一、代码

1、先上实现代码,如下

package cn.jt.emqxspringbootdesignpattern.emqx.controller;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class QpsCounter {

    /**
     * 多少毫秒作为一个window 窗口(bucket)
     */
    private static final int SEGMENT_DURATION_MS = 250;
    /**
     * 间隔秒数
     */
    private static final int REPORT_INTERVAL_SECONDS = 1;

    // 几个 window 窗口,bucket ,这里默认4个,即250ms一个区间,1s内4个
    private static final int MAX_LIVED_WINDOW_SIZE = REPORT_INTERVAL_SECONDS * 1000 / SEGMENT_DURATION_MS;

    /**
     * 全局计数,每次都增加,在每隔一秒打印qps的时候,置空即可
     */
    private final AtomicInteger globalCounter;
    /**
     * 定时任务打印qps
     */
    private final ScheduledExecutorService scheduler;
    /**
     * 用于存放 区间标识
     */
    private final Queue<Integer> segmentQueue;

    public QpsCounter() {
        globalCounter = new AtomicInteger(0);
        scheduler = Executors.newScheduledThreadPool(1);
        // 一个线程安全的队列实现
        segmentQueue = new ConcurrentLinkedQueue<>();
    }

    public void start() {
        scheduler.scheduleAtFixedRate(this::reportQps, REPORT_INTERVAL_SECONDS, REPORT_INTERVAL_SECONDS, TimeUnit.SECONDS);
    }

    public void stop() {
        scheduler.shutdown();
        try {
            if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
                scheduler.shutdownNow();
            }
        } catch (InterruptedException e) {
            scheduler.shutdownNow();
        }
    }

    public void requestReceived() {
        // 用 当前毫秒数据 /  SEGMENT_DURATION_MS(250),计算出当前window窗口是那个
        int currentSegment = (int) (System.currentTimeMillis() / SEGMENT_DURATION_MS);
        globalCounter.incrementAndGet();
        segmentQueue.offer(currentSegment);
        removeOldestWindowIfNeeded();
    }

    private void removeOldestWindowIfNeeded() {
        while (segmentQueue.size() > MAX_LIVED_WINDOW_SIZE) {
            segmentQueue.poll();
        }
    }

    private void reportQps() {
        int totalQps = globalCounter.getAndSet(0);
        System.out.println("QPS in the last " + (MAX_LIVED_WINDOW_SIZE * SEGMENT_DURATION_MS) + " milliseconds: " + totalQps);
    }

    public static void main(String[] args) {
        QpsCounter qpsCounter = new QpsCounter();
        qpsCounter.start();

        for (int i = 0; i < 100; i++) {
            qpsCounter.requestReceived();
            try {
                // 这里的 睡眠 0.1s 就是模拟业务处理
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        qpsCounter.stop();
    }
}

2、运行上面的 main方法,可以得到控制台输出,大约每秒10个QPS

在这里插入图片描述

3、如何在自己的业务代码里面使用,我这里就是统计的 mqttPlatformClient.publish(xxxx)的qps,换成你自己的业务代码即可

其实就是在自己的业务代码里面,开启即可,每次请求之后,我们执行 qpsCounter.requestReceived(); 计数即可

在这里插入图片描述

二、基本理论:

下面的的理论内容,原文是 Snetinel的滑动时间窗口是如何统计QPS的 ,说的挺好的,通俗易懂

1、首先我们要明确:任意时刻的 QPS 是指过去一秒内产生的请求个数。所以可以推导出,如果要获得时间轴上 X 位置上的 QPS,应该统计的是在 [X-1000ms,X) 这个时间范围通过的请求数。


2、所以,我们应该这么做(毫秒为维度):

3、每一次请求过来,我使用当前时间毫秒数在内存中查找是否存在一个统计请求个数的原子计数器,如果没有则创建并且计数器 + 1。当在 X 时刻,需要统计 QPS 时,我可以获得 [X-1000ms,X) 这个区间内所有存在的计数器做累加,就可以获得 X 时刻的 QPS 了。

4、这样做的问题在于:粒度太细了,在系统本身流量比较大的情况下,每秒可能就会产生几百个计数器,因为我们要限制 QPS,所以每次请求都需要做统计去和设置的阈值进行比较来判断本次请求应该被拦截或者放行。

5、所以我们做下优化:不用每一个毫秒刻度对应一个计数器,可以把时间窗口分成很多个段(sentinel 里面叫 Bucket),每个分段一个计数器,如下图所示:

6、我把 [X-1000,X) 这一秒的时间窗口分为了四段,也就是说每段为 250ms。当请求 Request-1 过来的时候,可以计算得到它处于 A 段,和前面的做法一样,我们先判断 A 段的计数器是否存在,不存在则创建并且计数器 + 1,这个时候我需要统计 QPS 的话,只需要往前再拿三个段的计数器,加上本身所在这个段的计数器求和就行了(也就是 A,B,C,D 这四个段,Request-2 过来的时候,就统计 B,C,D,E 这四个段)。这样对资源的消耗可以大大的减少。它的缺点就在于,如果段越宽,粒度就越粗,统计的 QPS 就会偏差越大(sentinel 默认为两段,每段 500ms)。

7、时间是永恒的,如果把每一秒的统计数据都放在内存,内存只会被无限消耗。并且,时刻 X 的 QPS 只需要前一秒的数据就好了,至于一秒之前的数据,则可以丢弃不用或者归档用于监控等。所以我们需要清理过期的段,清理的方法很多,这里介绍一下 sentinel 是怎么做的。

8、sentinel 把一秒钟的长度设计成一个圆环形,类似钟表,钟表只能显示 24 小时,而 sentinel 的圆环表示了一秒钟。

9、当 Request 进来时,首先确定这个 request 应该处于哪个段(比如我们可以通过当前时间戳的后三位 [0,999] 来确定)。

10、我们找到段 A 后,首先判断当前段的实例,是否存在,如果不存在,则创建,并记录当前段的开始时间 X。

11、如果存在,则判断当前段是否已经过期(因为不仅是 X+100 会落到 A 段,X-1000+100 也会落到 A 段),过期则重新创建并更新当前段的开始时间。

12、统计时也需要判断段是否过期,过期的则不要统计。

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

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

相关文章

如何提取视频中的音频转为mp3

如何提取视频中的音频转为mp3&#xff1f;在丰富多样的视频作品中&#xff0c;我们常常会遇到一些引人入胜的对话和有趣的音乐变奏。不少朋友可能曾经看过那种画面与其他作品声音巧妙搭配&#xff0c;给人带来无比愉悦和和谐感的趣味作品。然而&#xff0c;很多人虽然有着相似的…

小程序中如何查看会员卡的注册时间

会员系统是小程序中非常重要的一部分&#xff0c;可以帮助企业更好地管理客户&#xff0c;并提供更好的服务。在实际应用中&#xff0c;我们经常需要查看会员的注册时间&#xff0c;以便更好地了解客户的行为和需求。本文将介绍小程序如何查看会员的注册时间。 1. 找到指定的…

uniapp后台播放音频功能制作

在UniApp中&#xff0c;你可以使用uni.getRecorderManager()方法来创建一个录音管理器实例。但是&#xff0c;请注意&#xff0c;录音管理器并不直接用于后台音频播放功能&#xff0c;而是用于录制音频。如果想要在后台播放音频&#xff0c;你需要使用uni.getBackgroundAudioMa…

小程序壁纸demo,数据采集第三方的,没有服务端

概述 小程序demo&#xff0c;共有三个页面&#xff0c;首页&#xff0c;详情&#xff0c;搜索&#xff0c;数据来源于第三方。有兴趣的可以看看&#xff0c;比较简单 详细 小程序demo&#xff0c;共有三个页面&#xff0c;首页&#xff0c;详情&#xff0c;搜索&#xff0c;…

2种方法,jmeter用一个正则提取器提取多个值!

jmeter中&#xff0c;用json提取器&#xff0c;一次提取多个值&#xff0c;这个很多人都会。但是&#xff0c;用正则提取器一次提取多个&#xff0c;是否可以呢&#xff1f; 肯定&#xff0c;很多人都自信满满的说&#xff0c;可以&#xff01;形如&#xff1a;token":“…

STM32单片机入门学习(一)

一、购入硬件装备 心血来潮&#xff0c;想学STM32&#xff0c;话不多说&#xff0c;先把东西买了STM32F103C8T6开发板ST-LINK下载器&#xff0c;小元器件自备。 二、安装软件装备 1.Keil uVision5安装 其他都是下一步。 2.用注册机给 Keil 5 注册 打开keil 5&#xff0c;打…

【C++】unordered_map与unorder_set的封装(哈希桶)

文章目录 前言一、模板参数的改造二、模板的特例化操作三、仿函数的妙用四、unordered迭代器基本操作1.const迭代器注意&#xff1a;2.HashTable与HTIterator的冲突 五、迭代器的构造问题六、完整代码1.hash_bucket.h2.unordered_set.h3.unordered_map.h 前言 我们开辟一个指针…

340. 至多包含 K 个不同字符的最长子串

340. 至多包含 K 个不同字符的最长子串 vip

Michael.W基于Foundry精读Openzeppelin第36期——Ownable2Step.sol

Michael.W基于Foundry精读Openzeppelin第36期——Ownable2Step.sol 0. 版本0.1 Ownable2Step.sol 1. 目标合约2. 代码精读2.1 pendingOwner() && transferOwnership(address newOwner) && _transferOwnership(address newOwner)2.2 acceptOwnership() 0. 版本 …

YOLOv8快速复现 训练 SCB-Dataset3-S 官网版本 ultralytics

目录 0 相关资料SCB-Dataset3-S 数据训练yaml文件 YOLOv8 训练SCB-Dataset3-S相关参数 0 相关资料 YOLOV8环境安装教程.&#xff1a;https://www.bilibili.com/video/BV1dG4y1c7dH/ YOLOV8保姆级教学视频:https://www.bilibili.com/video/BV1qd4y1L7aX/ b站视频&#xff1a;…

第一百五十回 自定义组件综合示例:游戏摇杆

文章目录 概念介绍实现方法示例代码我们在上一章回中介绍了自定义组件相关的内容,本章回中将综合使用这些内容 自定义游戏摇杆组件.闲话休提,让我们一起Talk Flutter吧。 概念介绍 我们介绍的游戏摇杆就是一个内层的小圆嵌套一个外层的大圆,大圆的位置不变,小圆只能在大圆…

【AD9361】设置带宽

接收链路滤波器 RX TIA LPF&#xff1a;RX TIA LPF 是一款单极点低通滤波器&#xff0c;具有可编程 3dB 转角频率。转折频率可在 1 MHz 至 70 MHz 范围内进行编程。RX TIA LPF 通常校准为基带通道带宽的 2.5 倍。 RX BB LPF&#xff1a;RX BB LPF 是具有可编程 3dB 转角频率的…

2、Window上的 虚拟机端口 暴露到 宿主机局域网教程

今天在公司的服务器主机上捣鼓虚拟机&#xff0c;要在虚拟机上安装一个oracle&#xff0c;虚拟机和主机能互相ping通的前提下&#xff0c;要将虚拟机上的端口号暴露在主机上&#xff0c;让项目组内的所有员工的电脑都能访问到该oracle数据库。 也就是电脑A 访问主机&#xff0…

Linux -- 使用多张gpu卡进行深度学习任务(以tensorflow为例)

在linux系统上进行多gpu卡的深度学习任务 确保已安装最新的 TensorFlow GPU 版本。 import tensorflow as tf print("Num GPUs Available: ", len(tf.config.list_physical_devices(GPU)))1、确保你已经正确安装了tensorflow和相关的GPU驱动&#xff0c;这里可以通…

Scala 高阶:Scala中的模式匹配

一、概述 Scala中的模式匹配&#xff08;case&#xff09;类似于Java中的switch...case&#xff0c;但是Scala的模式匹配功能更为强大。通过模式匹配&#xff0c;可以匹配更复杂的条件和数据结构&#xff0c;包括常量、类型、集合、元组等。而 Java 的 switch 语句只能用于匹配…

字符串函数----篇章(1)

目录 补上章缺失的两道题 七.笔试题&#xff08;7&#xff09; 八.笔试题&#xff08;8&#xff09; 一.字符串函数 ( 1 )----strlen函数 二.字符串函数 ( 2 )----strcpy函数 2-1模拟实现strcpy 三.字符串函数 ( 3 )----strcmp函数 ​编辑 3-1模拟实现strcmp 四.字符串函…

新建excel出现由于找不到vcruntime140_1.dll,无法继续执行代码。系统错误

打开excel报错&#xff0c;提示缺少​​vcruntime140_1D.dll​​。 那解决方法无疑就是找到这个​​DLL​​​&#xff0c;然后放到电脑系统中。 在网站​中搜索​vcruntime140_1.dll&#xff0c;下载 把你下载的文件放到系统中&#xff0c;把dll文件放到​​C:\Windows\Syst…

2、Window上的虚拟机暴露到宿主机局域网教程

今天在公司的服务器主机上捣鼓虚拟机&#xff0c;要在虚拟机上安装一个oracle&#xff0c;虚拟机和主机能互相ping通的前提下&#xff0c;要将虚拟机上的端口号暴露在主机上&#xff0c;让项目组内的所有员工的电脑都能访问到该oracle数据库。 也就是电脑A 访问主机&#xff0…

1997-2023年1月中国企业跨国并购数据Zephyr数据库

1997-2023年1月中国企业跨国并购数据 1、时间&#xff1a;1997-2023年1月 2、来源&#xff1a;根据Zephyr数据库整理形成 3、样本量&#xff1a;3.6万 4、指标&#xff1a;变量信息全面&#xff0c;包括交易的详细时间、金额、标的、介绍、行业等等具体如下&#xff1a; D…