指数退避重试 Exponential Backoff Retry
调用一个接口,第一次超时了,要不要再试一次?
如果只是短暂的 网络波动,再试一次可能就成功了。但如果服务已经过载,每个客户端都立刻重试,就会让它收到更多请求,恢复得更慢。
指数退避重试解决的是这个问题:失败后先等待;如果仍然失败,就逐步拉长等待时间,给服务恢复的机会。
每次乘 2,为什么叫“指数”?
最常见的等待序列是:
首次请求 → 失败
等待 1 秒 → 第 1 次重试 → 失败
等待 2 秒 → 第 2 次重试 → 失败
等待 4 秒 → 第 3 次重试 → 失败
等待 8 秒 → 第 4 次重试 → 成功,结束
“每次乘 2”描述的是相邻两次等待时间的关系;“指数增长”描述的是等待时间与重试次数的关系。它们说的是同一件事。
设第一次重试前的等待时间为 base_delay,第 n 次重试从 1 开始计数:
第 n 次重试前的等待时间 = base_delay × 2^(n - 1)
| 第 n 次重试 | 乘法展开 | 等待时间,初始值为 1 秒 |
|---|---|---|
| 1 | 1 | 1 秒 |
| 2 | 1 × 2 | 2 秒 |
| 3 | 1 × 2 × 2 | 4 秒 |
| 4 | 1 × 2 × 2 × 2 | 8 秒 |
| 5 | 1 × 2 × 2 × 2 × 2 | 16 秒 |
线性增长是 每次加固定值,例如 1、2、3、4、5;指数增长是每次乘固定倍数,例如 1、2、4、8、16。倍数不必是 2,只要大于 1,就可以形成指数增长的等待序列。
这里增长的是两次尝试之间的等待间隔。如果忽略请求本身耗时,上面几次请求发生在第 0、1、3、7、15 秒,而不是第 0、1、2、4、8 秒。
为什么不一直等 1 秒?
固定间隔重试没有根据连续失败调整频率。一个服务如果已经连续失败多次,客户端仍然每秒请求一次,就可能持续占用它的处理能力。
指数退避会让连续失败的客户端逐渐降低请求频率。不过它也有代价:服务已经恢复时,客户端可能还在等待,不能立即发现恢复。因此,初始间隔和最大间隔需要结合业务可以接受的延迟来设置。
实际算法通常给等待时间加一个上限:
等待上限 = min(base_delay × 2^(n - 1), max_delay)
例如初始为 1 秒、最大为 30 秒,等待序列就是:
1、2、4、8、16、30、30……
最大等待间隔只限制一次等待,不会自动限制重试次数或整个操作的耗时。
为什么还要加随机抖动 Jitter?
假设 1,000 个客户端同时请求失败,而且都使用相同的退避参数。它们会一起等待 1 秒,再一起请求;下一次一起等待 2 秒,再一起请求。
请求频率虽然下降了,但流量仍然集中在几个时间点。
一种常见做法叫 Full Jitter:先算出当前等待上限,再在 0 到这个上限之间随机选择实际等待时间。
等待上限 = min(base_delay × 2^(n - 1), max_delay)
实际等待 = random(0, 等待上限)
例如当前上限为 8 秒,不同客户端可能分别等待 0.7 秒、3.2 秒和 6.5 秒,让请求分散开来。AWS 的退避与抖动实验比较了这些策略对竞争请求的影响。
加入 Full Jitter 后,实际等待时间不一定逐次增长。指数增长的是随机区间的上限。 第三次可能抽到 3 秒,第四次也可能只抽到 1 秒。