跳到主要内容

你的 LLM 网关缺少的长尾容错重试策略

· 阅读需 14 分钟
Tian Pan
Software Engineer

查看你的网关重试配置。三次尝试。带有抖动的指数退避。在 5xx 错误和超时时重试。最大延迟限制在几秒钟。这看起来很合理,而且是某人两年前从微服务运行手册中复制过来的。但这正是你的 P99 是 P50 两倍、在服务商发生故障期间 Token 费用激增,以及相当一部分用户在默默流失前盯着 30 秒加载圈的最主要原因。

专为 50 毫秒 RPC 设计的重试策略在面对 8 秒的 LLM 调用时注定会失效。失败的形式不同,每次尝试的成本不同,用户感知到的时间维度也不同。默认设置并不安全,只是让人感到熟悉。大多数团队通过同样的方式发现这一点:在一次事后回顾中,网关日志显示响应成功,而客户的截图却显示 UI 已经卡死。

微服务的重试惯例基于三个假设,而这三个假设在 LLM 流量面前都是错误的。它假设重试是廉价的,因为原始调用很廉价;它假设超时是罕见的,因为调优良好的服务很少超时;它假设慢路径和失败路径是同一条路径,因为对于 50 毫秒的调用来说,它们几乎就是一回事。一旦你开始调用一个需要 10 秒钟流式传输 Token 的模型,这些假设就都不成立了——在这里,每次重试都会重放一个 10 万 Token 的提示词,而且超时是最常见的故障形式,而非例外情况。

这篇文章将深入探讨 LLM 网关真正需要的重试策略。好消息是:大部分构建块都是现成的。坏消息是:以前没有人针对这种工作负载将它们组合在一起,而你从服务网格(Service Mesh)中继承的默认设置正在悄悄地烧钱并消耗用户的信任。

微服务的重试默认设置在 LLM 面前注定失效

先从数学计算开始。一个典型的 LLM 调用 P50 大约在 8 秒左右,P95 则接近 18 秒。应用标准的“超时重试、三次尝试、指数退避”模式,并设置单次尝试 10 秒的超时时间,那么在回退机制生效之前,单个缓慢的请求现在可能跨越长达 30 秒。LiteLLM 的用户已经记录过这种场景:大家为了“韧性”配置的回退路径,实际上默默地给失败的长尾请求增加了 30 多秒的延迟。

更糟糕的是,重试并不会收敛。当一个服务商过载并返回 529 错误时,其他所有团队的重试循环都在同时向同一个已经饱和的端点发起冲击。最初的故障可能只是 30 秒的波动,而重试风暴将其延长到了 10 分钟。分布式系统中大约 40% 的级联故障都可以追溯到重试逻辑,而 LLM 工作负载处于这种分布中最糟糕的一端,因为每次重试都会重放一个包含数千个 Token 的请求,其成本与原始请求相同。

此外还有微服务从未考虑过的成本维度。一个失败的 REST 调用只花费你一次 TCP 握手。而一个在生成中途夭折的 LLM 调用会让你支付完整的输入 Token 费用,通常还有部分的输出 Token 费用,再加上一次全新的全价重试。已经有公开的 Bug 报告称,网关重启触发了 10 万 Token 上下文的完整重发,从而产生了令人吃惊的四位数隔夜账单。重试预算不再是“我愿意消耗多少 CPU”的问题,而是下个月发票上的一个账单项。

超时是最常见的故障形式,而非例外情况

从微服务重试到 LLM 重试的最大转变在于:超时不再是一个边缘案例。对于一个 50 毫秒的服务,1 秒钟超时是一个明确的“某些东西坏了”的信号。而对于 LLM 来说,30 秒超时可能是“模型思考时间比平时长”,这在负载较高、长上下文推理或服务商正在部署期间是家常便饭。

这意味着超时重试需要有自己的策略,并与错误重试区分开。将它们混为一谈会产生两种失败模式:

第一种是对真实的缓慢生成进行过度重试。一个原本在 25 秒内能完成的长推理链,在 10 秒时被杀掉,重试,在累计 20 秒时再次被杀掉,重试,在累计 30 秒时第三次被杀掉——三次重试,三份完整的 Token 账单,而用户什么也没得到。这就是用户在版本发布后抱怨“AI 功能变难用了”的意思,而那个版本唯一的改动就是缩短了超时时间。

