API 重试逻辑与指数退避:真正有效的模式

讲解 API 重试、指数退避、抖动、Retry-After、幂等性、重试预算和熔断器,给出可用于生产的实现思路。

用 Apifox,节省研发团队的每一分钟

API 重试逻辑与指数退避:真正有效的模式

免费使用 Apifox

相关推荐

最新文章

API

一体化协作平台

API 设计

API 文档

API 调试

自动化测试

API Mock

API Hub

立即体验 Apifox
目录

你的支付 API 调用在凌晨 2 点失败了。是网络短暂抖动、速率限制,还是服务器宕机?答案决定了重试是能挽救这笔交易,还是会向客户重复扣款。

重试是分布式系统中最常见的弹性模式,也是最容易搞砸的模式。围绕 HTTP 调用包一层循环,看起来像防御式编程。做错了,它会把 30 秒的故障变成 30 分钟,因为成千上万的客户端会在同一时刻猛击一个苦苦支撑的服务器。做对了,重试会如此平滑地吸收瞬时故障,以至于用户完全察觉不到。

本指南涵盖生产系统所依赖的重试逻辑:哪些状态码应该重试、带 full jitter 的指数退避公式、Retry-After 请求头、幂等键、重试预算和熔断器。你还将看到如何通过使用 Apifox 模拟服务器来证明你的客户端行为正确,因为从未针对故障服务器测试过的重试模式只是猜测,不是设计。构建金融科技 API 重试逻辑的团队往往要以昂贵的方式学到这一点;你不必如此。

为什么朴素重试会让故障变得更糟

设想一个每秒处理 1,000 个请求的服务。它短暂卡顿了 5 秒。每个客户端都立即重试,而且每个客户端重试 3 次。原本 1,000 rps 的需求变成了直冲一个已经岌岌可危的服务器的 4,000 rps。服务器彻底崩溃。现在每个客户端又开始重试。

这种反馈循环有一个名字:重试风暴。服务器恢复时同步发生的踩踏称为惊群效应。Google 的 SRE 书在其关于处理级联故障的章节中指出了这种模式:没有退避的重试会恰恰在系统最无力承受时放大负载,而且即使原始故障已修复,也可能让服务持续宕机很久。

两种设计缺陷导致了大多数重试风暴:

  • 两次尝试之间没有延迟。立即重试会在最糟糕的时间窗口内放大负载。
  • 固定延迟。如果每个客户端都恰好等待一秒,它们会全都同时回来。服务器接收到的是一波波同步流量,而不是平滑上升的流量。

解决办法不是“永不重试”。而是有选择地重试,使用逐渐增加的随机延迟,并严格限制重试所额外增加的负载。

重试这些故障,绝不要重试那些

在进行任何退避计算之前,客户端需要一张决策表。重试一个服务器已经判定为无效并拒绝的请求会浪费容量并污染日志。重试瞬时故障才是重试的意义所在。

重试这些:

信号 含义
429 Too Many Requests 你触发了速率限制。退避后再以更慢的速度回来。
502 Bad Gateway 上游节点返回了无效内容。通常是瞬时故障。
503 Service Unavailable 服务器过载或正在重启。
504 Gateway Timeout 上游依赖项响应过慢。
连接重置、DNS 故障、socket 超时 请求可能根本没有到达。

一个 504 网关超时需要特别谨慎:即使网关放弃等待,源站也可能已经处理了你的请求。等讲到幂等性时,这一区别非常重要。

绝不要重试这些:

信号 含义
400 Bad Request 你的负载格式错误。下次也一样会出错。
401 Unauthorized 你的凭据错误或已过期。刷新令牌,不要循环重试。
403 Forbidden 你没有权限。重试不会授予你权限。
422 Unprocessable Entity 验证失败。修正数据,而不是调整时机。

规则是:当故障与服务器状态或网络有关时重试;当故障与请求本身有关时快速失败。429 介于两者之间:它可以重试,但同时也说明你的整体请求速率需要改进,这是一个限流问题,应在任何重试循环的上游解决。

指数退避公式,以及为什么抖动很重要

指数退避意味着每次重试的等待时间都比上一次更长,默认每次翻倍:

delay = base * 2^retry_count

基准值为 500 ms 时,等待时间就是 0.5s、1s、2s、4s、8s。再加一个上限(比如 30 秒),避免延迟增长到几分钟:

delay = min(cap, base * 2^retry_count)

这解决了“猛击服务器”的问题,却没有解决同步问题。如果 5,000 个客户端在同一时刻失败,普通指数退避会让这 5,000 个客户端都在 t=0.5s、接着 t=1s、再接着 t=2s 返回。仍然是一波波流量。仍然是惊群,只是更礼貌一些。

