指数退避与抖动:重试不该成为第二次故障

当很多客户端在同一时刻失败,又在同一时刻重新请求,重试可能把一次短暂故障放大。一个合理的策略,需要同时考虑错误类型、操作能否重复、等待节奏和总预算。

本篇目标: 写出有上限的退避规则,并知道什么情况下应该停止自动重试。

失败后分散重试,而不是一起冲回去
失败后分散重试,而不是一起冲回去放大图解 ↗

先判断这次失败是否值得重试

参数不合法、身份失效、权限不足等情况,通常需要修正请求或重新处理身份,重复发送原请求没有帮助。部分限流和临时服务错误可能值得重试,但仍要遵守服务端提示,以及操作本身的幂等条件。

如果请求可能已经产生结果,应先查明原结果,或复用稳定的幂等键。网络超时并不等于服务端没有执行。

给等待时间加上上限和抖动

下面是 full jitter 风格的演算函数:先计算本次等待上限,再从零到这个上限之间取一个随机值。它不会让所有客户端使用完全相同的等待时间。

function retryDelay(attempt, baseMs = 300, capMs = 4000) {
  const upper = Math.min(capMs, baseMs * 2 ** (attempt - 1));
  return Math.floor(Math.random() * upper);
}

这里约定 attempt 从 1 开始,函数只计算等待时间。最大尝试次数、总期限、取消信号和错误分类仍由外层负责。演示数字没有普遍最优含义。

总期限要包含等待

每次等待前检查剩余时间,如果等待加上一次有意义的尝试已经放不进去,就应结束。用户取消之后,也不能让已经排好的计时器继续启动新请求。

服务端返回 Retry-After 时,需解析它可能使用的秒数或日期形式,并与本地总预算协调;预算不足时应结束本次操作,而不是违背提示提前重试。

观察重试有没有真正改善结果

可以记录每次尝试的原因、等待时长和最终结果,但不要在日志中写入凭据。关注有多少请求靠重试成功、重试放大了多少请求量,以及最终耗时是否仍然合理。

用一批同时失败的模拟客户端比较固定间隔与抖动后的请求分布。目标是让压力更分散,同时让用户在确定的时间内得到一个可以理解的结果,而不是无限地看到“正在重试”。

见字如晤

图解