bigywater.tech
Python · 107

重试机制:固定间隔、指数退避和抖动

服务出问题时,客户端会重试;重试策略选不好,反而会把恢复中的服务再次压垮。这篇不靠推公式,而是启动 100 个真实线程,让它们分别用固定间隔、指数退避、指数退避加抖动去重试同一个「先宕机、后限流」的服务,把每 50 ms 打出去多少请求真的数出来。

133 → 0
固定间隔与「指数退避+抖动」下,服务恢复后被限流拒绝的请求数
1636 ms
指数退避但没有抖动时的完成时间——比固定间隔还慢(固定 579 ms、加抖动 606 ms)
每格恰好 100
固定间隔重试时,每 50 ms 内的请求数:所有客户端步调完全一致

做的事:模拟一次「服务先挂、后限流」

三种重试间隔 def delays(strategy, rng, n): for k in range(n): if strategy == "fixed": yield 0.05 elif strategy == "exp": yield min(0.02 * 2 ** k, 0.32) elif strategy == "exp_jitter": yield rng.uniform(0, min(0.02 * 2 ** k, 0.32)) # 全抖动
实测 · 本机执行输出
  fixed          50.0    50.0    50.0    50.0    50.0    50.0    50.0    50.0  (ms)
  exp            20.0    40.0    80.0   160.0   320.0   320.0   320.0   320.0  (ms)
  exp_jitter      2.7    33.9    61.1    40.8   158.5   143.8   208.5   252.4  (ms)

固定间隔永远是 50 ms;指数退避每次翻倍直到 320 ms 封顶;加了全抖动(full jitter)之后,每次等待是 0 到指数上限之间的随机值,同一个客户端的每次间隔不同,不同客户端之间更不同。

服务端的模型:0 到 300 ms 完全不可用,之后每个 20 ms 的窗口只能处理 15 个请求,多的被拒绝。100 个线程同时开始。每种策略跑 3 次,取总请求数居中的那一次。

验证一:三种策略的结果

实测 · 本机执行输出
  fixed        总请求   833 | 宕机期间   600 | 被限流拒绝   133 | 成功 100 | 全部完成用时    579 ms | 任意 10 ms 内最多 100 个请求 (3 次取总请求数居中的一次)
  exp          总请求   674 | 宕机期间   400 | 被限流拒绝   174 | 成功 100 | 全部完成用时   1636 ms | 任意 10 ms 内最多 100 个请求 (3 次取总请求数居中的一次)
  exp_jitter   总请求   645 | 宕机期间   545 | 被限流拒绝     0 | 成功 100 | 全部完成用时    606 ms | 任意 10 ms 内最多 126 个请求 (3 次取总请求数居中的一次)

固定间隔:重试最多,833 个请求里 133 个被限流拒绝。指数退避(无抖动):请求总数少了,但被拒绝的更多(174),而且完成得最晚(1636 ms)。指数退避加抖动:0 次被拒绝,606 ms 完成。「指数退避」本身并不等于「更好」——没有抖动时,所有客户端的重试时刻依然完全同步。(表里「服务恢复后任意 10 ms 内最多」一列的数字包含了 t=0 时 100 个客户端同时开始的那一次齐发,所以不是判断重试风暴的好指标;请看下一节的分布。)

验证二:时间分布

每 50 ms 一格,前 6 格(0–300 ms)是宕机期
实测 · 本机执行输出
  fixed        [100, 100, 100, 100, 100, 100, 98, 49, 23, 40, 19, 4, 0, 0, 0, 0]
  exp          [200, 100, 0, 100, 0, 0, 100, 0, 0, 0, 0, 0, 10, 75, 0, 0]
  exp_jitter   [290, 77, 74, 45, 31, 28, 28, 25, 19, 12, 12, 3, 1, 0, 0, 0]

固定间隔:前 6 格每格整整 100 个请求——所有客户端步调一致。指数退避无抖动:格子里不是 0 就是 100 的整数倍([200, 100, 0, 100, 0, 0, 100, …]),请求集中在几个瞬间同时到达,像一次次脉冲。抖动之后是平滑衰减的曲线:[290, 77, 74, 45, 31, 28, …](第一格的 290 包含了 t=0 时 100 个客户端的第一次请求)。

验证三:只该重试暂时性错误

实测 · 本机执行输出
  Transient  函数被调用 4 次后抛出 Transient(retry_on=Transient)
  ValueError 函数被调用 1 次后抛出 ValueError(retry_on=Transient)

重试函数用 retry_on=Transient 限定要重试的异常。暂时性错误重试到上限(共 4 次调用);参数错误(ValueError)只调用 1 次就抛出——重试不会让错误的参数变对。另外,对有副作用的操作(比如「扣款」)重试还要保证幂等,这一点本篇没有验证。

分步演示:三种重试策略下,每 50 ms 打向服务的请求数

数字取自上面的真实运行
固定 / 指数 / 指数+抖动
宕机期(前 6 格)/ 关键格

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7,标准库 threading、random;时间用 time.perf_counter,线程调度存在毫秒级抖动,因此每种策略跑 3 次取居中结果,具体数字每次会略有不同
    • 没有验证:decorrelated jitter、equal jitter 等其他抖动变体、服务端有排队而非直接拒绝的情形、跨机器网络延迟、重试预算(限制总重试比例)
    京ICP备2026067502号-1京公网安备11010502063487号