速率限制是许多开发人员可能在生活中某些时候必须处理的机制。它可用于多种用途,例如共享对有限资源的访问权限或限制对API端点的请求数量,并以 429状态码进行 响应 。
在这里,我们将探索一些使用 Python 和 Redis的 速率限制算法,从朴素的方法开始,最后达到一种称为 通用单元速率算法 (GCRA)的高级方法。
在以下文章中,我们将使用 redis-py 与Redis(pip install redis)通信。我建议您 从GitHub克隆 我的存储库 ,以进行一些限速实验。
很多人学习python,不知道从何学起。
很多人学习python,掌握了基本语法过后,不知道在哪里寻找案例上手。
很多已经做案例的人,却不知道如何去学习更加高深的知识。
那么针对这三类人,我给大家提供一个好的学习平台,免费领取视频教程,电子书籍,以及课程的源代码!??¤
QQ群:1057034340
时间桶
对一个请求中的请求数量进行速率限制的第一种方法 period 是使用时间分组算法,其中为每个速率密钥(诸如用户名或IP地址这样的唯一值)存储一个计数器(最初设置为 limit)和到期时间(period)并根据后续请求递减。
使用Python和Redis,我们可以通过以下方式实现时间桶逻辑:
- 检查价目码是否 key 存在
- 如果 key 不存在,则将其初始化为 limit 值( Redis SETNX )和到期时间 period ( Redis EXPIRE )
- 在随后的每个请求中减小此值( Redis DECRBY )
- 仅当key 值降至零以下时,请求才受到限制
- 给定 period 密钥后将自动删除
from datetime <b>import</b> timedelta from redis <b>import</b> Redis def request_is_limited(r: Redis, key: str, limit: <b>int</b>, period: timedelta): <b>if</b> r.setnx(key, limit): r.expire(key, <b>int</b>(period.total_seconds())) bucket_val = r.get(key) <b>if</b> bucket_val and <b>int</b>(bucket_val) > 0: r.decrby(key, 1) <b>return</b> False <b>return</b> True
您可以在模拟请求的操作中看到此代码,其限制为 20 requests / 30 seconds (为了使内容清楚,我将功能包装在模块中,就像您在 存储库中 看到的那样 )。
<b>import</b> redis from datetime <b>import</b> timedelta from ratelimit <b>import</b> time_bucketed r = redis.Redis(host='localhost', port=6379, db=0) requests = 25 <b>for</b> i in range(requests): <b>if</b> time_bucketed.request_is_limited(r, 'admin', 20, timedelta(seconds=30)): print ('Request is limited') <b>else</b>: print ('Request is allowed')
只有前20个请求不会受到限制,您必须等待30秒才能被允许再次提出新请求。
这种方法的缺点是,它实际上限制了用户在一段时间内可以执行的请求数量,而不是速率。这意味着整个配额可以立即用尽,只有在该期限到期后才重置。
漏斗
有一种算法可以处理速率限制并产生非常平滑的速率限制效果: 漏斗式方法 。该算法的工作原理与实际的漏水桶类似:您可以将桶中的水(要求)倒入最大容量,而一定量的水以恒定的速率逸出(通常使用后台过程实现)。如果进入水桶的水量大于通过泄漏口流出的水量,则水桶开始装满水,当水桶装满水时,新的请求将受到限制。
我们可以跳过这一点,以获得更优雅的方法,该方法不需要单独的过程即可模拟泄漏: 通用信元速率算法 。
通用信元速率算法
通用信元速率算法(GCRA) 来自一种称为“通信”的通信技术 Asynchronous Transfer Mode (ATM) ,用于ATM网络的调度程序中,以延迟或丢弃超出速率限制的信元(固定大小的小数据包)。
GCRA的工作原理是跟踪每个请求上称为 理论到达时间 (TAT)的时间的剩余限制
tat = current_time + period
如果到达时间小于存储的TAT,则限制下一个请求。如果rate = 1 request / period 在period通常情况下,通常在现实世界中,每个请求都被分隔开 , 那么这很好 rate = limit / period。例如, rate = 10 requests / 60 seconds 允许用户在前6秒内发出10个请求,而 rate = 1 request / 6 seconds 用户必须在请求之间等待6秒。
为了能够在短时间和在 limit > 1期间限制请求,每个请求应当由period / limit分开,下一个理论到达时间(new_tatt)用与以前以不同的方式来计算。使用t表示请求到达的时间:
- new_tat = tat + period / limit 如果请求符合 (t <= tat)
- new_tat = t + period / limit 如果请求不符合 (t > tat)
因此:
<b>new</b>_tat = max(tat, t) + period / limit.
如果new_tat 超过当前时间+期间段, 则请求将受到限制 new_tat > t + period。因为 new_tat = tat + period / limit 所以:
tat + period / limit > t + period
这样,使用下面条件限制请求:
tat — t > period — period / limit
period — period / limit <-----------------------> --|----------------------------|---> t TAT
在这种情况下,不应更新TAT,因为有限的请求没有理论上的到达时间。
现在是时候发布最终代码了!
from datetime <b>import</b> timedelta from redis <b>import</b> Redis def request_is_limited(r: Redis, key: str, limit: <b>int</b>, period: timedelta): period_in_seconds = <b>int</b>(period.total_seconds()) t = r.time()[0] separation = round(period_in_seconds / limit) r.setnx(key, 0) tat = max(<b>int</b>(r.get(key)), t) <b>if</b> tat - t <= period_in_seconds - separation: <b>new</b>_tat = max(tat, t) + separation r.set(key, <b>new</b>_tat) <b>return</b> False <b>return</b> True
在此实现中,我们使用 Redis TIME, 因为GCRA依赖于时间,并且确保多个部署之间的当前时间保持一致(计算机之间的时钟漂移可能导致误报)至关重要。