系统设计 - 设计一个速率限制器

news2025/1/16 4:56:58

实施速率限制器的位置主要取决于我们的应用程序、技术栈、技术团队等因素。通常有三个位置可供选择:客户端、服务器端或中间件。

客户端是不可靠的地方来执行速率限制,因为恶意行为者可以轻易伪造客户端请求。

比将速率限制器放在服务器端更好的方法是使用速率限制器中间件,它甚至可以对我们的服务器端进行限流。因此,如果您正在使用微服务架构,并且已经使用类似身份验证中间件的功能,则可以在其旁边实现速率限制器中间件。

75500ea6aa67478a2a4d0a9426000d0b.png

有许多速率限制算法可供选择:我们将介绍一些算法,包括“令牌桶”、“漏桶”、“滑动窗口计数器”等。

令牌桶算法

Stripe (点击此处阅读)[1] 使用令牌桶算法来限制他们的 API 请求。

根据 Stripe 技术博客:

我们使用令牌桶算法来执行速率限制。这个算法有一个中央的桶主机,你每次请求时都从中取出令牌,并慢慢地向桶中滴入更多令牌。如果桶是空的,就拒绝请求。在我们的情况下,每个 Stripe 用户都有一个桶,每次他们发出请求时,我们就从那个桶中删除一个令牌。我们使用 Redis 实现我们的速率限制器。

39d8f6564bb1d8ab678790dad0c8e4e5.png

桶中有预定义容量的令牌。当请求到达时,它从桶中取出一个令牌并进一步处理。如果没有令牌可供获取,则请求将被丢弃,用户将不得不重试。

示例用例:

•我们将速率限制规则设置为每个用户每分钟 3 个请求。•用户1在 00 时间间隔发出请求,当前桶已满,有 3 个令牌,因此请求将被处理。现在桶中的令牌数量更新为 2。•用户1在第 10 秒发出第二个请求,桶中有 2 个令牌,所以请求将进一步处理。现在桶中的令牌数量更新为 1。•用户1在第 30 秒发出第三个请求,桶中有 1 个令牌,所以请求将进一步处理。现在整整 1 分钟内桶都为空。•用户1在第 55 秒发出第四个请求,桶中没有令牌,因此请求被限制,用户将收到 429 状态码 - 请求太多,并被要求稍后重试。HTTP 429 Too Many Requests 响应状态码表示用户在给定的时间内发送了太多请求。•在那 1 分钟的完成时,令牌计数以固定速率刷新,并且桶再次装满了 3 个令牌,可以再次为该特定用户处理请求。

简单来说:

在令牌桶算法中,我们每次请求都会从桶中处理一个令牌。新令牌以速率 r 放入桶中。桶最多可以容纳 b 个令牌。如果请求到来时桶已满,则该请求将被丢弃。

令牌桶算法需要两个参数:

81a76f037f39812886e043eac1ee526d.png

令牌桶算法代码示例

package main


import (
    "fmt"
    "math"
    "time"
)


const (
    MAX_BUCKET_SIZE float64 = 3
    REFILL_RATE     int     = 1
)


type TokenBucket struct {
    currentBucketSize   float64
    lastRefillTimestamp int
}


func (tb *TokenBucket) allowRequest(tokens float64) bool {
    tb.refill() //refill of bucket happening at constant REFILL_RATE


    if tb.currentBucketSize >= tokens {
        tb.currentBucketSize -= tokens
        return true
    }


    return false
}


func getCurrentTimeInNanoseconds() int {
    return time.Now().Nanosecond()
}


func (tb *TokenBucket) refill() {
    nowTime := getCurrentTimeInNanoseconds()


    tokensToAdd := (nowTime - tb.lastRefillTimestamp) * REFILL_RATE / 1e9


    tb.currentBucketSize = math.Min(tb.currentBucketSize+float64(tokensToAdd), MAX_BUCKET_SIZE)
    tb.lastRefillTimestamp = nowTime
}