抖动通过随机化延迟来打破同步。AWS Architecture Blog 在其指数退避与抖动分析中模拟了相互竞争的客户端访问争用资源的情况。没有抖动的退避仍会产生聚集的调用尖峰。全抖动会在 0 到指数上限之间随机选择延迟,同时实现了最少的总调用次数和接近最短的完成时间:

delay = random_between(0, min(cap, base * 2^retry_count))

这个结果会让人意外。与整齐的翻倍计划相比,把随机化范围一直降到 0 似乎很草率。但让客户端均匀分布在整个时间窗口内,正是保持服务器负载平稳的关键。AWS 的分析还测试了“等抖动”(一半固定、一半随机)和“去相关抖动”;全抖动和去相关抖动表现更好,而全抖动是最容易正确编写的方案。除非测量结果表明另有选择,否则请将它作为默认的重试模式。

服务器何时告诉你就何时遵守 Retry-After

退避是客户端猜测需要等待多久。有时服务器会替你消除猜测。这个 Retry-After 请求头,针对 429 和 503 响应定义,携带的内容可以是秒数,也可以是 HTTP 日期:

HTTP/1.1 429 Too Many Requests
Retry-After: 12

当这个请求头存在时,它会覆盖你计算出的退避时间。服务器知道速率限制窗口何时重置,或维护何时结束;你的指数退避计划不知道。忽略 Retry-After 是服务提供商从限流升级到直接封禁的原因之一。解析它、遵守它,同时仍然应用你的上限和最大重试次数,这样恶意或有 bug 的Retry-After: 86400 不能让你的 worker 挂起一天。

幂等性:重试 POST 的前提

这里就是前面那个 504 的陷阱。GET、PUT 和 DELETE 按约定是幂等的:发送两次会让系统保持相同状态。POST 则不是。如果 POST /v1/payments 在服务器已经处理请求后超时,你的重试会创建第二笔支付。恭喜你,你已经构建了一台运行时间极佳的重复扣款机器。

解决办法是一个 幂等键:一个由客户端生成的唯一 ID(通常是 UUID),在每个逻辑操作中作为请求头发送。服务器会将这个 key 与第一次响应一起存储,并对任何重复请求重放已存储的响应。Stripe 的幂等请求正是这样工作的,大多数支付和资源配置 API 也都采用了这种方式。

两条规则可以让 key 发挥作用:

  • 同一个操作使用同一个 key。同一笔逻辑支付的每次重试都复用同一个 key。新的用户操作则使用全新的 key。
  • 在第一次发送之前生成 key,而不是在重试循环内部生成。否则每次重试看起来都像一次新操作,保护机制就会失效。

如果你调用的 API 不支持幂等键,就不要自动重试非幂等写入。直接呈现故障,让人工或对账作业来决定。

重试预算和熔断器:最后的防线

退避决定重试何时发生,但并不限制重试发生多少次。在长时间故障期间,即使使用了良好随机化的客户端也会积累重试负载,而多层重试还会相互放大:如果你的 API 网关重试 3 次,服务客户端也重试 3 次,一次用户点击就可能变成 9 个请求。

两种机制可以限制损害:

重试预算。不要采用“每个请求重试 3 次”,而应执行“重试最多只能额外增加 10% 的负载”,并在滑动窗口内进行测量。当预算用尽时,立即返回故障。无论同时有多少请求失败,这都能让重试放大效应保持在有界范围内。Linkerd 和 Envoy 都将其作为一等配置提供。

熔断器。跟踪每个下游服务的故障率。当故障率越过阈值时,熔断器打开:调用会立即失败,不接触网络。冷却一段时间后,少量探测请求会测试依赖项是否恢复,然后熔断器才会再次关闭。退避是在礼貌地放慢踩踏,而熔断器则将其取消。所有严肃的重试设计都会把两者配对,因为仅靠退避最终仍会发送每个请求。

一个可用于生产环境的 Python 示例

完整模式集中如下:可重试状态过滤、全抖动、Retry-After 支持、幂等键和严格的重试上限。

import random
import time
import uuid
import requests

RETRYABLE = {429, 502, 503, 504}
BASE = 0.5     # seconds
CAP = 30.0     # ceiling on any single delay
MAX_RETRIES = 5

