速率限制是控制发送到资源的流量量的概念。你如何实现这种控制?通过一个速率限制器 – 一个让你控制网络流量速率的组件,以保护API服务器或其他应用程序。
以下是速率限制器在系统环境中的概览。
例如,如果客户端向服务器发送3个请求,但服务器只能处理2个请求,你可以使用速率限制器限制额外请求到达服务器。
速率限制的必要性
在你的系统中应用速率限制的一些重要原因如下:
- 防止恶意主体滥用系统,试图通过异常大量的请求压垮你的服务器。
- 只允许服务器在特定时间点处理得了的可处理流量。
- 限制外部系统对某些昂贵资源的消耗。
- 防止级联故障,当顶级系统受到大量请求干扰时对下层系统的影响。
速率限制的关键概念
每个速率限制设置都共享三个关键概念。无论你采用何种类型的速率限制算法,这三个概念都会发挥作用。
以下是对每个概念的快速介绍。
- 限制 – 它定义了系统在给定时间范围内允许的最大请求数量。例如,Twitter(现在X)最近将未经验证的用户的查看推文限制为每天只能查看600条。
- 窗口 – 这是限制的时间周期。它可以是秒、分钟、甚至天数。
- 标识符 – 它是一个唯一的属性,可让你区分每个请求的所有者。例如,像用户ID或IP地址这样的东西可以作为标识符。
设计一个带有速率限制的系统
速率限制器的基本原理非常简单。在高层次上,你计算特定用户、IP地址或地理位置发送的请求数量。如果计数超过允许的限制,你就拒绝请求。
然而,在幕后,设计速率限制器时还有一些要考虑的事项。例如:
- 计数器应该存储在哪里?
- 速率限制的规则如何?
- 如何响应拒绝的请求
- 如何确保规则的更改得到应用
- 如何确保速率限制不会降低应用程序的整体性能
为了平衡所有这些考虑因素,你需要多个相互配合的组件。以下是一个描绘这样一个系统的插图。
让我们了解这里发生了什么:
- 当请求到达API服务器时,首先经过速率限制器组件。速率限制器检查规则引擎中的规则。
- 然后速率限制器继续检查缓存中存储的速率限制数据。这些数据基本上告诉了特定用户或IP地址已经被处理的请求数量。使用缓存的原因是为了实现高吞吐量。
- 如果请求在可接受范围内,则速率限制器允许请求转发到API服务器。
- 如果请求超过限制,速率限制器拒绝请求并通知客户端或用户已被速率限制。通常的做法是返回HTTP状态码429(请求过多)。
可能的改进
现在,你可以进行一些改进:
- 首先,你可以不返回HTTP状态码429,而是悄悄地丢弃请求。这是一个有用的技巧,可以让攻击者误以为请求已被接受,即使速率限制器实际上已完全丢弃了请求。
- 其次,你还可以在规则引擎前面加一个缓存来提高性能。在规则更新时,可以有一个后台工作进程将缓存更新为最新的一套规则。
速率限制算法
虽然每个算法都需要详细讨论,但在这里我们将简要地介绍最流行的算法。
固定窗口计数器
这可能是最简单的速率限制算法之一。在这个算法中,我们将时间分成固定的时间窗口。对于每个窗口,我们维护一个计数器。每个传入的请求都会将计数器加一,当计数器达到最大限制时,后续的请求将被丢弃,直到开始一个新的时间窗口。
虽然这个算法简单易懂,但存在一个很大的问题。在时间窗口的边缘出现的流量尖峰可能会导致大量请求在短时间内通过,并使服务器不堪重负。
如果感兴趣的话,可以看看我关于固定窗口速率限制算法的基本实现的文章。
滑动窗口日志
滑动窗口日志解决了固定窗口算法的问题。它不再使用固定的时间窗口,而是采用滑动时间窗口。
对于每个请求,该算法在缓存中跟踪请求的时间戳。当有新的请求到达时,它删除所有早于当前时间窗口开始的过期时间戳。如果日志的大小在允许的限制内,则接受请求。否则,拒绝请求。
当然,这个算法的问题是会消耗大量的内存,因为你必须跟踪所有的时间戳。
滑动窗口计数器
滑动窗口计数器结合了固定窗口和滑动窗口的优点。
与存储时间戳不同,该算法对滑动窗口维护一个计数器。为了确定是否接受或丢弃新的请求,它基本上计算当前窗口中的请求总数,并将其加上前一个窗口中请求数的重叠百分比的近似值。前一个窗口中请求的近似值通过将前一个窗口中的总请求数乘以滑动窗口的重叠百分比来计算。
这个算法的优点是平滑流量峰值和内存效率。然而,它不计算准确的请求数,而是处理近似值。
令牌桶
令牌桶算法是一个非常流行的速率限制算法。
在这个算法中,我们创建一个具有预定义容量的令牌桶。令牌以预设速率放入桶中。一旦桶满了,就不再添加令牌。每当有新的请求到来时,它会消耗一个令牌。如果桶中没有可用的令牌,速率限制器将丢弃请求。
这种方法的好处是它允许短时间内通过突发的流量。
主要的挑战是调整桶的大小和令牌补充速率的组合。
漏桶
漏桶算法与令牌桶非常类似,唯一的区别是请求以固定速率处理。
它采用先进先出的方法进行实现。当请求到达时,系统检查队列是否已满。如果没有满,请求将被添加到队列中,否则将被丢弃。
队列中的请求以固定速率进行处理,以避免对服务器造成压力过大。
速率限制器的用例
虽然我们可能容易将速率限制器视为保护系统免受外部请求的工具,但它也可以在内部帮助你。
在结束这篇介绍文章之前,让我们看一下速率限制器的一些著名用例,包括外部和内部用例。
- 防止灾难性的 DDoS 攻击 — 通过识别试图超负荷攻击系统的恶意IP地址、用户或请求令牌来实现。根据算法丢弃请求。
- 优雅地处理大量用户的激增 — 并非所有的流量都是恶意的,有时你的网站可能会吸引大量合法的流量。然而,如果你没有处理增加的流量的能力,最好的做法是优雅地处理事情,而不是向用户显示失败页面。
- 多层定价 — 这是一个内部用例,你可能希望为不同层次的用户提供不同的使用级别。这是许多按需SaaS服务的典型需求。
- 不过度使用第三方系统 — 这是另一个内部用例,你可能希望限制对昂贵的第三方API(如深度学习模型)的调用次数。你可以使用速率限制来限制调用次数。
- 保护一个未受保护的系统 — 有时,我们可能希望谨慎对待一个未受保护的系统。例如,如果你打算从数据库中删除一百万条记录,你运行的操作可能会导致数据库崩溃。你可以利用速率限制算法来平均分布删除操作。
结论
速率限制是构建具有高可用性的系统的重要工具。在本文中,我们仅仅覆盖了速率限制的高层次视角以及为什么我们首先需要它。本文中提到的每个算法都有其自己的理论基础和实际考虑因素。
如果您有任何疑问或意见,请随时在下面的评论区提出。