retry-storm-backoff · ZH · 2026-10-20

一次失败被放大成风暴:聚合后端上的重试退避该怎么做

本文解释在 LLM API 聚合站的架构下,客户端重试叠加平台侧重试如何导致计费翻倍和拥塞放大,并给出幂等键、带抖动的指数退避和按模型并发上限的具体写法,帮助开发者构建更健壮的上游调用逻辑。

问题背景:重试叠加的隐藏代价

在直接调用官方 API 时,超时重试通常只影响单个调用。但在聚合站场景下,调用链路变成:你的客户端 → 聚合站 → 上游官方 API。如果客户端和聚合站都实现了自动重试,一次失败可能触发多次重试,不仅放大延迟,还会产生非预期的计费。

更隐蔽的是,当多个客户端在同一时间段内对同一模型发起重试时,会形成重试风暴,导致聚合站和上游同时拥塞,正常请求也被拖慢。

为什么重试会翻倍计费

聚合站按成功完成的请求计费。当客户端因超时发起重试,而聚合站也因上游超时重试时,同一次逻辑请求可能被上游实际执行多次。如果请求本身不是幂等的(例如生成长文本、触发写操作),重复执行会导致资源浪费和重复计费。

即使聚合站不重试,客户端盲目重试也会让聚合站收到多个相同请求,每个都可能被计费。

方案一:使用幂等键避免重复执行

在客户端生成一个幂等键(Idempotency Key),随请求头发送。聚合站可以根据该键识别重复请求,直接返回首次结果,避免重复执行和计费。

示例(伪代码):

import uuid
import requests

idempotency_key = str(uuid.uuid4())
headers = {
    "Authorization": "Bearer YOUR_API_KEY",
    "Idempotency-Key": idempotency_key,
    "Content-Type": "application/json"
}
payload = {
    "model": "gpt-4",
    "messages": [{"role": "user", "content": "Hello"}]
}

response = requests.post("https://api.example.com/v1/chat/completions", json=payload, headers=headers)

注意:幂等键应由客户端在第一次请求前生成,并在重试时复用同一个键。聚合站需要支持该头部才能生效。

方案二:带抖动的指数退避

避免固定间隔重试,采用指数退避加随机抖动(jitter),分散重试时间,减少拥塞。

import random
import time

def retry_with_backoff(max_retries=3, base_delay=1.0):
    for attempt in range(max_retries):
        try:
            # 发起请求
            return make_request()
        except (TimeoutError, ConnectionError) as e:
            if attempt == max_retries - 1:
                raise
            # 指数退避 + 抖动
            delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
            time.sleep(delay)

关键点:

  • 只对可重试的错误(如超时、连接错误)重试,不要对 4xx 错误重试。
  • 设置最大重试次数,避免无限循环。
  • 抖动范围建议为 0 到基础延迟的 50%,避免所有客户端同步重试。

方案三:按模型并发上限

不同模型的吞吐能力和速率限制不同。在客户端为每个模型设置独立的并发上限,可以避免因单一模型过载导致整体重试。

from concurrent.futures import ThreadPoolExecutor

# 为每个模型维护独立的线程池
model_pools = {
    "gpt-4": ThreadPoolExecutor(max_workers=5),
    "claude-3": ThreadPoolExecutor(max_workers=3),
    "deepseek-chat": ThreadPoolExecutor(max_workers=10)
}

def call_model(model, payload):
    pool = model_pools.get(model)
    if not pool:
        raise ValueError(f"Unknown model: {model}")
    return pool.submit(make_request, payload)

并发上限应根据聚合站的实际承载能力和你的业务需求调整。建议从保守值开始,观察错误率和延迟后再逐步放宽。

综合建议

  • 优先在客户端实现幂等键 + 抖动退避,并确认聚合站是否支持幂等键。
  • 为每个模型设置独立的并发限制,避免重试连锁反应。
  • 监控重试率和延迟,及时调整退避参数和并发上限。
  • 避免在聚合站和客户端同时启用激进重试,建议客户端重试次数不超过 2-3 次。

通过以上方法,你可以显著降低重试带来的重复计费和拥塞风险,让聚合后端调用更加稳定高效。