无限重试造出来的东西
一句话总结
重试必须具备上限、指数退避、抖动,以及放弃后的去处。四项中缺少任何一项,重试都会成为故障放大器。
为什么需要了解这些
最糟糕的重试方式,是立即、以固定间隔、无限次重试。服务器因短暂过载而变慢时,所有客户端立刻再次请求,会让原本勉强支撑的服务器彻底崩溃。重试非但没有修复故障,反而使其恶化。
第一项改进是指数退避:把间隔依次增加为 1 秒、2 秒、4 秒、8 秒。失败持续时间越长,重试压力就呈指数下降,为服务器留下恢复空间。
第二项是抖动。只有指数退避还不够。服务器短暂宕机后,一万个客户端会同时感知失败,并遵循同一规则,在完全相同的时刻重试。服务器刚要恢复,一万个请求便同时涌入,让它再次崩溃;这股浪潮会在 2 秒、4 秒、8 秒后不断重复。完全抖动(full jitter)会在 0 到计算出的退避值之间随机等待,从而打破这种同步。
工作原理
还需要上限和放弃机制。应设置最大等待时间(例如 30 秒),避免间隔无限增长;达到固定次数后则停止重试。被放弃的消息会进入 DLQ(dead-letter queue,死信队列)。
DLQ 不是垃圾桶,而是待调查列表。因此,只放入消息本身毫无用处。还必须同时保存四项内容:原始消息、失败原因(异常消息或状态码)、尝试次数、首次接收时间。具备这些信息,才能回答“这条消息为何在这里”,并在修复原因后重新处理。
重新处理脚本也必须提前准备。凌晨三点 DLQ 开始堆积时,如果现场临时制作工具,很容易发生错误。
而且,不能对所有失败一概重试。只有临时错误值得重试。消息本身错误(违反模式、引用不存在)时,即使尝试 100 次也不会改变,最好第一次失败就直接送入 DLQ。若不作区分,一条错误消息就会阻塞整个队列,这类消息称为毒消息(poison message)。
生产现场中的常见情况
许多团队没有为 DLQ 设置告警。DLQ 很安静,往往几周后才被发现。“DLQ 深度 > 0”即使不需要立即呼叫值班人员,至少也应成为每天有人检查一次的指标。
还有一点:为 DLQ 本身创建消费者并自动重新处理是危险设计。如果原因没有消失,就会形成无限循环。更安全的方式是由人确认原因后,明确触发重新处理。
退避没有抖动时
失败任务会在同一时刻集中重试。下游短暂故障后刚刚恢复, 请求群便同时涌入,再次将其压垮(thundering herd)。
# ❌ 모두가 정확히 1s, 2s, 4s 뒤에 재시도한다
delay = base * (2 ** attempt)
# ✅ 흩어진다 — full jitter
delay = random.uniform(0, base * (2 ** attempt))
# ✅ 최소 대기는 보장하면서 흩기 — decorrelated jitter
delay = min(cap, random.uniform(base, prev * 3))
full jitter 最简单,实测分散效果也很好。还应设置上限(cap),
避免指数值无限增长。
如何确定重试次数
“三次”只是一种惯例。更好的方式是根据总等待时间反向计算。
하류가 보통 30초 안에 복구된다면 → 총 대기가 60초쯤 되게 잡는다
base=1s, 지수 2, 상한 20s → 1, 2, 4, 8, 16, 20 … 6번이면 51초
而且,重试也无济于事的失败应立即放弃。 400(错误请求)、401、403(权限)、404 无论发送多少次结果都相同。不作区分,注定失败的任务就会永久阻塞队列。
class Permanent(Exception): pass # 재시도하지 않는다
if 400 <= status < 500 and status not in (408, 429):
raise Permanent(f"고칠 수 없는 실패: {status}")
如何运维死信队列
耗尽重试次数的任务会被送入 DLQ。但如果送入 DLQ 就算结束,便无人会查看。 必须同时具备三项机制。
- 同时保存原因——最后一次错误、尝试次数、原始消息、首次失败时间。
- 按数量告警——平时为 0 的值开始增加,本身就是信号。
- 重新投入路径——修复原因后,必须有命令把消息从 DLQ 送回原队列。若只能手工复制,就不会有人去做。
# 예: 원인을 고친 뒤 되돌리기
labhub-cli dlq replay --queue orders --since 2026-09-06T12:00 --limit 500
重新投入时,重要的是不要一次全部放回。一次投入 5,000 条消息,会形成新的流量尖峰并再次压垮系统。应限制速率,让消息逐步流入。
下一次实验要做什么
为队列消费者添加指数退避和完全抖动,设置上限,失败 5 次后送入 DLQ;在 DLQ 消息中保存原因与尝试次数;最后消除失败原因,通过重新处理脚本完成恢复。