← 中文资源

简体中文版本

HTTP 429 — 重试风暴让限流雪上加霜

在不放大负载的前提下处理 429:尊重 Retry-After、加抖动的指数退避、去重调用方,并在触限之前主动降载。

阅读英文原文

症状

  错误率图上的锯齿就是签名:错误攀升、恢复开始、随后一波同步重试再次打爆。被无视的契约就在响应头里:Retry-After: 30 与 X-RateLimit-Remaining: 0。接着量化放大:每请求尝试次数分布拖着重尾(10 次以上)说明没有预算;多层重试栈(网关 × 服务 × SDK)把一个逻辑请求放大成十几个上游调用,全共享同一固定退避、也就共享同一个醒来时刻——同步波由此成形。

诊断思路

  先读响应头里的 Retry-After 与限流头,那是服务器声明的安全重入时刻;再统计每请求的尝试次数分布,重尾即无预算;最后对齐各层重试策略,多层独立重试相乘是放大器,重试应收敛到最贴近故障的一层。客户端日志显示毫秒级重试而响应明确说等 30 秒,是最直接的违规证据。

根因

  同步重试把瞬时限流响应变成持续过载:限流触发时所有被拒请求共享同一恢复时序,同时重发再次触限,系统在过载与恢复之间循环而不收敛。

修复与验证

  以服务器提示为锚的全抖动指数退避:上限取 Retry-After 与 2^attempt 的较大者、延迟取 [0, cap) 随机值;每请求设尝试预算(如 4 次);只重试幂等操作或携带 Idempotency-Key——服务器可能已处理请求,盲目重试非幂等操作会双写;重试收敛到单一层级;客户端在触限前就排队或降载。验证时在上游劣化场景下重试波从指标上消失:上游请求率保持平坦、错误单调收敛、客户端时序日志显示遵守了 Retry-After。

代码示例

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Remaining: 0
async function callWithRetry(url, opts) {
  const maxAttempts = 4;
  let attempt = 0;
  while (true) {
    const res = await fetch(url, opts);
    if (res.status !== 429) return res;
    if (++attempt >= maxAttempts) throw new RateLimitError(attempt);

    const serverHint = Number(res.headers.get('retry-after')) || 0;
    const cap = Math.max(serverHint, 2 ** attempt);   // never below server hint
    const delay = Math.random() * cap;                 // full jitter
    await sleep(delay);
  }
}
retry policy: max 3, jittered, idempotent methods only (or Idempotency-Key header)