ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大模型请求重试预算与自适应熔断:防止网络微抖动引发供应商级联雪崩

大模型请求重试预算与自适应熔断:防止网络微抖动引发供应商级联雪崩

在分布式系统高可用工程中,重试(Retry)向来被视为应对网络偶发抖动最廉价、也最直接的“治愈灵药”。

然而,当你的系统接入的是单次响应长达数秒、底层算力极其紧绷的大语言模型(LLM)集群时,盲目的重试往往会从“治愈良方”直接蜕变成亲手点燃整片集群的“引爆装置”。

在去年双 11 的大模型网关压测中,我们曾亲身经历过一场惊心动魄的重试风暴:
后端承载的商用大模型供应商由于机房交换机突发单包丢包,接口错误率从 0.01% 轻微上浮至 5%,平均耗时由 800ms 增加到 2500ms。调用端的所有微服务框架由于默认开启了“失败重试 3 次”,在短短 3 秒钟之内,上游打向该供应商的请求量瞬间从 1.2 万 QPS 暴增至 4.5 万 QPS!

原本只是轻微感冒的后端供应商,瞬间被这突增近 4 倍的重试洪峰彻底打穿。供应商的所有 GPU 显存被并发长连接直接挤爆,HTTP 错误率瞬间飙升到 100%,引发了全网数十个核心业务线的集体瘫痪。

为什么原本为了提高可用性的重试机制,反而成了彻底摧毁系统的凶手?本文将深入拆解大模型调用中的“重试放大效应”,并给出基于 Google SRE 重试预算(Retry Budget)与自适应客户端熔断的生产级终极解法。


重试风暴的物理传导:指数放大的死亡螺旋

在大模型架构中,重试之所以比传统微服务更具破坏性,是由以下三个底层物理特性决定的:

[正常状态: 10,000 QPS 流量] ──> [后端推理集群负载 80%] (平稳运行) │ ▼ (供应商网络轻微抖动,出现 5% 延迟与超时) [上游 10,000 个请求中有 500 个超时] │ ▼ (开启 3 次盲目重试) [发起第一轮重试: +500 QPS] ──> 供应商负载达到 95% (排队延迟进一步恶化) │ ▼ (延迟恶化导致更多原本正常的请求也超时) [发起第二轮重试: +1,500 QPS] ──> 供应商负载突破 120% (触发 CUDA OOM) │ ▼ (恶性循环全面失控) [全网重试叠加洪峰: 40,000+ QPS] ──> [后端 100% 宕机,全链路雪崩]
  1. 计算资源的沉没与无法撤销性:
    传统 RPC 接口(如读取 Redis 或轻量查询)执行失败通常瞬间完成;但大模型推理是沉重的流水线。上游即使因为超时放弃了连接,后端 GPU 已经在跑的 Prefill 预填充计算并不会立即中断。盲目重试相当于在旧任务还没跑完时,又向 GPU 塞入了一个一模一样的新任务,直接将算力开销翻倍;
  2. 重试时钟的波峰同步共振:
    如果重试间隔是固定的(如每隔 1000ms 重试一次),成千上万个失败请求就会在下一个整数秒时刻同时向后端发起重试攻击,形成剧烈的流量脉冲波峰,直接把处于恢复边缘的后端再次按死在地上;
  3. 缺乏全局配额约束:
    每个调用线程只知道“我当前这一笔请求失败了,我有权再试两次”,没有任何人站在全局视角判断“现在整个系统是不是已经充斥着过量的重试流量”。

终极防护网:重试预算(Retry Budget)与自适应熔断架构

为了彻底封死重试雪崩的漏洞,系统必须构筑两道防御城墙:全局重试预算控制与自适应客户端熔断。

[调用方发起推理请求] │ ▼ [第一道防线: Google SRE 自适应熔断器] │ (根据全局请求成功率动态计算丢弃概率 P_drop) ├── [判定丢弃 (Drop)] ──> 直接抛出 CircuitBreakerException (快速失败) │ └── [判定放行] ──> 向大模型发起物理调用 │ ├── [调用成功] ──> 累加成功指标,正常返回 │ └── [调用失败 (5xx/超时)] │ ▼ [第二道防线: 重试预算门禁 (Retry Budget)] │ (检查过去 60s 内重试量是否 < 成功量 * 10%) ├── [预算超额] ──> 坚决禁止重试,直接返回失败 │ └── [预算充足] ──> 扣减预算,执行 Full Jitter 退避重试

1. 重试预算的核心数学模型

Google SRE 在《Site Reliability Engineering》中提出了优雅的配额思想:一个系统能够承受的额外重试量,必须严格与该系统当前处理成功请求的体量成正比。

设定滑动窗口内(如过去 60 秒):

  • 成功完成的请求总数为 $Requests_{success}$;
  • 允许的最大重试比例为 $Ratio$(推荐设置为10%,即 0.1);
  • 系统可用的重试预算池为:$Budget = Requests_{success} \times 0.1$。