第二种是对瞬时错误重试不足。Anthropic 基础设施产生的 529 过载在大多数情况下会在 10-30 秒内恢复。如果你的重试预算与超时共享,并且已经被一个正常完成的缓慢生成耗尽,那么真正的可恢复错误就无处可去,最终表现为用户可见的失败。

解决方法是在重试之前进行分类。建立一个小型的分类体系:模型过载错误(529, 503)、工具调用验证失败、Schema 验证失败、网络错误以及纯粹的超时。每一类都有自己的预算、自己的退避曲线和自己的上限。尤其是纯粹的超时,需要独立的预算,因为它们是正常的、预料之中的且高频发生的——请像对待缓存未命中(Cache Miss)一样对待它们,而不是像对待异常(Exception)那样。

对于慢尾部(Slow Tail),对冲请求优于重试

Google 的《The Tail at Scale》论文在 2013 年就提出了这一观点,并且它已成为任何具有高尾部变差(Tail Variance)系统的核心模式:与其在请求失败后进行重试,不如在请求耗时过长时发出一个副本请求,并采纳最先返回的结果。两者的区别至关重要。失败后重试(Retry-after-failure)是在等待一个确认的问题,然后让你再次等待第二次尝试。而对冲请求(Hedged Requests)将“缓慢”本身视为信号,并并行运行第二次尝试,因此用户可见的延迟是 min(primary, hedge),而不是 primary + hedge。

对于 LLM 流量,数学计算结果极度偏向于对冲。一个使用自适应对冲(即当首个请求超过其测量的 P90 时触发对冲)的团队记录显示,在非 LLM 工作负载下,他们将 P99 从 64 ms 降低到了 17 ms,而额外负载仅增加约 9%。对于 LLM,虽然绝对数值不同(你的 P90 基准是以秒计,而不是毫秒),但相对收益是相似的:最慢的 5–10% 的请求不再缓慢,你只是用一些 Token 换取了这种差异。

LLM 对冲中有一个特定的陷阱,这是微服务对冲库无法处理的。推理服务器通常在模型产生单个 Token 之前就发送 200 OK 响应头,然后以流式传输响应体。一个简单的对冲器如果根据接收响应头的时间来衡量延迟,会认为请求很快而从不触发对冲,即使首字生成时间(TTFT)已经长达 10 秒,用户还在盯着加载动画。你应该测量首个 Body 字节的延迟,而不是首个 Header 字节的延迟。仅此一点就能解决一类“仪表盘显示我们很快,但用户并不认同”的谜团。

另一项准则是对冲限流。将令牌桶限制在基础流量的 5–10%,可以防止最坏情况的发生——即真实的供应商故障,导致每个首选调用都很慢,每个对冲都会触发,你的网关会突然对已经降级的供应商增加一倍的负载,从而将 30 秒的波动变成 5 分钟的停机。当确实出现问题时,令牌桶会在几秒钟内耗尽,对冲停止,系统会正常失效而非灾难性崩溃。

当路径本身有问题时,故障转移路由优于原路径重试

在超时后,针对同一个区域端点重试同一个供应商,通常不是一个好的选择,不如将第二次尝试路由到其他地方。如果首选请求超时是因为该区域性能降级,那么原路径重试只会增加延迟而不会改变结果。如果首选请求超时是因为模型本身对该提示词处理缓慢,那么原路径重试也只会产生第二次同样的缓慢生成。

一贯表现更优的模式是对冲故障转移(Hedged Failover):当首选请求超过其 P95 时,针对不同的区域或完全不同的供应商发起第二次尝试,取最先返回的结果。虽然从纸面上看成本更高,因为你可能需要支付两次 LLM 调用,但对冲的边际成本与长尾延迟的节省以及避免由于故障导致的用户影响成本相比,简直微不足道。

LiteLLM 风格的“按顺序尝试供应商 A,然后 B,再 C”的模式有两个实现上的错误:

首先,顺序故障转移会累积延迟。3 个供应商 × 10 秒超时 = 用户需要等待 30 秒才能看到结果,即使供应商 B 本可以在 2 秒内响应。而并行的对冲故障转移会获取最快的可用回答,因此无论供应商 A 处于多慢的状态,用户都能看到供应商 B 的 2 秒响应。

其次,顺序故障转移通常会使用导致首选请求出错的相同提示词进行重试,这意味着相同的提示词注入或上下文长度问题会在备选请求中触发同样的失败。如果故障转移能够对错误进行分类并调整请求——例如,当失败原因是上下文窗口溢出时,切换到具有更长上下文的模型——这将优于仅仅更改目标 URL 的方案。

