实现指数退避与 DLQ
目标
实现重试的四个要素——指数退避、抖动、上限、放弃后的 DLQ——并仅凭 DLQ 中的信息查明原因,直至完成重新处理。
为什么重要
重试看似最简单,却是最常引发事故的代码。以固定间隔无限重试,会周期性地再次压垮正在恢复的服务器。只加入指数退避时,1 万个客户端仍会在同一时刻重试,形成冲击波。即使加入抖动,如果没有上限,第 10 次尝试也要等待 17 分钟。如果没有放弃条件,一条错误消息就会永远阻塞整个队列。这称为毒消息。DLQ 是隔离该消息、让其余消息继续流动的机制,同时也是待调查事项列表。因此,只把消息放进去毫无用处——必须同时记录原因、尝试次数和首次时间,才能修复原因并重新处理。
步骤
- 使用
/root/qr/worker.py创建一个消费q:tasks的工作进程。/opt/app/taskproc.py中的process(msg)在msg["kind"]为bad时抛出异常。 - 将失败消息的尝试次数加 1 后重新放入队列。
/root/qr/requeue.out中必须显示attempts=2。 - 在
/root/qr/schedule.txt中,以attempt=<n> wait=<초>格式写入 base 为 1 秒、尝试编号 0~5 的指数退避值,共 6 行。其值为 1、2、4、8、16、32。 - 在
/root/qr/jitter.txt中逐行写入 20 个尝试编号 3 的完全抖动等待时间。所有值必须在 0 到 8 之间,并且至少有 15 个不同值。 - 应用 30 秒的上限,在
/root/qr/capped.txt中写入尝试编号 0~8 的值。最后三个值必须都为 30。 - 将失败 5 次的消息发送到
q:tasks:dlq。q:tasks必须为空,DLQ 长度必须为 1。 - DLQ 消息 JSON 必须包含
payload、error、attempts、first_seen_at四个键,且attempts为 5。 /root/qr/replay.py从 DLQ 取出消息,将attempts重置为 0,再放入q:tasks。执行后 DLQ 必须为空,q:tasks中必须有 1 条消息。
参考
- 完全抖动:
wait = random.uniform(0, min(cap, base * 2 ** attempt)) - 应重试的错误:超时、503、429。不得重试:模式违规、不存在的引用。
- 常见错误 1:创建 DLQ 消费者进行自动重新处理——如果原因仍然存在,就会形成无限循环。
- 常见错误 2:没有对 DLQ 深度设置告警——DLQ 十分安静,往往几周后才会被发现。
创建队列消费工作进程
使用 /root/qr/worker.py 创建一个消费 q:tasks 的工作进程。/opt/app/taskproc.py 中的 process(msg) 在 msg["kind"] 为 bad 时抛出异常。
逐条取出消息并交给处理函数。请让处理函数能够故意失败。
将失败消息重新入队
将失败消息的尝试次数加 1 后重新放入队列。/root/qr/requeue.out 中必须显示 attempts=2。
将尝试次数放在消息中,下次消费时就可以接着计数。
计算指数退避计划
在 /root/qr/schedule.txt 中,以 base 1 秒、尝试编号 0~5 的指数退避值 attempt=<n> wait=<초> 格式写入 6 行。其值为 1、2、4、8、16、32。
以尝试编号为指数。先完成计算并保存到文件中确认,然后再放入代码。
应用完全抖动
在 /root/qr/jitter.txt 中逐行写入 20 个尝试编号 3 的完全抖动等待时间。所有值必须在 0 到 8 之间,并且至少有 15 个不同值。
它是从 0 到计算值之间的随机数。即使尝试编号相同,每次的值也必须不同。
设置等待时间上限
应用 30 秒的上限,在 /root/qr/capped.txt 中写入尝试编号 0~8 的值。最后三个值必须都为 30。
指数增长得很快。如果没有上限,第 10 次尝试要等待 17 分钟。
失败 5 次后发送到 DLQ
将失败 5 次的消息发送到 q:tasks:dlq。q:tasks 必须为空,DLQ 长度必须为 1。
有放弃条件才称得上重试。消息必须从原队列消失,并且只存在于 DLQ 中。
在 DLQ 消息中加入调查信息
DLQ 消息 JSON 必须包含 payload、error、attempts、first_seen_at 四个键,且 attempts 为 5。
必须有原始消息、失败原因、尝试次数和首次接收时间这四项,之后才能开展调查。
消除原因后重新处理
/root/qr/replay.py 从 DLQ 取出消息,将 attempts 重置为 0,再放入 q:tasks。执行后 DLQ 必须为空,q:tasks 中必须有 1 条消息。
从 DLQ 取出消息放回原队列,并重置尝试次数。必须由明确的操作来执行,而不是自动循环。