bigywater.tech
爬虫工程 · 30

重试与指数退避:加抖动和不加抖动的重试风暴

“失败了就指数退避”是重试的标准答案,但如果所有客户端同时开始、同时失败,没有抖动的指数退避只是让它们每一轮同时回来一次。这篇用 200 个客户端和一个先故障后限流的本机服务器,把五种策略在服务器端打出的请求节奏量出来。

26
无抖动的指数退避下,12 次尝试用完仍然没成功的客户端数(完全抖动是 0)
200
无抖动的指数退避和固定延迟下,任意 0.1 秒内服务器收到的请求数——200 个客户端同步
2.9 秒
去相关抖动让全部 200 个客户端成功所用的时间(无抖动指数退避 31.6 秒)

五种策略的节奏

实测 · 本机执行输出
  策略                       服务器收到请求     任意 0.1 秒内最多       任意 1 秒内最多      全部成功于      放弃的客户端    最多尝试
  立刻重试                    2400 个           994 个          2400 个          —         200       0
  固定延迟 0.5 秒              1260 个           200 个           400 个      4.8 秒           0      10
  指数退避(无抖动)               1766 个           200 个           800 个     31.6 秒          26      12
  指数退避 + 完全抖动             1167 个           411 个           980 个      5.3 秒           0       8
  去相关抖动                    918 个           200 个           727 个      2.9 秒           0       8

立刻重试:12 次机会在 1 秒的故障期内就用光,服务器总共收到 2400 个请求,任意 0.1 秒内最多 994 个,200 个客户端全部放弃。重试而没有间隔,等于对已经出问题的服务器加压。

固定间隔与无抖动的指数退避:固定 0.5 秒时 200 个客户端每 0.5 秒同时回来一次;服务器每轮只能处理 20 个。无抖动的指数退避看起来更“聪明”,但所有客户端同步,每一轮仍是 200 个同时到达,间隔越来越长而一轮只成功 20 个,最后 26 个客户端用完 12 次也没成功,总共 31.6 秒。

加抖动:完全抖动(在 0 到退避上限之间随机)5.3 秒全部成功;去相关抖动(下一次等待取决于上一次)2.9 秒、只用 918 个请求。开头的 0.1 秒内仍有 200–400 个请求,因为所有客户端是同时开始的——抖动解决的是后续重试的同步,不能消除一开始的集中。

换随机种子,以及结果的波动

实测 · 本机执行输出
  指数退避(无抖动):服务器收到请求 [1570, 1438, 1840, 1840];任意 0.1 秒内最多 [200, 200, 200, 200];全部成功于 [31.6, 26.5, 31.6, 31.6] 秒
  指数退避 + 完全抖动:服务器收到请求 [1165, 1194, 1168, 1184];任意 0.1 秒内最多 [339, 390, 387, 388];全部成功于 [3.8, 4.6, 3.5, 4.1] 秒

无抖动的指数退避不依赖随机数,4 个种子下任意 0.1 秒内都是正好 200 个(客户端同步),完成时间 26.5–31.6 秒;完全抖动 3.8–4.6 秒。服务器“同时处理超过 20 个就拒绝”依赖线程调度,所以每次运行细节不同:我把整个脚本跑了两遍,固定间隔 5.8 秒与 4.8 秒,完全抖动 5.6 秒与 5.3 秒,去相关抖动 3.6 秒与 2.9 秒,无抖动指数退避放弃的客户端 20 个与 26 个;文中取第二遍。不变的结论是:无抖动同步重试最慢,抖动最快。

分步演示

重试与退避
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。服务器:启动后前 1.0 秒对所有请求回 503,之后若同时在处理的请求数超过 20 就立刻回 503,否则处理 0.05 秒后回 200;200 个 aiohttp 客户端同时开始、各自重试至多 12 次;退避基数 0.1 秒、上限 5 秒;抖动用固定种子的随机数;服务器收到的请求数、0.1 秒窗口内的最大值由服务器记录的时间戳统计;文中的数字来自最后一次运行(整个脚本跑了两遍,第一遍的数字在上面的说明里标出)
    • 没有验证:真实服务器对 429、Retry-After 的处理(下一篇讲);重试预算(限制总重试比例)与断路器;不同故障形态(慢而不是错)下的表现;客户端数量更大(几千)时的节奏;非幂等请求重试的风险(再下一篇)
    京ICP备2026067502号-1京公网安备11010502063487号