LLM 重试风暴:一次部署失误烧光整月服务器费用
工程师复盘一起因部署顺序错误与自动重试叠加引发的 LLM API 费用事故,单日账单超过整月服务器开销。
一位工程师近日复盘了一起发生在自家 SaaS 服务中的费用事故:某一天 LLM API 的开销几乎相当于整个月服务器费用之和,账单上出现了一座「富士山」式的尖峰。最初他怀疑是产品负责人(CFO 出身的非工程师)反复测试造成,但深入排查日志后才发现,真正的「纵火犯」是后台的任务队列与一段部署顺序颠倒的代码。
现场:一天顶一个月的账单
故事起点是一款由非工程师借助 Claude Code 快速搭建、内部已上线运行的 SaaS。工程师接手后逐项审查后端实现,发现某一天 LLM API 的费用曲线异常突兀,单日花费接近其余日总和的一半。「我盯着那张图,胃往下沉——单日 AI 用量比跑一整个月的服务器舰队还贵。」他回忆道。
排查:按下按钮的不是人
工程师的第一反应是「人肉压测」:查看当日提交记录,发现从早到晚有 20 多次围绕 AI 生成流程的改动,看似符合「人为反复触发」的特征。但当他核对应用侧日志(任务队列、数据库、请求流水)后,看到的画面完全不同:同一个重量级批量任务被机器完整重跑了 21 次。「人不会在一天里把同一个按钮按 21 次,按按钮的根本不是人。」
反直觉的核心:成功的请求被丢掉了
这起事故最反直觉的地方在于:所有 LLM 调用本身全部成功,返回了 200,费用正常计费。批量任务的执行链路大致如下:
- 向多个 LLM 发起大批量请求(费用发生在这里)
- 将返回结果写入数据库
问题出在第二步:代码假设某个新列已经存在,但生产环境的数据库尚未添加该列,于是写入时报 column does not exist,任务以 500 失败。任务队列识别到 500 后,自动把整个批量任务重新跑了一遍。由于任务不具备幂等性,每次重跑都从零开始,全部 LLM 调用再次计费。工程师用吃饭类比:吃完整顿饭、付完账,刚要道谢就摔倒失忆,回到座位再点一模一样的套餐——重演 21 次。
这就是所谓的「重试风暴(retry storm)」,但与常见的「反复失败」不同,这次是「反复成功后把结果扔掉」。
两个致命缺陷叠加
事故由两个工程隐患叠加而成:
- 部署顺序颠倒:代码先行部署,假设新列已存在;但新增列的数据库迁移并未应用到生产环境。代码尝试读取不存在的列,每次必败,且属于「确定性失败」——无论重试多少次都不会自愈。
- 任务队列的「善意」自动重试:托管型任务队列对 500 错误的默认行为是自动重跑。对瞬时网络抖动而言这是正确设计,但面对「列不存在」这种结构错误,再跑一万次也不会长出新列。
「确定性失败 × 自动重试 × 非幂等任务」三者同时满足时,费用会在无人察觉的情况下持续燃烧。当工程师把链路画给那位非工程师负责人时,对方一脸困惑:「成功了还计费,然后又把成功丢掉?」这正是整个事件中最难消化的一点。
经验与教训
工程师把这次事故的复盘结论总结为几条对所有 LLM 应用开发者都有价值的实践准则:
- 确定性失败不应被重试:模式不匹配、列不存在、4xx 类「请求方错误」等失败,无论重试多少次结果都一样,应当立即中止并设置重试上限。重试不是万能保险。
- 副作用越重,越要保证幂等:任何涉及计费 API、LLLM 调用等高成本副作用的批量任务,从设计第一天起就必须支持「跳过已处理部分」。缺乏幂等性的重跑不是「重做」,而是「重复计费」。
- 部署顺序应为「先 schema,后代码」:先把数据库变更应用到生产,再发布依赖该 schema 的代码。一旦颠倒,就会在生产环境批量制造确定性错误,为重试风暴提供燃料。
