API 速率限制解释:算法、标头和处理 429 错误

  1. 首页
  2. 博客
  3. 软件开发

软件开发

API 速率限制说明:算法、标头和处理 429 错误

速率限制可保护 API 免遭过载和滥用。使用我们运行的模拟了解主要算法的行为方式、良好的 429 响应是什么样的以及客户端应如何退出。

阅读约 8 分钟
API 速率限制解释:算法、标头和处理 429 错误

要点速览

  • 速率限制限制客户端在一段时间内可以发出的请求数量。
  • 令牌桶允许短突发,同时强制执行平均速率。
  • 一个简单的固定窗口可以让窗口边界处的限制加倍。
  • 返回带有 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 标头,说明要等待多长时间。

令牌桶算法是什么?

每个请求都使用来自存储桶的令牌,该令牌以稳定的速率重新填充到最大值。它允许短突发,同时强制执行平均速率。

速率限制和节流有什么区别?

速率限制通常会拒绝超过限制的请求。节流通常会减慢它们的速度或使它们排队。这些术语经常互换使用。

客户应如何处理速率限制?

尊重重试后,使用指数退避和抖动重试,限制重试次数,并在剩余请求不足时放慢速度。

继续学习

与Stack Quora合作

需要测试、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%覆盖率仍然会错过错误,以及合理的目标。

阅读文章 →
数据库与 SQL

SQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们

DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。

阅读文章 →
数据库与 SQL

SQL 视图解释:创建、使用和避免陷阱(附示例)

SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。

阅读文章 →
软件开发

TypeScript 接口与类型:真正的差异,使用编译器进行测试

TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。

阅读文章 →
软件开发

Python *args 和 **kwargs 解释:灵活的函数和清晰的示例

Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。

阅读文章 →
软件开发

Python Lambda 函数:语法、实际用途以及何时不使用它们

Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。

阅读文章 →
人工智能

什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环

什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。

阅读文章 →
DevOps

监控与可观察性:真正的区别,包括日志、指标和跟踪

监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。

阅读文章 →
DevOps

Docker 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 循环更合适。

阅读文章 →
数据库与 SQL

SQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)

通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。

阅读文章 →