def create_payment(payload):
    idempotency_key = str(uuid.uuid4())  # one key per logical payment
    headers = {"Idempotency-Key": idempotency_key}

    for retry_count in range(MAX_RETRIES + 1):
        try:
            resp = requests.post(
                "https://api.acmepay.com/v1/payments",
                json=payload, headers=headers, timeout=10,
            )
            if resp.status_code < 400:
                return resp.json()
            if resp.status_code not in RETRYABLE:
                resp.raise_for_status()  # 400/401/403/422: fail fast
            retry_after = resp.headers.get("Retry-After")
        except (requests.ConnectionError, requests.Timeout):
            retry_after = None  # network fault: fall through to backoff

        if retry_count == MAX_RETRIES:
            raise RuntimeError("payment failed after all retries")

        if retry_after and retry_after.isdigit():
            delay = min(CAP, float(retry_after))
        else:
            delay = random.uniform(0, min(CAP, BASE * 2 ** retry_count))
        time.sleep(delay)

值得注意的是:key 只在循环外生成一次。Retry-After 优先于计算出的退避时间,但仍然遵守上限。不可重试的状态码会立即抛出异常。如果你使用 JavaScript,axios-retry 库提供相同的形式,使用 retryConditionretryDelay 钩子;决策表保持不变。

如何在生产环境替你触发故障之前测试重试行为

大多数团队发布的重试代码,从未真正执行过一次故障分支。测试过的是成功路径;503 路径却要等到真实故障时才首次运行。借助两个 Apifox 的功能,你可以做得更好。

使用模拟服务器模拟故障。 Apifox 的智能 mock 功能让你可以定义类似 /v1/payments 的 endpoint 并编写其响应脚本。让它前两次调用返回 503,第三次返回 200,或者返回一个带有 Retry-After: 5 的 429,或者添加 15 秒延迟以触发客户端超时。将客户端指向 mock URL,观察重试循环处理每种场景,无需生产事故。

使用测试场景断言客户端行为。 Apifox 测试场景可以串联请求,并进行断言和时序检查。创建一个针对不稳定 mock 的场景,并断言调用最终成功、总耗时处于预期退避范围内,而且恰好创建了一个资源(证明幂等键发挥了作用)。将该场景接入 CI,这样你的重试逻辑会在每次提交时得到执行,而不是每次故障时才执行。

常见问题

我应该重试 429 吗?

应该,而且这是服务器通常会告诉你该怎么做的唯一状态码。读取 Retry-After 请求头,至少等待指定的时间;如果缺少该请求头,则使用带抖动的指数退避作为后备方案。还要把反复出现的 429 视为信号,通过客户端限流或缓存来修正请求速率,而不是把它当作正常运行。

什么是全抖动?

Full jitter 会在 0 到指数上限之间均匀随机选择每次重试的延迟:random(0, min(cap, base * 2^n))。它能防止多个客户端产生同步的重试波次。在 AWS 的模拟中,无论总调用次数还是完成时间,它都优于普通退避和等抖动,这也是它成为 AWS SDK 默认方案的原因。

重试 POST 请求安全吗?

只有当请求在实际操作中是幂等的才安全;对于 POST,这意味着发送一个服务器会据此进行去重的幂等键。没有幂等键时,超时后的重试可能会重复创建支付、订单或记录,因为服务器可能已经处理了你以为失败的请求。调用写入 API 的 AI agent 经常遇到这种情况;Agent 错误恢复模式与本文介绍的模式相同:带 key 的写入、有上限的重试和熔断器。

我应该重试多少次?

三到五次尝试几乎可以处理所有瞬时故障;超过这个次数后,成功率趋于平缓,而负载和延迟仍会持续上升。将单请求上限与全局重试预算(例如,重试最多只能增加 10% 的额外流量)结合起来,这样全面故障就无法使你的负载成倍增加。如果某个依赖项在最后一次重试后仍处于宕机状态,那是熔断器该介入的场景,而不是继续重试。

开发必备:API 全流程管理神器 Apifox

介绍完上文的内容,我想额外介绍一个对开发者同样重要的效率工具 —— Apifox。作为一个集 API 文档、调试、设计、测试、Mock、自动化测试于一体的工具,Apifox 是目前提升研发效率的首选。

如果你正在开发项目,不妨试试其极其友好的界面设计,它完全兼容 Postman 和 Swagger 数据格式,导入数据非常方便,,即使是新手也能很快上手,点击这里即可注册使用

Apifox

值得一提的是,除了个人和常规团队使用,针对有高安全合规要求、或需要在内网环境协作的企业,Apifox 还提供了深度定制的私有化部署方案

获取专属报价与部署方案

icon 详细的私有化部署系统架构与安全白皮书
icon 针对您公司规模的专属报价单
icon 免费的 1v1 专属产品演示 (Demo) 机会
获取部署方案
* 提交后,我们的客户经理将在 1 个工作日内与您联系
林俊锋 企业微信
@Apifox 专属顾问
扫码备注: 私有化 + 公司名