【简介】限流

news2025/5/16 22:52:52

限流

  • 为什么要限流
  • 限流算法
    • 单机限流
      • 计数器算法
      • 滑动窗口算法
      • 漏桶算法
      • 令牌桶算法
    • 分布式限流
    • 配额算法
  • 限流策略
  • 限流位置

为什么要限流

作为有追求的程序员,我们都希望自己的系统跑的飞快,但是速度再快,系统处理请求耗时也不可能为0,这样我们的系统也就无法做到处理能力无上限。此外,机器CPU、磁盘IO、内存、网络带宽也都有自己的处理上限的,这也进一步限制系统处理能力上限。
当请求超过了系统上限,系统的某处节点必然存在瓶颈:

  • CPU使用率100%:没有空闲CPU只能排队等调度
  • 内存满了:无法申请新的内存,频繁发生内存置换影响处理速度
  • 磁盘IO满了:后续读写请求只能排队等调度
  • 网络带宽满了:后续收发网络包只能排队进出

如果不采取任何措施,新请求源源不断的进来,旧请求依旧排队等待处理,但往往请求都有超时时间,当超时后用户可能会发起重试,这样就会进一步使情况恶化。当系统复杂时,各个节点都可能出现超时,最终导致大量请求超时,系统成功处理数急剧下降,从而导致系统雪崩,所有用户无法使用。

为了避免以上情况出现,我们必须想法来保护我们的系统,避免出现雪崩。降级限流是经常采用的两种手段:

  • 限流-事前预防:保证请求量在系统处理的安全范围内
  • 降级-事中应对:保证核心系统可用

下面我们主要围绕限流来进行讲述。

限流算法

单机限流

在微服务兴起之前,主要是单机限流,也叫本地限流,其对应的主要算法有 计数器滑动窗口算法漏桶算法令牌桶算法

计数器算法

这个是最容易想到的算法,思想:在指定的时间窗内,如果请求数超过预期数,则认为流量过大,需要限流。
计数器算法
实现思路:
统计单位时间内(比如1分钟内)的请求数,如果超过阈值,则拒绝请求,否则累加计数器。下个单位时间(下1分钟)开始时,重新开始计数。

#include <chrono>

typedef std::chrono::duration<long, std::milli> MilliTimeUnit;

class CounterLimit {
public:
    CounterLimit(int max_limit, MilliTimeUnit interval) {
    	this->max_limit = max_limit;
    	this->interval = interval;
    	base_time = std::chrono::steady_clock::now();
    	counter = 0;
    }

    bool canPass(int num) {
        std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
    	MilliTimeUnit time_span = std::chrono::duration_cast<MilliTimeUnit>(now - base_time);
    	//当前时间跟基准时间超过指定时间窗时,重置计数器和基准时间
    	//这里没必要精确控制时间窗的起始和截止时间
	    if (time_span.count() > interval.count()) {
	        counter = 0;
	        base_time = now;
	    }
	    counter += num;
    	return counter < max_limit;
    }

private:
    int max_limit;
    MilliTimeUnit interval;
    unsigned long counter;
    std::chrono::steady_clock::time_point base_time;
};

优点:实现简单
缺点:无法应对时间窗口临界的突发请求场景

时间窗口临界的突发请求是指在当前周期和下一周期的交界处瞬间有大量请求过来,比如设定1分钟1000个请求,用户在当前周期的最后1秒和下个周期第1秒突然发起大量请求。
临界处的突发请求

期望并发流量是1000/分钟,但在临界处的瞬时流量就达到2000/分钟,超过我们系统的处理上限,可能导致系统雪崩。

也许有人会说1分钟粒度太粗了,我们可以将时间窗口设置为1秒,但是即使将时间窗口设置为1秒也会有类似问题,对坏坏的高手而言,他们可以在最后10毫秒发来大量请求。

滑动窗口算法

计数器算法的窗口前移是整个窗口往前移动,此时就会存在窗口边界。为了消除窗口边界,我们可以将窗口缓慢向前滑动,类似TCP的滑动窗口,这样就解决了计算器算法的临界处突发请求的问题。

思想:将单位时间窗等分n份,比如1s等分5格(每格代表200ms),每隔200ms窗口向前移动一格,每格都有一个计数器记录该窗格时间内的请求数。当所有窗格的请求数之和大于阈值时,则需要限流。
滑动窗口

#include <chrono>
#include <vector>

