2024 年 11 月黑五凌晨 1 点,华东一家美妆出海 3PL 的 OMS 开始批量报接口超时。值班工程师的第一反应是加重试——超时 5 秒就重发,最多 3 次。凌晨 3 点,整个链路雪崩了:WMS 收不到下单回执,TMS 查不到运单,仓内拣选任务状态 40 分钟没回传,最后只能手工对单。这不是个例,而是仓库系统对接里反复上演的经典剧本:一次接口超时,被"好心"的重试滚成了重试风暴。

现象:一次超时,是怎么滚成全链路雪崩的

复盘那晚的时间线,很有代表性。黑五零点大促,OMS 下单峰值一到,WMS 的接单接口响应变慢,开始出现 5 秒超时。WMS 端的重试逻辑是写死的:超时立即重发 3 次。问题是,下游的 TMS 查运单接口也被同样的峰值压着,WMS 的重试流量直接把 TMS 的 QPS 翻了几倍。TMS 扛不住,又去压承运商的 tracking API,触发了对方的限流。限流一开,回调排队,WMS 的拣选任务状态回传全部堵死——一个环节的超时,沿着调用链被每一层的重试逐级放大,最后演变成全链路不可用。

根据公开的系统设计资料,这个放大效应是可以算出来的。假设调用链有 4 跳(OMS→WMS→TMS→承运商 API),每一跳在超时后都重试 2 次,那么最坏情况下,最下游服务承受的请求量是正常值的 3×3×3=27 倍。正常 200 QPS 的接口,风暴时要面对 5400 QPS——而承运商 API 的限流阈值通常只有正常峰值的 2 到 3 倍。这是一个示例推演,但它解释了为什么那晚 TMS 先倒:它承受的不是"多一点"流量,而是数量级的碾压。

还有第二个案例,同样值得警惕。一家 3PL 的 TMS 调用承运商 tracking API,超时时间设了 30 秒,而且没有任何熔断。旺季一到,大量请求卡在 30 秒超时上,线程池被占满,WMS 的状态回传请求连线程都拿不到,排队 40 分钟。根因与其说是超时,不如说是"超时的请求一直占着线程不放"——长超时比短超时更伤下游,这个判断很多人是反过来的。

数据:重试放大的倍数,算出来有点吓人

把上面的推演再往前走一步。重试风暴的破坏力来自两个乘数效应的叠加:一是层数,每多一跳,放大倍数就多乘一个重试次数;二是时间,超时时间越长,每个" hanging 住"的请求占用资源越久,下游恢复的时间窗口就越短。根据 AWS 架构博客 2015 年《Exponential Backoff And Jitter》一文对重试机制的分析,固定节奏的重试还有一个隐性杀手:同步性——所有客户端在同一时刻超时、同一时刻重试,形成共振,下游刚要喘口气,下一波又到了。

所以那晚的雪崩不是"流量太大",而是"重试太齐"。OMS、WMS、TMS 用的都是同一套"超时 5 秒、立即重试 3 次"的模板配置,三层节奏完全同步,相当于三支队伍同时往一扇门里挤。这也是为什么很多团队的复盘结论是"加机器"却收效甚微:机器加的是容量,而风暴消耗的是"恢复的时间窗口",两者不在一个维度上。

归因:四种重试机制,为什么只有"退避+抖动+熔断"能活

把常见的重试策略摆在一起对比,差异一目了然:

策略 机制 放大效应 致命伤
立即重试 超时立刻重发,最多 N 次 N 倍,且与故障时长正相关 下游还没缓过来,第二波又到了
固定间隔重试 每隔 T 秒重发 N 倍,错峰有限 所有客户端同一节奏重试,形成共振
指数退避 等待 2 秒、4 秒、8 秒递增 随时间衰减,给下游喘息窗口 没解决"同时重试"的同步性问题
指数退避+抖动 退避基础上加随机抖动 衰减加去同步化 实现稍复杂,要写对随机种子
熔断+降级 失败率超阈值直接开路,半开试探恢复 趋近于零 需要预先设计好降级路径

指数退避的价值在于"越往后越慢",给下游留出恢复时间;抖动的价值在于打破同步性——重试风暴真正的燃料不是"重试"本身,而是"所有客户端在同一秒重试"。熔断器则是 Michael Nygard 在《Release It!》中提出的模式:失败率超过阈值就直接开路,不再发请求,隔一段时间半开试探,好了才放行。WMS 对接承运商 API 这类场景,熔断几乎是必选项:对方限流时你继续重试,只会把自己也拖进对方的黑名单。

还有一条容易被忽略的铁律:重试只配给幂等接口。下单、扣库存这类写操作,如果没有幂等键设计,重试一次就可能变成两次下单、两次扣减——这比超时贵得多。很多 WMS/OMS 对接文档里"重试 3 次"写得轻飘飘,却从没回答"重复下单了怎么办"。

实战:仓库系统对接的三条铁律

第一,超时时间分层。边缘查询接口 3 秒,核心下单接口 10 秒,批量对账这类重活不要走同步 HTTP,直接进异步队列。超时不是越长越宽容,越长越占资源。

第二,重试策略写进对接文档,而不是藏在代码默认值里。指数退避的基数、抖动范围、最大重试次数、熔断阈值,四个数要在联调时就和上下游对齐。那晚雪崩的直接原因,就是三层系统用了同一套模板配置,谁也没想过它们会共振。

第三,下游限流加舱壁隔离。TMS 的查运单接口和 WMS 的状态回传不要共用线程池,一个被打满不应该拖死另一个。调用承运商这类外部 API,客户端侧也要做限流——对方限流是保护它自己,你的限流是保护你自己。

一个反常识的判断值得单独拎出来:超时后"多等一会儿再试",很多时候不如"快速失败、让调用方排队"。线程池是按"并发占用数"死的,不是按"重试次数"死的。一个占 30 秒的超时请求,比"3 秒超时加 2 次重试"更伤下游。把超时设短、把重试次数设少、把失败快速暴露给调用方做排队,看似"体验差",实则是对整条链路最负责的做法。

从趋势看,重试正在从"调用方责任"变成"基础设施能力"。消息队列的死信队列、发件箱(outbox)模式、服务网格把重试策略下沉到基础设施层,业务代码越来越不需要手写重试逻辑。这是个好方向:风暴的根因之一就是每个团队各写各的重试,节奏永远对不齐。当重试逻辑从业务代码里彻底消失的那天,风暴才算真正被驯服。但在那之前,你的 WMS 对接文档里,"重试策略"这一节,值得现在就重写一遍。

作者:陈博士|供应链行业研究员