func main() {
    obj := TokenBucket{
        currentBucketSize:   3,
        lastRefillTimestamp: 0,
    }


    fmt.Printf("Request processed: %v\n", obj.allowRequest(1)) //true
    fmt.Printf("Request processed: %v\n", obj.allowRequest(1)) //true
    fmt.Printf("Request processed: %v\n", obj.allowRequest(1)) //true
    fmt.Printf("Request processed: %v\n", obj.allowRequest(1)) //false, request dropped
}

Leaky Bucket Algorithm

Leaky Bucket是使用队列实现速率限制的一种简单直观的方式。它是一个先进先出的队列(FIFO)。进来的请求被附加到队列中,如果没有足够的空间容纳新的请求,则会被丢弃(泄漏)。

2cfc1632ba54d8a8c2b53647056d3f5c.png

•当一个请求到达时,系统检查队列是否已满。如果没有满,则将请求添加到队列中。•否则,请求将被丢弃。•请求以固定的间隔从队列中提取并处理。

算法的工作原理:

d7230a414f5d7cdf86187f353f18731a.png

泄漏桶算法接受以下两个参数:

•桶大小:等于队列大小。队列保存要以固定速率处理的请求。•流出速率:定义在固定速率内可以处理多少请求,通常以秒为单位。

该算法的优点是,它平滑处理请求的突发,并以大致平均的速率处理它们。

Sliding Window Algorithm

•该算法跟踪请求的时间戳。时间戳数据通常保存在缓存中,例如 Redis 的有序集合。•当一个新请求到来时,删除所有过期的时间戳。过期的时间戳被定义为早于当前时间窗口的开始时间。•将新请求的时间戳添加到日志中。•如果日志大小与允许的请求数相同或更低,则接受请求。否则,拒绝该请求。

2858fb4b8611ad48b5b877268fbe43e7.png

在下面的示例中,速率限制器允许我们每分钟2个请求。窗口内超出此限制的请求将被丢弃。

c7932999503df19c9cc522c8c6a2c478.png

注意: 为了便于理解,我们使用了hh:mm:ss格式,但在redis中,我们将推送UNIX时间戳。

•当一个新请求在1:00:01到达时,日志为空。因此,请求被允许。•一个新请求在1:00:30到达,时间戳1:00:30被插入日志中。插入后,日志大小为2,不大于允许的请求数。因此,请求被允许。•一个新请求在1:00:50到达,时间戳被插入到日志中。插入后,日志大小为3,大于允许的大小2。因此,该请求被拒绝,即使时间戳仍然存在于日志中。•一个新请求在1:01:40到达。在[1:00:40,1:01:40)范围内的请求在最新时间范围内,但在1:00:40之前发送的请求已过时。两个过时的时间戳1:00:01和1:00:30从日志中删除。删除操作后,日志大小变为2;因此,该请求被接受。

速率限制器的详细设计:

我们将使用以下组件:

•配置和数据存储来存储速率限制器规则。•工作进程经常从磁盘中获取规则并将其存储在缓存中。•速率限制器中间件从缓存中获取规则。它还从Redis中获取时间戳、计数器等数据。当请求到来时,它进行计算并决定是处理该请求还是对其进行速率限制。•如果需要对请求进行速率限制,这里有两个选项•选项1:拒绝请求并向客户端发送状态代码429: too many requests。•选项2:将请求推送到消息队列以稍后处理该请求。

35c334e6d02b9e65246964295919e5ec.png

现在,我们可能需要将以上系统扩展到分布式环境中,以支持多个服务器和并发线程。这里可能会出现两个挑战:

竞争条件

如上所述:

从Redis中读取计数器值,检查(计数器+1)是否超过阈值。如果没有,将计数器值在Redis中增加1。

ae35066bdddf948009dc18d1add03243.png

假设Redis中的计数器值为3(如上图所示)。如果两个请求并发读取计数器值,而在任一请求写回值之前,都不检查另一个线程。它们都会将计数器增加1并写回,而不会检查其他线程。两个请求(线程)都认为它们有正确的计数器值4。但是,正确的计数器值应该是5。

锁可以在这里使用,但这可能会影响性能。因此,我们可以使用Redis Lua脚本[2]