typedef std::chrono::duration<long, std::milli> MilliTimeUnit;
class SlidingTimeWindow {
public:
    SlidingTimeWindow(int max_limit, int window_size, MilliTimeUnit window_interval) {
        this->max_limit = max_limit;
	    this->window_interval = window_interval;
	    base_time = std::chrono::steady_clock::now();
	    window_idx = 0;
	    total = 0;
    }

    bool canPass(int num) {
	    int window_size = window.size();
	    std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
	    MilliTimeUnit time_span = std::chrono::duration_cast<MilliTimeUnit>(now - base_time);
	    //最新请求所处的窗格编号
	    int new_window_idx = time_span / window_interval;
	    if (new_window_idx - window_idx >= window_size) {
	    	//当最新请求时间和最近请求时间的时间间隔超过整个时间窗时,直接重置窗口
	        for (int idx = 0; idx < window_size; ++idx) {
	            window[idx] = 0;
	        }
	        total = 0;
	        window_idx = 0;
	        base_time = now;
	    } else if (new_window_idx > window_idx) {
	    	//当最新请求时间和最近请求时间的时间间隔不超过整个时间窗时,将最近窗格和最新请求窗格之间的窗格重置
	        for (int idx = window_idx + 1; idx <= new_window_idx; ++idx) {
	            int wIdx = idx % window_size;
	            total -= window[wIdx];
	            window[wIdx] = 0;
	        }
	        window_idx = new_window_idx;
	    }
	    total += num;
	    window[window_idx % window_size] += num;
	    return total < max_limit;
    }

private:
    int max_limit;
    MilliTimeUnit window_interval;
    //基准时间,用于计算当前时间对应的时间窗窗格
    std::chrono::steady_clock::time_point base_time;
    //时间窗的起始窗格
    long window_idx;
    std::vector<int> window;
    long total;
};

优点:解决了临界处的突发流量
缺点:实现相对复杂,每个窗格都需要记录请求数;无法平滑的处理请求流量

由于滑动窗口每次往前滑动一格,当时间窗内请求数达到上限后就拒绝新请求,因此可以解决临界处的突发流量问题。
在这里插入图片描述
以上图为例,当最后一个窗格达到阈值后,后续4个窗格时间内的新请求都将会被拒绝。理想情况下,我们希望请求均匀的进来,这样系统就可以平滑的处理所有请求。但现实是我们没法控制请求的频率,这样就导致滑动窗口面对突发流量时,没法做到平滑处理。

要做到请求平滑处理,要么控制请求进来的速度,要么控制请求出去的速度。因此,也就有了以下两种算法:

  • 漏桶算法:限制请求出去的速度
  • 令牌桶算法:限制请求进来的速度

漏桶算法

思想:请求随机进来,先放入一个桶里,但桶下方有个洞,该洞可以匀速流出请求。
漏桶算法
常规实现:有个计数器记录当前桶中剩余请求数,开启一个线程定期扣减桶中的请求数,当新请求来临时,检查请求数是否超过桶的最大限制,如果超过则判定限流。

上述实现需要开启一个线程来专门扣减桶中的请求数,这种方式将浪费一个线程资源,可以通过请求时间计算来实现桶中请求数的扣减:
桶中剩余请求数 = Max(0, 桶中当前请求数 - ((请求时间 - 基准时间)/ 频率 - 上次流逝请求数))

#include <chrono>

typedef std::chrono::duration<long, std::milli> MilliTimeUnit;

class LeakyBucket {
public:
    LeakyBucket(int max_limit, MilliTimeUnit interval) {
        this->max_limit = max_limit;
	    this->interval = interval;
	    counter = 0;
	    base_time = std::chrono::steady_clock::now();
	    last_output = 0;
    }

    bool canPass(int num) {
	    std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
	    MilliTimeUnit time_span = std::chrono::duration_cast<MilliTimeUnit>(now - base_time);
	    //基于基准时间计算桶的总流出量,减去上次的总流出量last_output 即为上次请求时间到当前时间的请求流出量
	    long new_output = time_span.count() / interval.count();
	    //需要保证桶中剩余请求数一定不小于0
	    counter = std::max(0L, counter - (new_output - last_output));
	    //更新上次流出的请求数
	    last_output = new_output;
	    if (counter + num <= max_limit) {
	        counter += num;
	        return true;
	    }
	    return false;
    }

private:
    int max_limit;
    MilliTimeUnit interval;
    long counter;
    std::chrono::steady_clock::time_point base_time;
    long last_output;
};

