- 速率限制限制客户端在一段时间内可以发出的请求数量。
- 令牌桶允许短突发,同时强制执行平均速率。
- 一个简单的固定窗口可以让窗口边界处的限制加倍。
- 返回带有 Retry-After 标头的 429 Too Many Requests,并让客户端因抖动而退出。
一个有缺陷的脚本在紧密循环中调用您的 API。如果没有限制,它可能会减慢其他每个用户的服务速度,或者产生大量云费用。 API 速率限制 是防止单个客户端获取超过其公平份额的护栏。
为了展示常见算法的行为方式,我们编写了小型 Python 版本并为其提供模拟请求计时。下面的输出来自该运行。它们是教学模型,而不是生产代码,真正的网关具有更多功能。
什么是速率限制?
速率限制对每个客户端的请求进行计数,并拒绝或延迟超过商定限制(例如每分钟 100 个请求)的请求。一个相关的想法,节流,通常意味着减慢请求而不是拒绝请求。 配额通常是较长时期内的较大限额,例如定价计划每月 10,000 次通话。
为什么 API 需要它
- 稳定性:流量峰值不会压垮数据库或下游服务。
- 公平性: 一个重度客户不能让其他客户挨饿。
- 安全性:限制减慢暴力登录尝试和抓取的速度。
- 成本控制:昂贵的操作,例如AI模型调用,保持在预算范围内。
在不限制资源消耗的情况下,速率限制是 OWASP API Security Top 10 中的核心控制。我们的 使用 Postman 进行 API 测试 指南展示了如何发送检查所需的重复请求。
令牌桶
想象一个最多可容纳 5 个令牌的桶。每个请求都需要一个令牌。令牌以每秒 1 个的速度重新填充,并且桶中的令牌永远不会超过 5 个。如果桶为空,则请求将被拒绝。
class TokenBucket:
def __init__(self, capacity, refill_per_sec):
self.capacity = capacity
self.rate = refill_per_sec
self.tokens = capacity
self.last = 0.0
def allow(self, now):
elapsed = now - self.last
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.last = now
if self.tokens >= 1:
self.tokens -= 1
return True
return False
我们一次发送了 7 个请求,然后又发送了更多请求:
burst of 7 at t=0: [True, True, True, True, True, False, False]
t=2.0: [True, True, False]
t=10: 5 allowed of 10
前 5 个爆发了,然后桶就空了。两秒后,2 个令牌已重新填充,因此又通过了 2 个请求。长时间停顿后,桶又满了,但上限为 5。这种先突发后稳定的行为适合真实流量,这就是令牌桶在 API 网关中常见的原因。 漏桶 是一个近亲,它以固定速率处理请求,平滑输出而不是允许突发。
固定窗与推拉窗的比较
固定窗口计数器是最简单的方法:每分钟允许 10 个请求,并在分钟发生变化时重置计数器。它在边界处有一个已知的缺陷。我们在 59.9 秒时发送了 10 个请求,在 60.0 秒时又发送了 10 个请求,每 60 秒窗口限制为 10 个:
fixed window allowed around boundary: 20 of 20
sliding log allowed around boundary: 10 of 20
固定窗口在十分之一秒内允许所有 20 个请求,是预期速率的两倍,因为它们落入两个不同的窗口。 滑动日志会记住最近每个请求的时间并计算过去 60 秒内的请求,因此它正确地只允许 10 个请求。
权衡是内存。滑动日志存储每个请求的时间戳。 滑动窗柜台是一种流行的中间立场。它通过混合当前和先前的窗口计数来估计速率,使用很少的内存且具有良好的准确性。
算法对比
| 算法 | 爆发 | 准确度 | 内存 |
|---|---|---|---|
| 固定窗 | 边界处高达 2 倍 | 低 | 很低 |
| 滑动原木 | 否 | 准确 | 高 |
| 推拉窗柜台 | 小号 | 好 | 低 |
| 令牌桶 | 允许达到容量 | 好 | 低 |
| 漏水桶 | 平滑了 | 好 | 低 |
限制什么
- API 密钥或用户 ID: 经过身份验证的 API 的最公平选项。
- IP 地址: 对于公共端点很有用,但许多用户可以共享一个 IP,攻击者可以轮换它们。
- 端点:昂贵的操作,例如搜索、导出或登录,应该受到更严格的限制。
- 计划等级:免费和付费客户通常会获得不同的限制。
登录端点需要特别小心。按帐户和 IP 进行限制,因此攻击者无法简单地将猜测传播到多个 IP 或多个帐户。
429 响应
当客户端超出限制时,返回 HTTP 429 Too Many Requests,而不是 500 或 403。包含告诉客户端发生了什么以及何时重试的标头。我们的模拟为没有留下任何请求的客户生成了这套:
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30
Retry-After: 30
Retry-After 是标准的 HTTP 标头。 RateLimit-* 标头来自 IETF 草案,许多 API 使用自己的 X-RateLimit-* 变体。无论您选择什么,都记录下来。有关状态代码的更多信息,请参阅我们的 HTTP 状态代码备忘单。
作为客户处理限制
收到 429 后立即重试会使情况变得更糟。如果数千个客户端同时执行此操作,它们就会以同步的方式访问服务器。解决方法是带有抖动的指数退避:每次失败后等待更长时间,并添加随机性。
def backoff(attempt, base=0.5, cap=30, rng=random.Random(4)):
return round(min(cap, base * 2 ** attempt) * rng.random(), 2)
[backoff(a) for a in range(6)]
# [0.12, 0.1, 0.79, 0.62, 0.53, 6.43]
这是“完全抖动”:在零和指数增长上限之间的随机等待。等待看起来不规律,这就是重点。客户端分散开来而不是一起重试。我们为随机生成器播种,以便可以重现输出。
客户端清单:尊重Retry-After当它存在时,限制重试次数,仅重试可以安全重复的请求,并观察剩余请求标头,以便您可以在达到限制之前放慢速度。
分布式限速
由于负载均衡器后面有多个 API 服务器,每个服务器都依靠自己,从而让客户端通过在它们之间分散请求来超出限制。团队通常将计数器保存在共享存储(例如 Redis)中,并使用原子操作或简短的脚本,因此两个服务器不能同时获取最后一个令牌。 Kong、NGINX、Envoy 和云 API 网关等网关提供此配置。我们没有为本文运行分布式设置。
测试速率限制
- 发送请求直至达到限制并确认它们成功。
- 再发送一封并确认 429 的标头正确。
- 等待重置时间并确认请求再次成功。
- 检查每个密钥的限制:一个客户端的使用不得阻止另一个客户端的使用。
- 测试双突发问题的窗口之间的边界。
- 使用负载工具(如负载测试与压力测试中所述)来检查高并发下的行为。
在单元测试中,注入当前时间,就像我们的模拟在 now 中所做的那样,因此测试永远不必休眠。
设计限制了用户可以忍受的范围
选择数字和选择算法一样重要。从真实流量开始:查看最重的合法客户每分钟发出的请求数,并将限制设置在该值之上。阻止正常使用的限制会产生支持请求,而不是安全。
- 在 API 文档中发布每个端点和每个计划的限制。
- 阻塞前发出警告。 显示剩余请求的标头可以让行为良好的客户端自行减慢速度。
- 提供繁重轮询的替代方案,例如 webhook、批量端点或更大的页面。
- 监控 429 速率。 突然上升可能意味着攻击、有问题的客户端版本或现在太严格的限制。
- 故意授予例外情况,例如对合作伙伴提出限制,记录在配置中而不是硬编码中。
还要决定如何处理刚刚超过限制的请求。一些 API 会短暂地对少量额外请求进行排队,而不是拒绝它们,从而消除短暂的峰值。其他人则优先考虑支付等重要端点,而不是分析等不太重要的端点,以便在系统面临压力时重要流量保持流动。
最后的话
良好的速率限制是 API 和客户端之间的契约。服务器选择一个公平的算法,通常是令牌桶或滑动窗口,并用 429 和 Retry-After 清楚地解释其限制。客户端尊重这些信号并通过抖动退出。从每个 API 密钥的限制开始,观察实际使用情况,然后仅在需要的地方收紧。
常见问题
什么是 API 速率限制?
它是对客户端在给定时间内可以向 API 发出的请求数量的限制,例如每分钟 100 个。超过限制的请求将被拒绝或延迟。
HTTP 429 是什么意思?
429 Too Many Requests 表示客户端超出了速率限制。响应通常包含一个 Retry-After 标头,说明要等待多长时间。
令牌桶算法是什么?
每个请求都使用来自存储桶的令牌,该令牌以稳定的速率重新填充到最大值。它允许短突发,同时强制执行平均速率。
速率限制和节流有什么区别?
速率限制通常会拒绝超过限制的请求。节流通常会减慢它们的速度或使它们排队。这些术语经常互换使用。
客户应如何处理速率限制?
尊重重试后,使用指数退避和抖动重试,限制重试次数,并在剩余请求不足时放慢速度。
继续学习
需要测试、WordPress 或 SEO 方面的帮助?
我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。
更多指南 测试、人工智能和现代开发
喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。
人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它
人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。
阅读文章 →DevOps环境变量解释:.env 文件、秘密和配置正确完成
使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。
阅读文章 →软件开发API 速率限制解释:算法、标头和处理 429 错误
API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。
阅读文章 →测试自动化Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误
使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。
阅读文章 →软件测试代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义
用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。
阅读文章 →数据库与 SQLSQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们
DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。
阅读文章 →数据库与 SQLSQL 视图解释:创建、使用和避免陷阱(附示例)
SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。
阅读文章 →软件开发TypeScript 接口与类型:真正的差异,使用编译器进行测试
TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。
阅读文章 →软件开发Python *args 和 **kwargs 解释:灵活的函数和清晰的示例
Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。
阅读文章 →软件开发Python Lambda 函数:语法、实际用途以及何时不使用它们
Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。
阅读文章 →人工智能什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环
什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。
阅读文章 →DevOps监控与可观察性:真正的区别,包括日志、指标和跟踪
监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。
阅读文章 →DevOpsDocker Volumes 与 Bind Mounts:以正确的方式保留容器数据
Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。
阅读文章 →测试自动化TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式
TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。
阅读文章 →软件开发Git merge 与 rebase 的区别:示例及如何选择
用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。
阅读文章 →软件开发Python 列表推导式:语法、示例与何时避免使用
Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。
阅读文章 →数据库与 SQLSQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)
通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。
阅读文章 →