•这可能会导致更好的性能。•此外,脚本中的所有步骤都以原子方式执行。在脚本执行时,没有其他Redis命令可以运行。

同步

在分布式环境中,同步是另一个需要考虑的重要因素。为了支持数百万用户,一个速率限制服务器可能无法处理流量。当使用多个速率限制服务器时,需要同步。

一种可能的解决方案是使用黏性会话,允许客户端将流量发送到相同的速率限制器。这种解决方案既不可扩展也不灵活。更好的方法是使用像Redis这样的集中式数据存储。

在所有事情都就绪之后,我们还可以监视我们的速率限制器以获取性能、规则、算法有效性等指标。例如,如果速率限制规则过于严格,会丢弃许多有效请求。在这种情况下,我们希望稍微放松一下规则。在另一个例子中,我们注意到当有突然增加的流量,如抢购时,我们的速率限制器变得无效。在这种情况下,我们可以更改算法以支持突发流量。令牌桶在这里非常合适。

引用链接

[1] (点击此处阅读)https://stripe.com/blog/rate-limiters
[2] Redis Lua脚本: https://www.freecodecamp.org/news/a-quick-guide-to-redis-lua-scripting/

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

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

相关文章

海量视频处理的应对和算法实践

随着短视频、直播、智慧城市、5G等的快速发展,视频内容铺天盖地,五花八门,相应的处理需求也多种多样。如何能高效地应对?需要在数据处理系统,底层计算能力,以及算法研究等多方面协同努力。LiveVideoStackCo…

深度:ChatGPT只是表面的喧嚣,大模型才是那柄尖刀!

‍数据智能产业创新服务媒体 ——聚焦数智 改变商业 如果把时钟拨到2023年底,当我们回过头来看今年科技界最激动人心的大事件,ChatGPT的横空出世无疑会占据一席之地。就像几年前大家被谷歌AlphaGo点燃对人工智能的热情一样,人们对ChatGPT的热…

安装 Jenkins 2.346.3 LTS

环境: apache-tomcat-8.5.88 Jenkins 2.346.3 LTS Running Jenkins system Jenkins requires Java 11 or 17 since Jenkins 2.357 and LTS 2.361.1. Read more about this in the announcement blog post. https://www.jenkins.io/doc/administration/requirement…

SSM_jsp游戏-账-号-装-备虚拟物品交易系统

开发语言:Java 框架:ssm 前端框架:jsp/Bootstrap JDK版本:JDK1.8 服务器:tomcat8 数据库工具:Navicat 开发软件:idea 支持eclipse 游戏账号交易系统主要是为了提高工作人员的工作效率和更方便快捷的满足用户…

AIPRM for ChatGPT 是一个 Chrome 浏览器扩展程序

AIPRM for ChatGPT 是一个 Chrome 浏览器扩展程序,基于 Chromium 内核开发的浏览器,都可以使用该扩展,比如微软的 Edge 浏览器等。 在 AIPRM 的帮助下,我们可以在 ChatGPT 中一键使用各种专门为网站 SEO、SaaS、营销、艺术、编程…

ZBX_NOTSUPPORTED: Unsupported item key.

问题 ZBX_NOTSUPPORTED: Unsupported item key. 详细问题 笔者安装zabbix后,自定义item key进行测试。需在zabbix-server 端 切换目录: cd /usr/local/zabbix/bin 执行查询命令: ./zabbix_get -s 192.168.174.136 -p 10050 -k “home.file…

学编程遇到问题,如何更好地提问?

入门教程、案例源码、学习资料、读者群 请访问: python666.cn 大家好,欢迎来到 Crossin的编程教室 ! 一个编程学习者,尤其是刚入门的初学者,在学习过程中必然会遇到各种问题,于是难免需要向人提问寻求解答。…

让AI替你打工?GPT提升开发效率指南

👉腾小云导读 开发者日常的整个工作流中,AI 大模型能做什么?ChatGPT 等 AI 大模型能不能通过开发者的指引,一步步完成从技术方案输出、编码、测试、发布到运营维护的整套流程?使用中有什么避坑点?本文从实际…