优点:保证了系统处理速度恒定,避免了瞬间峰值的压力
缺点:当系统资源充足时,无法应对突发流量;当依赖消息队列时,则存在请求处理时延
在具体使用场景中,有以下方式:

  • canPass通过后,直接调用后端请求
    这里要求请求处理速度要跟桶流出速率保持一致,当桶流速大于请求处理速度,将可能导致限流逻辑失效,即桶未满但系统已高负载。
  • canPass通过后,请求放入消息队列
    这种场景将会导致请求的处理有时延,不适合时延敏感的场景。如果引入了消息队列,感觉没必要在用漏桶了,直接在收到请求后就将请求放入消息队列中,后端慢慢处理。

令牌桶算法

思想:匀速往桶中放令牌,后端在处理请求前先获取令牌,如果取到令牌则可以处理,否则限流
令牌桶算法
常规实现:有个计数器记录当前桶中剩余令牌数,开启一个线程定期往桶中放入令牌,即增加计数器的值。当新请求来临时,需要从桶中取令牌,即减少计数器的值,当取到令牌,则继续处理请求,否则判定限流。

上述实现需要开启一个线程来专门往桶中放如令牌,这种方式将浪费一个线程资源,可以通过请求时间计算来实现往桶中放入令牌数:
桶中剩余令牌数 = Min(桶限额, 桶中剩余令牌数 + ((请求时间 - 基准时间)/ 频率 - 上次放入令牌数))

#include <chrono>

typedef std::chrono::duration<long, std::milli> MilliTimeUnit;

class TokenBucket {
public:
    TokenBucket(int max_limit, MilliTimeUnit interval) {
        this->max_limit = max_limit;
	    this->interval = interval;
	    counter = 0;
	    base_time = std::chrono::steady_clock::now();
	    last_output = 0;
    }

    bool canPass(int num) {
        std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
	    MilliTimeUnit time_span = std::chrono::duration_cast<MilliTimeUnit>(now - base_time);
	    //基于基准时间计算总放入令牌数,减去上次放入令牌数last_output 即为上次请求时间到当前时间的新增令牌数
	    long new_output = time_span.count() / interval.count();
	    counter = std::min((long) max_limit, counter + (new_output - last_output));
	    last_output = new_output;
	    if (counter >= num) {
	        counter -= num;
	        return true;
	    }
	    return false;
    }

private:
    int max_limit;
    MilliTimeUnit interval;
    long counter;
    std::chrono::steady_clock::time_point base_time;
    long last_output;
};

优点:支持一定的突发流量
缺点:系统需要预热,刚启动时没有令牌,初始请求都会失败

分布式限流

在微服务兴起之后,通过每台机器的单机限流来达到分布式限流目的,可能存在着以下问题:

  • 配置维护复杂:当机器处理能力不一样时,需要单独维护每台机器的限流配置
  • 无法准确反映全局情况:当请求分布不均时,可能会出现某台机器限流了,其他机器没请求

限流的思想是固定的,但分布式限流需要在单机限流的基础上解决限流统计信息如何在不同机器间共享的问题。对于该问题,第一反应就是将内存中的统计信息移到第三方组件中存储,比如Redis缓存。

如果每次请求都去访问第三方组件,这将会带来严重的性能问题。为了解决这里的性能问题,我们可以减少访问第三方组件的次数,比如一次拉取多个配额存储在本地,当请求来临时消耗本地配额,当本地配额不足时再去远端拉取新配额。

配额算法

思想:客户端在启动时从服务端拉取配额存到本地内存中,当请求来时,先扣减本地配额,如果扣减成功,则继续处理请求;如果扣减失败,则尝试从服务端拉取配额存储到本地,如果拉取后本地配额扣减成功,则继续处理请求,否则提示限流。
配额算法
实现过程中需要注意的点:

  • 配额具有时效性:每个时间段都应该有专门的配额计数器,需要定时从服务端拉取当前时间段内的配额信息,可以通过当前时间计算所对应的配额信息
  • 何时拉取配额:最好能做到预拉取,比如本地配额剩余5%时就异步拉取配额
  • 注意配额大小设置以及请求负载的均衡性,当请求负载不均时,可能会导致某台机器限流,其他机器无请求

限流策略

  • 拒绝请求
    当达到请求阈值时,直接拒绝请求。
    主要适用于实时性交互的场景、敏感的写操作等,比如交易下单等。
  • 缓存请求
    当被限流后,将请求放入消息队列或者缓存队列中,等服务空闲后,继续处理这些请求。
    主要适用于非实时性交互的场景,非敏感操作等,比如发送触达。

