实现指数退避、抖动与 DLQ
目标
实现带指数退避和抖动的重试,区分禁止重试的错误,并创建 DLQ 加载、重新处理脚本和重试策略表。
为什么重要
重试并非没有成本。网关重试 4 次 × 服务 A 重试 4 次 × 服务 B 重试 4 次,会让用户的一次点击向最终系统发出64 次请求。正在恢复的对方系统会被这轮轰炸再次击倒。此外,没有抖动的指数退避会让 1 万个客户端在完全相同的时刻重试,这就是 thundering herd。由于平时不会出现问题,往往直到真正的大规模故障发生时才首次发现。这个实习要求把重试当作需要设计的机制,而不是简单讨论“加了还是没加”。
步骤
- 启动不稳定 API。
python3 /opt/lab/fixtures/eai/rest/flaky_api.py 9300(后台运行)/flaky?key=<키>:相同键的前 3 次调用返回 503,从第 4 次起返回 200/bad:始终返回 400/dead:始终返回 503
- 调用一次
/flaky?key=t1,把结果保存到/root/r/first.txt。文件中必须有一行http_code=503。 - 创建
/root/r/retry.sh。接收两个参数(URL 최대시도수),以固定间隔重试,并在最后一行输出result=<ok|fail> attempts=<n>。成功时退出码为 0。 - 创建
/root/r/backoff.sh。接收两个参数(URL 최대시도수),按指数退避(1、2、4、8 秒,上限 30 秒)重试。- 每次尝试前输出一行
attempt=<n> sleep=<초>。 - 最后一行输出
result=<ok|fail> attempts=<n>。 - 环境变量为
DRY=1时,不实际等待,只输出计划。
- 每次尝试前输出一行
- 为
backoff.sh添加抖动。sleep值必须是**计算值的 50% 以上、100% 以下或等于 100%**的随机值,并且以DRY=1运行两次时,数值必须不同。 - 让
backoff.sh区分禁止重试的错误。收到 HTTP 400/401/403/404/409 时立即停止,并以attempts=1结束。(使用/bad确认。) - 创建
/root/r/senddlq.sh。接收两个参数(URL 메시지ID),重试超过最大次数时创建/root/r/dlq/<메시지ID>.json。JSON 必须包含msg_id,url,reason,attempts,first_failed_at五个键。(url是第 7 步重新处理时再次调用的目标。)使用/dead和消息 IDM-001运行并创建文件。(使用同一 ID 再次运行时覆盖同一文件。) - 创建
/root/r/dlq-replay.sh。接收一个参数(DLQ 目录),对其中每个.json尝试重新处理;失败时将该文件的attempts增加 1 后重新保存。最后一行输出replayed=<시도수> succeeded=<성공수>。目录不存在时以非 0 退出码结束。 - 创建
/root/r/policy.csv。第一行为error,retry,max_attempts,backoff,final。必须包含以下六种类型:connection-refused,timeout,http-500,http-400,http-401,http-429。retry为Y/N/조건부,final为DLQ/중단/통보之一。
参考
- 仅获取状态码:
curl -s -o /dev/null -w '%{http_code}' <URL> - bash 随机数:
$RANDOM(0~32767)。请注意范围转换。 - 创建 JSON:
jq -n --arg a "$A" '{msg_id:$a}'或直接使用 printf - 常见错误 1:重试循环中即使成功仍继续执行。成功时应立即 break。
- 常见错误 2:让抖动值大于计算值。超过上限会削弱退避的意义。
- 常见错误 3:使用时间戳作为 DLQ 文件名,导致每次重新运行都增加文件。 必须以消息 ID 为准,才能管理重新处理。
重现失败
调用一次 /flaky?key=t1,把结果保存到 /root/r/first.txt。文件中必须有一行 http_code=503。
创建重试机制之前,首先要观察失败是什么样子。请同时确认 HTTP 状态码和响应正文。
固定间隔重试
创建 /root/r/retry.sh。接收两个参数(URL 최대시도수),以固定间隔重试,并在最后一行输出 result=<ok|fail> attempts=<n>。成功时退出码为 0。
重试次数必须通过参数接收,脚本才能复用。成功时立即退出,并输出第几次尝试成功。
指数退避
创建 /root/r/backoff.sh。接收两个参数(URL 최대시도수),按指数退避(1、2、4、8 秒,上限 30 秒)重试。
- 每次尝试前输出一行
attempt=<n> sleep=<초>。 - 最后一行输出
result=<ok|fail> attempts=<n>。 - 环境变量为
DRY=1时,不实际等待,只输出计划。
实际等待会使评分变慢。请先输出计划的等待时间,并在试运行模式下不要真正休眠。
添加抖动
为 backoff.sh 添加抖动。sleep 值必须是**计算值的 50% 以上、100% 以下或等于 100%**的随机值,并且以 DRY=1 运行两次时,数值必须不同。
大量客户端遵循相同规则时会同时重试。请在计算值中加入随机因素,但不得超出规定范围。
区分禁止重试的错误
让 backoff.sh 区分禁止重试的错误。收到 HTTP 400/401/403/404/409 时立即停止,并以 attempts=1 结束。(使用 /bad 确认。)
格式错误发送 100 次也会失败 100 次。请在脚本中加入根据状态码判断是否重试的规则。
加载到 DLQ
创建 /root/r/senddlq.sh。接收两个参数(URL 메시지ID),重试超过最大次数时创建 /root/r/dlq/<메시지ID>.json。JSON 必须包含 msg_id, url, reason, attempts, first_failed_at 五个键。(url 是第 7 步重新处理时再次调用的目标。)使用 /dead 和消息 ID M-001 运行并创建文件。(使用同一 ID 再次运行时覆盖同一文件。)
如果 DLQ 中只保存原始消息,日后人工无法作出判断。必须同时保存原因、尝试次数和首次失败时间。
DLQ 重新处理脚本
创建 /root/r/dlq-replay.sh。接收一个参数(DLQ 目录),对其中每个 .json 尝试重新处理;失败时将该文件的 attempts 增加 1 后重新保存。最后一行输出 replayed=<시도수> succeeded=<성공수>。目录不存在时以非 0 退出码结束。
重新处理必须通过参数接收目标目录,才能保证安全。如果不保留重新处理历史,就无法知道已经尝试了多少次。
重试策略表
创建 /root/r/policy.csv。第一行为 error,retry,max_attempts,backoff,final。必须包含以下六种类型:connection-refused, timeout, http-500, http-400, http-401, http-429。retry 为 Y/N/조건부,final 为 DLQ/중단/통보 之一。
不同错误类型的重试判断和最大次数不同。尤其需要注意 429,因为它表示对方要求“放慢请求速度”。