rate-limit-backoff-strategies · ZH · 2026-10-13

批处理被限流时的退避与排队策略:在不换模型的前提下把队列跑完

针对单 API key 执行长批处理任务时遇到的限流问题,介绍指数退避、抖动、队列分级和死信队列等策略,帮助在不更换模型的情况下稳定跑完队列。

限流是批处理的常态,不是意外

用单个 API key 跑长批处理(比如几千条文本分类或摘要)时,限流几乎一定会发生。与其把它当作需要立刻根除的错误,不如把它当作容量信号:服务端在告诉你「当前速率超过公平使用线」。

本文讨论的策略不涉及更换模型或申请提额,只聚焦于:如何设计重试与排队逻辑,让队列在限流下最终跑完。

识别限流与其他错误的区别

重试前先区分错误类型,否则会浪费重试次数:

  • 限流:通常返回 429 状态码,或响应体中包含 rate limit 相关字段。这类错误应退避重试。
  • 瞬时错误:5xx 或网络超时,可重试,但退避策略可以更激进。
  • 永久错误:4xx(除 429 外),如参数错误、认证失败。重试无意义,应直接记录并跳过。

把这三类分开处理,是队列健壮性的第一步。

指数退避与抖动

基础指数退避

每次重试的等待时间按倍数增长:

wait = base * (2 ** attempt)

base 可以取一个较小的值(如 0.5 秒),attempt 从 0 开始。这能避免在服务端已经过载时继续高频冲击。

为什么必须加抖动

如果多个 worker 或任务同时被限流,它们会按照相同的指数曲线在同一时刻重试,形成「重试风暴」。抖动就是在等待时间上加入随机性:

  • 全抖动:wait = random(0, base * 2**attempt)
  • 等抖动:wait = base * 2**attempt / 2 + random(0, base * 2**attempt / 2)

实践中等抖动或全抖动都能显著分散重试压力。

队列设计:分级与并发控制

固定并发 + 信号量

不要一次性把全部请求并发发出。用信号量控制同时在途的请求数,比如 5 或 10。限流往往就是因为瞬时并发过高。

优先级队列

如果批处理任务有轻重缓急,可以用两个队列:

  • 快队列:小批量、低延迟要求的任务。
  • 慢队列:大批量、可延迟的任务。

限流时优先保障快队列,慢队列自然降速。

令牌桶与自适应降速

在客户端维护一个简单的令牌桶:每秒生成固定数量的令牌,请求前先取令牌。当连续收到 429 时,动态降低令牌生成速率;当一段时间无 429 时,再缓慢恢复。

这比固定速率更贴近服务端的实际容量。

重试预算与死信队列

设置最大重试次数

每个请求应有最大重试次数(如 5 次)。超过后不再重试,避免无限循环。

死信队列

达到最大重试次数的请求,不应直接丢弃。把它们写入死信队列(一个独立文件或数据库表),记录:

  • 请求参数
  • 最后一次错误
  • 重试次数
  • 时间戳

批处理主体跑完后,可以人工检查死信队列,决定是调参重跑还是放弃。

监控与日志

没有监控,退避策略就是盲调。至少记录:

  • 每分钟成功请求数
  • 每分钟 429 次数
  • 当前并发数
  • 平均等待时间

这些指标能帮你判断:限流是偶发还是持续,退避是否有效,队列是否在前进。

与聚合 API 的配合

在使用聚合 API(一个 key 调多个模型)时,上述策略同样适用。聚合层通常有自己的限流逻辑,但客户端仍应做好退避与排队。

注意:不同模型可能有不同的限流阈值。如果批处理任务允许,可以把队列按模型拆分,分别控制并发,避免一个模型的限流影响另一个模型的任务。

小结

在不更换模型的前提下,让长批处理队列跑完的关键是:

  1. 区分限流、瞬时错误和永久错误。
  2. 用指数退避 + 抖动处理可重试错误。
  3. 用信号量、优先级队列和自适应降速控制请求节奏。
  4. 设置重试上限,并用死信队列兜底。
  5. 监控关键指标,持续调整。

这些模式不依赖特定供应商,是通用的工程实践。