限流位置

  • 接入层限流:依赖全链路压测性能,可以按照请求接口限流,实施更简单
  • 应用层限流:依赖应用接口的压测性能,不同应用有不同的接口,这些接口有不同的性能,实施更复杂
  • 基础层限流:依赖底层服务(如数据库)的性能

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

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

相关文章

线程池下载4K壁纸

学习记录 学习记录 一、目标 aHR0cHM6Ly9waWMubmV0Ymlhbi5jb20vDQo 拿到每张图片的href 和 标题&#xff0c;跳转到详情页进行图片下载地址的提取并请求实现图片本地下载 二、代码实现 """ CSDN: 抄代码抄错的小牛马 mailbox&#xff1a;yxhlhm2002163.com…

计算机毕业论文内容参考|基于java的房产营销系统的设计与实现

文章目录 导文摘要前言绪论课题背景课题内容相关技术与方法介绍技术分析技术设计技术实现总结与展望本文总结后续工作展望导文 计算机毕业论文内容参考|基于java的房产营销系统的设计与实现 摘要 当今社会,随着经济的快速发展和人们对生活品质的不断提高,房地产行业正在面临…

Java --- docker安装redis

目录 一、拉取redis容器镜像 二、新建目录 三、拷贝redis.conf文件模板进/app/redis目录 四、修改redis.conf文件 五、启动redis镜像服务 一、拉取redis容器镜像 docker pull redis:7.0.0 二、新建目录 mkdir -p /app/redis 三、拷贝redis.conf文件模板进/app/redis目录 cp …

Java面试(4)面向对象

文章目录 概念1. 面向对象和面向过程的区别2. 面向对象三大特征 类1. 接口和抽象类有什么共同点和区别&#xff1f;2. 深拷贝和浅拷贝区别了解吗&#xff1f;什么是引用拷贝&#xff1f;3. 内部类1. 内部类2. 内部类的优点3. 内部类有哪些应用场景4. 局部内部类和匿名内部类访问…

python 3.8 + tensorflow 2.4.0 + cuda11.0 的问题

版本匹配 &#x1f517;从源代码构建 | TensorFlow 报错&#xff1a;Could not load dynamic library ‘cupti64_110.dll’; dlerror: cupti64_110.dll not found 是因为我电脑中的 cuda 版本以前是 10&#xff0c;现在是 11.4 &#xff0c;所以需要安装对应版本的 cudatoolk…

基于粒子群算法的微网经济优化调度——附Matalb代码

目录 摘要&#xff1a; 代码主要内容&#xff1a; 研究背景&#xff1a; 微电网模型&#xff1a; 粒子群算法&#xff1a; 运行结果&#xff1a; Matlab代码分享&#xff1a; 摘要&#xff1a; 提出了一种经济与环保相协调的微电网优化调度模型&#xff0c;针对光伏电池…

SAP入门到放弃系列之需求管理的基本要素

需求管理目标&#xff1a; 一般而言&#xff0c;生产计划&#xff08;PP&#xff09;的总体目标&#xff0c;特别是需求管理的总体目标是通过减少以下内容来更好地为客户服务&#xff1a; 补货提前期存货成本 需求管理的要素&#xff1a; 需求管理工作的主要要素广义上可分…

2013蓝桥杯真题省赛翻硬币 C语言/C++

[蓝桥杯 2013 省 B] 翻硬币 题目背景 小明正在玩一个“翻硬币”的游戏。 题目描述 桌上放着排成一排的若干硬币。我们用 * 表示正面&#xff0c;用 o 表示反面&#xff08;是小写字母&#xff0c;不是零&#xff09;&#xff0c;比如可能情形是 **oo***oooo&#xff0c;如果…

暴涨700w播放,星穹铁道恰饭频频登上B站爆款热榜!

B站作为现在年轻一代聚集的多元化社区&#xff0c;游戏内容则是社区内受众较为广泛的存在&#xff0c;而星铁作为面向年轻群体的回合制游戏&#xff0c;自然是赢得B站核心用户群体的青睐。 4月26日&#xff0c;暌违已久的手游《崩坏&#xff1a;星穹铁道》&#xff08;后文简称…

【Java】平时开发中遇到的问题你是否遇到过?

