跳转至

Fault Tolerance

容错使服务在副本、节点、网络或模型加载失败时保持可控降级和恢复。目标不是「永不失败」,而是在故障发生时让影响面可控、恢复过程自动、对用户可感知但可预期,并把故障与告警对应起来,避免同类问题反复发生。

快速开始

先配置基础防线:对每个副本做健康检查(存活与就绪分开)、为请求设置超时、对可重试错误做有限次重试、并部署多个副本。健康检查要能区分「进程活着但推理已不可用」的情况,例如显存耗尽或模型进程卡死。

存活与就绪探针的分工可以这样落地到 Kubernetes:存活探针只判断「进程是否还在」,就绪探针判断「是否已加载完权重、能接流量」:

livenessProbe:
  httpGet:
    path: /health
    port: 8000
  periodSeconds: 10
readinessProbe:
  httpGet:
    path: /ready
    port: 8000
  periodSeconds: 5

若就绪探针依赖模型已加载,新副本在预热完成前就不会被分配流量,避免「刚起来就再次被打挂」。

为请求设置幂等 ID 是重试安全的关键。若请求会触发有副作用的工具调用(写库、发消息、扣费),盲目重试会导致副作用重复执行;幂等 ID 让服务端能识别重复请求并返回上一次结果,而不是再次执行。

重试必须有限并配合退避:无限重试会放大故障,形成重试风暴。客户端应在收到可重试错误时使用指数退避,并对不可重试错误(如参数非法、模型不存在)直接失败,避免无意义地反复打满服务。指数退避通常写成:

\[ \text{delay}=\min\left(\text{cap},\ \text{base}\times 2^{\text{attempt}}\right)+\text{jitter} \]

其中 base 是初始退避时长,attempt 从 0 起每失败一次加 1,cap 是上限,jitter 是随机抖动。抖动让多个客户端不会在同一时刻同时重试,是防重试风暴的关键一环。

策略

区分瞬态故障与确定性错误是容错策略的前提。瞬态故障(网络抖动、连接重置、临时限流)值得重试;确定性错误(无效参数、权限不足、不支持的模型)重试也不会成功,应直接返回错误并告警,让调用方尽快修正。

不同故障需要不同处理。流式连接中断时,服务应停止生成并释放对应资源,客户端按可重试与否决定是否重发;GPU OOM 需要重启或降级该副本、必要时回退到更小 batch;模型进程崩溃则依赖进程管理器拉起并完成预热后再接入流量;上游限流需要退避并反馈到调度层。

可以用下面这段伪代码统一表达「按错误类型决定是否重试」:

def handle_error(err, attempt):
    if err.is_retryable and attempt < max_retries:
        delay = min(cap, base * (2 ** attempt)) + random_jitter()
        time.sleep(delay)
        return RETRY
    if err.is_retryable:
        return GIVE_UP_AND_ALERT
    return FAIL_FAST

告警要按故障类型分级:单副本失败是常规事件,多点失败是严重事件。把故障类型、发生副本、影响请求数和恢复耗时记录到监控,才能形成「失败-恢复-改进」的闭环,而不是每次靠人工重启。

案例

某副本发生 OOM 后,健康检查标记其为不健康,负载均衡器停止向它分配新流量,其余副本继续服务。该副本被重启后重新加载权重、完成预热并通过就绪检查,才被重新加入流量池,避免「刚起来就再次被打挂」。

客户端侧只对可重试错误使用指数退避:第一次失败退避较短,连续失败逐步延长间隔并设置上限,超过重试次数后放弃并上报。对确定性错误则不重试,直接快速失败。

验证时观察故障期间的成功率与恢复耗时:单副本 OOM 不应导致整体不可用,p95 延迟的波动应被其他副本吸收。结论是容错的核心在于「健康检查 + 有限重试 + 幂等 + 分类型处理」,四者缺一不可。

相关主题