【观察】共建“伙伴+华为”背后,是华为平台到体系的战略“升维”

2017年,在当年的“伙伴大会”上,华为首次提出以“平台生态”双轮驱动的战略,以更加开放的心态积极拥抱产业变化,通过和产业链上各个层级的合作伙伴一起合作,共同实现新的市场突破。 彼时中国企业和行业的数字化转型刚刚…

2023智源大会议程公开 | 生成模型论坛

2023年,人工智能新研究、新系统、新产品竞放——我们即将见证另一场有关智能的惊叹演化。6月9日,2023北京智源大会,将邀请这一领域的探索者、实践者、以及关心智能科学的每个人,共同拉开未来舞台的帷幕,你准备好了吗&a…

chatgpt赋能Python-python_geo

Python在地理空间数据分析中的应用 在地理空间信息系统领域,Python已经成为最流行的编程语言之一。Python有许多强大的地理空间库,例如GDAL,Shapely和Fiona等,这使得它成为了地理空间数据分析中不可或缺的一部分。 1. Python的地…

快速上手打通java中的IO流

目录 IO原理 流的分类 节点流和处理流 IO 流体系 InputStream & Reader InputStream Reader OutputStream & Writer OutputStream Writer 节点流(或文件流) 读取文件 写入文件 注意点 缓冲流 转换流 标准输入、输出流 打印流 数据流 对象流 O…

chatgpt赋能Python-python_for_局部变量

Python局部变量详解 Python是一种动态语言,其中一个特点是变量声明和赋值同时进行,因此变量类型在声明时可以不指定,而是在赋值时根据数据类型来推断。Python中的变量分为全局变量和局部变量。在本文中,我们将讨论Python中的局部…

【PCB专题】案例:使用SI9000阻抗计算线宽线距为PCB Layout提供参考

在实际工作中,我们会使用到高速信号(如USB2.0、MIPI、HDMI、以太网等)或射频信号。那么在PCB中这些信号都需要做阻抗匹配,防止信号产生反射而损耗和生成噪声。 阻抗对信号质量的影响很明显,我们可以利用眼图来分析信号质量。 下图所示为TI都是在90欧姆差分阻抗源经过了90欧…

论文改进想法

论文改进想法 当 z p r e v z_{prev} zprev​(上一轮本地训练好的发往server的模型得到的表征)与 z z z(这轮正在被更新的本地模型得到的表征)相似或是差别不大时,我们将 l c o n \mathcal{l}_{con} lcon​定为0&…

Go设计模式--中介者,最后的模式!

大家好,这里是每周都在陪你一起进步的网管~!今天继续学习设计模式,也是我们要学习的最后一个设计模式—中介者模式,对这个模式有一点了解后会觉得它跟我们已经学过的观察者模式挺像,但是两者还是有些区别的…

chatgpt赋能Python-python_entry如何清空

Python Entry如何清空 Python是一种高级编程语言,因其简单易学、代码优美而被广泛应用于数据科学、机器学习、Web开发等领域。在Python中,Entry是常用的GUI元素之一,用于接受用户输入。但有时候需要清空Entry中的文本,本文将介绍…

python中字符串的类型转换

一、使用eval----含有{}字符串的转换为list、tuple、dict- eval():将字符串str当成有效的表达式来求值并返回计算结果 (1)字符串转换为列表 (2)字符串转换为元组 a " ([1,3],[1,2],[1,1])…

STM8 调试红外遥控器HS003B

背景 项目中使用STM8S003F3P6,控制红外遥控器HS003B,调试过程也遇到了很多问题,这里做个记录。 问题1:长按按键问题,如果长按遥控的某个按键,应该是一直能响应该按键值(需要对接收状态、接收信…

工作模式(3)

8bit PWM PWM模式开启时,计数器与比较寄存器连续比较,当计数值小于比较寄存器,输出为低,当计数值大于等于比较寄存器,输出为高,并且保持到计数值等于重载寄存器,然后计数器回到0开始重新计数&am…