bigywater.tech
微服务 · 3

指数退避与抖动:四种抖动算法的重试时间分布

重试为什么要加抖动?AWS 提出的 Full、Equal、Decorrelated 三种抖动,等待时间的分布有什么不同?在容量有限的服务面前,200 个 client 一起重试会怎样?这篇用本机的小服务把它们量出来。

67.39 s
无抖动指数退避时,200 个 client 里最后一个成功的时间(固定间隔 3.02 s,Decorrelated 3.48 s)
2667 → 822
服务端收到的请求总数:固定间隔 对 Decorrelated Jitter
544
base 只有 20 ms 时,Full Jitter 最忙的 100 ms 窗口收到的请求数(base 100 ms 是 315)

四种算法的等待分布

实测 · 本机执行输出
  算法                  | n=0           | n=1           | n=2            | n=3            | n=4              | n=5              | n=6             
  --------------------+---------------+---------------+----------------+----------------+------------------+------------------+-----------------
  固定间隔                | 100 (100~100) | 100 (100~100) | 100 (100~100)  | 100 (100~100)  | 100 (100~100)    | 100 (100~100)    | 100 (100~100)   
  指数退避(无抖动)           | 100 (100~100) | 200 (200~200) | 400 (400~400)  | 800 (800~800)  | 1600 (1600~1600) | 3200 (3200~3200) | 3200 (3200~3200)
  Full Jitter         | 50 (5~95)     | 100 (10~190)  | 199 (20~380)   | 402 (40~761)   | 795 (78~1513)    | 1597 (154~3034)  | 1601 (164~3031) 
  Equal Jitter        | 75 (53~97)    | 150 (105~195) | 300 (210~390)  | 601 (420~781)  | 1197 (839~1556)  | 2399 (1677~3117) | 2400 (1682~3116)
  Decorrelated Jitter | 200 (110~290) | 350 (122~694) | 572 (133~1419) | 891 (143~2686) | 1191 (154~3200)  | 1426 (163~3200)  | 1592 (177~3200) 
  括号里是 5%~95% 分位。无抖动时每个 client 的等待完全相同(区间宽度为 0);Full Jitter 的下限是 0,所以有的 client 几乎立刻重试;Equal Jitter 的下限是一半;Decorrelated 依赖上一次的等待

无抖动时所有 client 的等待完全相同;Full 的下限接近 0,Equal 的下限是一半,Decorrelated 依赖上一次的等待。

同时被拒的 200 个 client

实测 · 本机执行输出
  算法                  | 轮数 | 服务端收到的请求总数(中位数,括号是范围) | 平均每个 client 试几次 | 最忙的 100 ms 窗口收到 | 完成时间中位数                | p99                    | 最后一个完成                
  --------------------+----+-----------------------+-----------------+-----------------+------------------------+------------------------+-----------------------
  固定间隔                | 1  | 2667                  | 13.3            | 200             | 1.29 s                 | 3.01 s                 | 3.02 s                
  指数退避(无抖动)           | 1  | 2314                  | 11.6            | 200             | 19.25 s                | 64.22 s                | 67.39 s               
  Full Jitter         | 3  | 1066 (1065~1069)      | 5.3 (5.3~5.3)   | 317 (305~320)   | 1.08 s (1.06 s~1.10 s) | 4.19 s (4.18 s~4.24 s) | 4.99 s (4.49 s~5.00 s)
  Equal Jitter        | 3  | 1034 (1024~1038)      | 5.2 (5.1~5.2)   | 228 (214~240)   | 1.30 s (1.30 s~1.32 s) | 5.38 s (5.35 s~5.42 s) | 5.67 s (5.50 s~5.81 s)
  Decorrelated Jitter | 3  | 822 (797~825)         | 4.1 (4.0~4.1)   | 200 (200~200)   | 1.06 s (1.05 s~1.08 s) | 2.99 s (2.69 s~3.37 s) | 3.48 s (3.13 s~4.69 s)
  服务端每个 100 ms 窗口收到的请求数(从 0 开始的前 20 个窗口,取第一轮):
  算法                  | 0   | 1   | 2   | 3   | 4   | 5   | 6   | 7   | 8   | 9   | 10  | 11  | 12  | 13 | 14 | 15 | 16 | 17 | 18 | 19
  --------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+----+----+----+----+----+----+---
  固定间隔                | 200 | 200 | 200 | 200 | 191 | 133 | 134 | 141 | 143 | 133 | 118 | 115 | 111 | 99 | 87 | 75 | 66 | 58 | 47 | 16
  指数退避(无抖动)           | 200 | 200 | 0   | 200 | 0   | 0   | 0   | 129 | 59  | 0   | 0   | 0   | 0   | 0  | 0  | 94 | 82 | 0  | 0  | 0 
  Full Jitter         | 317 | 203 | 125 | 68  | 69  | 54  | 37  | 26  | 24  | 20  | 22  | 19  | 11  | 6  | 7  | 3  | 5  | 5  | 3  | 8 
  Equal Jitter        | 228 | 173 | 156 | 43  | 33  | 94  | 60  | 5   | 1   | 9   | 37  | 40  | 31  | 28 | 2  | 0  | 0  | 0  | 2  | 7 
  Decorrelated Jitter | 200 | 46  | 105 | 85  | 52  | 53  | 42  | 35  | 34  | 30  | 26  | 20  | 16  | 15 | 8  | 10 | 7  | 7  | 5  | 3 

无抖动的指数退避让请求一波一波地同时到,容量有限时一波只能放过几个;加抖动后请求数降到不到固定间隔的一半。

base 与 cap

实测 · 本机执行输出
  参数                        | 服务端收到的请求总数 | 最忙的 100 ms 窗口收到 | 完成时间中位数 | 最后一个完成
  --------------------------+------------+-----------------+---------+-------
  base 20 ms / cap 3200 ms  | 1499       | 544             | 1.05 s  | 4.78 s
  base 100 ms / cap 3200 ms | 1074       | 315             | 1.09 s  | 4.56 s
  base 400 ms / cap 3200 ms | 729        | 234             | 1.05 s  | 5.17 s
  base 100 ms / cap 400 ms  | 1380       | 322             | 1.00 s  | 1.97 s
  base 100 ms / cap 800 ms  | 1117       | 334             | 1.02 s  | 2.50 s
  (连接类错误 0 次;不是超时,应为 0)

base 太小退避没拉开;cap 小则最后一个更早完成,代价是请求更多。

分步演示

指数退避与抖动
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇的服务是 Python 3.14 http.server(线程化)在本进程里起的,最多同时处理 4 个请求、每个 20 ms,忙时立刻回 503;200 个 client 是 200 个线程,先各自建好 keep-alive 连接再用屏障同时发出第一个请求(否则 200 个连接同时握手会超出内核的 accept 队列,出现与重试算法无关的 reset);第二节的抖动算法各跑 3 轮,固定间隔与无抖动指数退避各跑 1 轮;实验里连接类错误 0 次
    • 没有验证:更多 client(几千个)的情形——Python 线程撑不住;真实网络的延迟;服务端是排队而不是立刻拒绝时的表现;重试带 jitter 以后与重试预算、熔断一起用的效果(见后面的篇目);Equal Jitter 的 p99 比 Full 差(5.38 s 对 4.19 s)的原因没有单独分析
    京ICP备2026067502号-1京公网安备11010502063487号