文章目录 前言一、六类典型空指针问题1.1包装类型的空指针问题1.2 级联调用的空指针问题1.3 Equals方法左边的空指针问题1.4 ConcurrentHashMap 这样的容器不支持 Key&#xff0c;Value 为 null。1.5 集合&#xff0c;数组直接获取元素1.6 对象直接获取属性 二、日期YYYY格式设…

Linux和UNIX的关系及区别(详解)

UNIX 与 Linux 之间的关系是一个很有意思的话题。在目前主流的服务器端操作系统中&#xff0c;UNIX 诞生于 20 世纪 60 年代末&#xff0c;Windows 诞生于 20 世纪 80 年代中期&#xff0c;Linux 诞生于 20 世纪 90 年代初&#xff0c;可以说 UNIX 是操作系统中的"老大哥&…

Fortinet FortiNAC RCE漏洞复现(CVE-2022-39952)

0x01 产品简介 FortiNAC(Network Access Control) 是Fortinet的一种零信任网络访问控制解决方案&#xff0c;可增强用户对企业网络上的物联网 (IoT) 设备的监控。NAC 是零信任网络访问安全模型的重要组成部分&#xff0c;在该模型中&#xff0c;IT 团队可以轻松了解正在访问网络…

低代码让开发变得不再复杂

文章目录 前言低代码 VS 传统开发为什么选择IVX&#xff1f;平台比对总结 前言 在数字化的时代背景下&#xff0c;企业都面临巨大的数字化转型的挑战。为了应对这样的挑战&#xff0c;企业软件开发工具和平台也在不断革新和发展。低代码开发平台随之应运而生&#xff0c;成为了…

面向对象编程与面向过程编程的区别

面向过程&#xff1a; 以坦克飞机大战为例&#xff0c;如果按面向过程的思路去书写代码&#xff0c;则需要按如下顺序进行编码&#xff1a; 游戏初始化确定一台坦克的位置绘制坦克确定多架飞机的位置绘制飞机为坦克设置上下左右四个转向为坦克添加按下键盘k键发射子弹为所有飞…

ATFX国际:国内成品油价格下调,国际油价仍维持震荡态势

ATFX国际&#xff1a;5月16日&#xff0c;发改委发布成品油价格下调通知&#xff0c;其中提到&#xff1a;国内汽、柴油价格每吨分别降低380元和365元。以上海地区为例&#xff0c;价格下调后&#xff0c;每吨汽油的售价为9255元&#xff0c;每吨柴油的价格为8225元。经计算&am…

【容器适配器的认识与模拟】

目录&#xff1a; 前言一、引入二、容器适配器&#xff08;一&#xff09;stackdequestack模拟实现 &#xff08;二&#xff09;queuequeue模拟实现为什么栈和队列要使用deque &#xff08;三&#xff09;priority_queuepriority_queue模拟实现 总结 前言 打怪升级&#xff1a…

小程序获取用户信息实现一键登录

文章目录 旧版获取用户信息实现登录流程login页面代码个人中心页面代码全局app.vue代码下面是小程序获取用户信息最新调整的方式 温馨提示 &#xff1a;以下小程序登录方式只适用于2.27.1版本库以下使用 详情请看微信官方文档调整 旧版获取用户信息实现登录流程 由于我是在hbu…

公司招了一个腾讯拿30K的人,让我见识到了什么是天花板···

前言 人人都有大厂梦&#xff0c;对于软件测试人员来说&#xff0c;BAT 为首的一线互联网公司肯定是自己的心仪对象&#xff0c;毕竟能到这些大厂工作&#xff0c;不仅薪资高待遇好&#xff0c;而且能力技术都能够得到提升&#xff0c;最关键的是还能够给自己镀上一层金&#…

Linux系统防火墙iptables(你委屈什么,爱而不得的又不止你一个)

文章目录 一、iptables防火墙概述1.简介2.netfilter/iptables关系3.iptables的四表五链&#xff08;1&#xff09;四表&#xff08;2&#xff09;五链 4.数据包过滤的匹配流程&#xff08;1&#xff09;入站&#xff08;2&#xff09;转发 二、iptables防火墙配置1.下载相关服务…

一个有点好用的信息收集工具

功能 domainscan 调用 subfinder 被动收集&#xff0c;调用 ksubdoamin 进行 dns 验证 泛解析、CDN 判断 获取 domain 相关的 web&#xff08;host:port&#xff09;资产&#xff0c;使用 webscan 扫描 webscan 支持 http/https scheme 自动判断 获取 statusCode、contentL…