启用重试后的延迟视角是唯一重要的数字

一种微妙的评估准则失效困扰着几乎所有的 LLM 网关仪表盘。团队测量并报告模型延迟的 P50、P95 和 P99——指的是在重试、对冲或故障转移之前,底层供应商调用的延迟。他们优化这个数字,并将其发布在延迟 SLA 中。然而,面向用户的延迟是重试策略生效之后的延迟。一旦重试机制介入,这两个数字就会产生巨大分歧。

假设模型延迟的 P99 为 18 秒,设置了 10 秒超时、1 秒指数级退避和 3 次重试尝试。仪表盘显示的模型延迟 P99 是 18 秒。而用户体验到的 P99——即触发了三次超时的最坏情况——接近 33 秒,因为重试在它们取代的调用之上增加了自身的延迟。如果重试策略本身就是被调整的变量,那么隐藏重试效果的仪表盘就是在隐藏团队真正需要的信号。

你应该在用户可见的边界追踪延迟,包括启用重试的情况,并计入退避时间。然后,叠加第二张没有重试的模型延迟图表,以便你可以看到差距。这个差距就是你的重试策略的成本,由用户在等待时间中支付,由公司在 Token 中支付。当差距大且稳定时,说明重试策略正在发挥实际作用。当差距大且在增长时,说明重试策略正在掩盖底层服务的降级,这时你应该发出警报。

幂等性、预算上限以及你应该规划的故障模式

几个工程细节决定了一个重试策略是能够容忍长尾延迟,还是会在事故期间发生爆炸。

幂等 Key (Idempotency keys) 使得在 LLM 调用具有副作用(例如发送消息、扣款、通过工具调用写入数据库)时,投机性对冲 (speculative hedging) 变得安全。如果没有幂等 Key,对冲操作可能会产生两次副作用。有了它,第二次写入会检测到去重并执行空操作 (no-op)。这对于 Agent 工作流尤为重要,因为模型调用通常伴随着状态变更。

预算上限 (Budget caps) 可以防止失控的重试风暴。设置一个全局上限——例如每分钟用于重试的总 Token 数,或所有调用者每分钟的总重试次数——这个比例相对于基础流量应该很小,大约在 5–15% 之间。当超过上限时,对新的重试请求采取快速失败 (fail-fast) 策略,而不是让它们排队。不应允许单个出现故障的服务商区域耗尽你的整个 LLM 预算;上限机制会强制系统放弃那条错误的路径,让用户看到错误信息,而不是等待 90 秒。

浏览器标签页取消处理 (Tab-cancellation handling) 是大多数团队都会忽视的、不起眼的工作。浏览器标签页在 15 秒时放弃了。网关在 22 秒时进行第二次重试。服务商在 28 秒时成功返回。网关记录了 200 成功码,费用仪表盘显示了一次成功的计费请求,而用户却什么也没看到,因为他们在 13 秒前就关闭了标签页。解决方法是双向取消:当上游连接关闭时,将取消信号传递给正在进行的重试任务,停止为无人阅读的响应消耗 Token。

重试策略是延迟 SLA 的一部分

在 LLM 网关上运行微服务默认配置的团队,在不知不觉中做出了一个架构选择。他们选择交付一个未经衡量的 P99 指标、一个超出预算的 Token 账单,以及一种在仪表盘最关键的故障期间与实际数据偏离数十秒的客户体验。

将重试策略视为延迟 SLA 的核心组成部分才是明智之举。这意味着要在开启重试的情况下测量延迟,根据发票上的成本线来调整预算规模,区分超时与错误,针对长尾延迟进行对冲而不是等待其失败,并行执行故障转移而不是顺序执行,并限制风暴规模,使服务商的波动仅停留于波动。这些都不是什么离奇的想法。它们只是由于从服务网格继承了错误的默认配置,并应用到了服务网格从未设计过的负载上。

好消息是,这是技术栈中收益最高的领域之一。一个为期两周的重写网关重试层项目,可以将 P99 提升 40–60%,并将故障引发的成本飙升降低一个数量级。坏消息是,组织架构图中没有人对此负责,因为重试策略存在于网关中,网关由平台团队所有,延迟指标由 AI 团队负责,而成本指标则由 FinOps 团队管理。这就是元模式 (meta-pattern):收益最高的问题往往是那些没有直接责任人 (DRI) 的问题,而谁先定义了责任人,谁就赢了。

References:Let's stay in touch and Follow me for more thoughts and updates