当后端发生故障时,随着成功请求数急速下跌,可用重试预算也会呈断崖式枯竭。当下跌至临界点时,网关会强制将重试配额归零,彻底扼杀重试风暴在摇篮之中。

2. 带全抖动的指数退避(Exponential Backoff with Full Jitter)

即使有重试预算,重试的间隔也绝不能固定。必须引入全抖动(Full Jitter)随机化:

$$Sleep = \text{random}(0, ; \min(MaxSleep, ; BaseSleep \times 2^{attempt}))$$

通过在 0 到指数上限之间进行均匀随机分布,所有并发重试的流量波峰被彻底“削平”成均匀的平滑微流,防止对供应商造成二次冲击。


生产级重试预算与熔断器实现

以下是基于 Java 实现的无锁高并发重试预算器(Retry Budget Gate)核心工程代码:

package com.architect.resilience; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.LongAdder; /** * 生产级大模型全局重试预算与退避控制器 */ public class RetryBudgetController { // 滑动窗口成功请求计数器 private final LongAdder successRequestCounter = new LongAdder(); // 滑动窗口已消耗重试计数器 private final LongAdder executedRetryCounter = new LongAdder(); // 允许的重试比例上限 (如 10%) private final double maxRetryRatio; // 重试保底基础预算 (防止系统刚启动无流量时完全无法重试) private final long minBaseBudget; public RetryBudgetController(double maxRetryRatio, long minBaseBudget) { this.maxRetryRatio = maxRetryRatio; this.minBaseBudget = minBaseBudget; } /** * 记录一次成功的业务请求 */ public void recordSuccess() { successRequestCounter.increment(); } /** * 判断当前是否允许发起重试 */ public boolean canRetry() { long currentSuccess = successRequestCounter.sum(); long currentRetries = executedRetryCounter.sum(); // 动态计算可用重试预算上限 long allowedRetries = (long) (currentSuccess * maxRetryRatio) + minBaseBudget; if (currentRetries < allowedRetries) { executedRetryCounter.increment(); return true; // 预算充足,准许重试 } // 预算已耗尽,坚决阻断重试 return false; } /** * 计算带 Full Jitter 的退避休眠毫秒数 */ public long calculateBackoffWithJitter(int attempt, long baseSleepMs, long maxSleepMs) { // 计算当前指数退避上限: base * 2^attempt long ceiling = Math.min(maxSleepMs, baseSleepMs * (1L << Math.min(attempt, 10))); // 在 [0, ceiling] 之间均匀随机生成,彻底打散洪峰 return ThreadLocalRandom.current().nextLong(ceiling + 1); } /** * 由后台定时器驱动,每隔 60 秒平滑衰减滑动窗口统计值 */ public void resetSlidingWindow() { successRequestCounter.reset(); executedRetryCounter.reset(); } }

架构师的避坑红线:关于大模型重试的三条铁律

在大模型网关的容灾设计中,必须严格恪守以下三条防御性规范:

1. 业务逻辑错误(HTTP 4xx)严禁重试

重试只能用于处理瞬时网络错误(Transient Faults),如 TCP 连接重置、DNS 解析超时、HTTP 502/503/504。
如果模型提供商返回的是 HTTP 400(参数格式错误)、HTTP 401(鉴权失败)或者 HTTP 429(配额被限流),在没有更换 Token 或修正参数的情况下重复发起一万次重试,除了白白增加延迟和打满日志外没有任何意义。遇到 4xx 状态码必须立即直接终止并向上抛出业务异常。

2. 区分 Prefill 阶段与 Decode 阶段的失败

如果一个流式 SSE 请求已经成功返回了前 100 个 Token(说明模型已经成功度过了昂贵的 Prefill 计算阶段),随后中途发生断连:
绝对不要从头重新发起重试!因为从头重试意味着再次支付 100 个 Token 的计算开销。成熟网关应支持断点续传(Prefix Resume),将已生成的 Token 拼接回上下文,仅要求模型从断点处继续推理生成。

3. 请求超时配置严禁“一刀切”

大模型的推理耗时与 Prompt 的长度及期望生成的 Max Tokens 呈现高度正相关。
如果你把长文本分析任务和短文本问答任务的超时时间一刀切配置为 5 秒,那么所有长文本请求都会在刚刚计算到一半时被客户端单方面判定为超时,进而触发无效的重试死循环。必须根据入参的 Token 预估量,动态计算合理的客户端超时时间(Dynamic Deadline)。

高可用的真谛,不是在系统出现裂纹时盲目加大推力,而是在风暴来临之际懂得适时收手。用重试预算管住并发,用全抖动削平峰值,大模型链路才能在双 11 的狂风暴雨中平稳穿行。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进