跳转至

04 · 排障与暗坑

本文按"现象 → 定位 → 修复"组织:

  • §1-§4:四类常见排障路径
  • §5:跟 health check / rate limiting 的关系(再次明确)
  • §6:实际故障场景排查 checklist

排障流程总图

flowchart TD
    Start{现象是什么?}
    Start -->|"deployment 失败但没自动避让"| S1[§1 没 cooldown]
    Start -->|"deployment 被避让过度"| S2[§2 single deployment 特殊行为]
    Start -->|"wildcard model 永远不冷"| S3[§3 wildcard 永不 cooldown]
    Start -->|"多 Pod 部署 allowed_fails 不准"| S4[§4 InMemoryCache 陷阱]
    Start -->|"分不清 cooldown / health / rate limit"| S5[§5 三套机制对比]
    Start -->|"想知道当前哪些在 cooldown"| S6[§6 状态可观测性]

1. 为什么没 cooldown

1.1 现象

某 deployment 明显在返回 5xx,但 /v1/model/info 还能选到它,Prometheus 上 litellm_deployment_cooled_down 也没增长。

1.2 定位 checklist

按概率排序:

A. 错误码不在白名单

第一关 _is_cooldown_required 把 400/403/422/APIConnectionError 直接放行不 cooldown。

# 看业务请求实际收到的错误码
grep "exception_status" <proxy-log>

如果是 400/403/422 → cooldown 不会触发,这是设计(用户请求错怪不到 deployment 头上)。

B. V2 模式当分钟流量太低

路径 1.3 要求 当分钟内 ≥ 5 个请求。如果 deployment 流量稀疏(每分钟 1-2 个),即使全失败也不触发。

唯一兜底是路径 1.4(永久错误),看你的失败码是不是 401/404。

解决:要么开 V1 模式(allowed_fails)按累计计数,要么提高流量。

C. is_single_deployment_model_group=True

如果 model_group 只 1 个 deployment: - 路径 1.1(429)不触发 - 路径 1.3(50% 失败率)不触发 - 只剩路径 1.2(100% 失败 + 流量 ≥1000)和路径 1.4(永久错误)

详见 §2.

D. disable_cooldowns: true

grep -E "disable_cooldowns" proxy_server_config.yaml

设了就完全没 cooldown。

E. deployment 是 provider_default_deployment_ids 成员

详见 §3.

F. retry 期间不写 cooldown(重要!)

如果 num_retries: 3,业务方报失败前 retry 了 3 次。这 3 次内的失败不写 cooldown,只有第 4 次(最终失败)才走 callback 触发 cooldown。

→ 业务侧"看起来"该 deployment 失败了 3 次但没冷——其实是 retry 内部消化了,只算 1 次"最终失败"。


2. 单 deployment model group 的特殊行为

2.1 现象

某 model_name 在 model_list 里只配了一个 deployment,发现这个 deployment 偶发失败但没被 cooldown。或反之,配了多 deployment,某个失败后 cooldown 行为跟单 deployment 不同。

2.2 代码

_should_cooldown_deployment:191-194:

model_group = litellm_router_instance.get_model_group(id=deployment)
is_single_deployment_model_group = False
if model_group is not None and len(model_group) == 1:
    is_single_deployment_model_group = True

V2 模式 4 条路径中,3 条带了 not is_single_deployment_model_group 限制:

路径 单 deployment 是否触发
1.1: 429 + 多 deployment
1.2: 100% 失败 + 流量 ≥ 1000 ✅(这条专门给 single 用)
1.3: 失败率 > 50% + 流量 ≥ 5 + 多 deployment
1.4: 永久错误(401/404 等)

2.3 设计意图

单 deployment 一旦冷就没人服务这个 model_name 了——业务方所有请求都失败。所以触发条件被抬高很多:必须是"持续 100% 失败 + 大流量"或"明确永久错误(auth 错)"才冷。

瞬时抽风 / 偶发 5xx 不会冷。

2.4 怎么办

如果你确实想让单 deployment 也对偶发 5xx 敏感:

litellm_settings:
  allowed_fails: 3        # 启用 V1 模式,3 次失败就冷
                          # V1 模式不区分 single vs multi

或者给这个 model_name 多配一个 deployment(哪怕指向同一个上游)—— 让它脱离 single deployment 行为。


3. Wildcard model 永不 cooldown

3.1 现象

配了通配符模型如:

model_list:
  - model_name: openai-anything
    litellm_params:
      model: openai/*
      api_key: os.environ/OPENAI_API_KEY

某次请求 model: openai/o3-mini 失败了,发现 openai-anything 这个 deployment 没被 cooldown。

3.2 代码

router.py:6539-6540 维护一个集合 provider_default_deployment_ids,把所有 model_name* 的 deployment 加进去。

_should_run_cooldown_logic:157-161:

if deployment in litellm_router_instance.provider_default_deployment_ids:
    verbose_router_logger.debug(
        "Should Not Run Cooldown Logic: deployment is in provider_default_deployment_ids"
    )
    return False

→ wildcard deployment 永远不进入 cooldown 流程

3.3 设计意图

通配符 deployment 是"兜底路由",用户可能用各种各样的 openai/xxx model name 通过它走。如果它被冷,所有未明确配置的 OpenAI 模型都没法走了——影响面太大。

→ wildcard 不冷是有意为之。别期待它的故障保护

3.4 怎么办

通配符 deployment 一般用于"长尾模型路由",不是核心业务路径。如果你需要核心业务的故障保护,别用通配符 —— 显式列出每个 model_name + 多 deployment。


4. allowed_fails 计数器是 InMemoryCache → 多 Pod 状态不一致

4.1 现象

启用了 V1 模式(allowed_fails: 3allowed_fails_policy),prod 多 Pod 部署。明明某 deployment 全局失败超过 3 次了,但没 cooldown。

4.2 根因

router.py:487-489:

self.failed_calls = (
    InMemoryCache()
)  # cache to track failed call per deployment

failed_callsInMemoryCache,不是 DualCache。意味着: - 每个 Pod 各自维护一份计数器 - Pod A 上失败 2 次、Pod B 上失败 1 次、Pod C 上失败 0 次 - 全局已经失败 3 次但每个 Pod 内部计数不到 3 - 没有任何 Pod 触发 cooldown - 实际触发"冷却"延迟了 N 倍(N=Pod 数)

4.3 跟 V2 默认模式对比

维度 V2 默认 V1 allowed_fails
失败计数存储 track_deployment_metrics(InMemoryCache,local_only=True router.failed_calls(InMemoryCache)
多 Pod 共享?

两个模式都有多 Pod 不共享问题。但 V2 用百分比阈值(50%),即使分散到多 Pod 也能在每个 Pod 上独立触发。V1 用绝对计数,多 Pod 下要"每个 Pod 各自累计够阈值"才冷。

4.4 怎么办

多 Pod 部署推荐 V2 默认模式(什么都不配)。V1 模式适合单 Pod 服务或跟 Pod 数无关的"全局阈值"语义。

如果你必须用 V1 多 Pod:把 allowed_fails 设小(如 2-3),让每个 Pod 都能独立触发。代价:可能比预期更激进 cooldown。


5. 跟 health check / rate limiting 的关系

三套机制经常被混淆,再次明确:

机制 触发者 影响 默认时长 多 Pod 协调
Cooldown 业务请求失败 路由决策(跳过 deployment) 5 秒 Redis 共享
Health check proxy 主动 ping /health 端点 + DB 表 30s tick / 5min ping Redis 共享(详见 心跳文档
Rate limiting auth 层 拒绝请求(429) 视配置 Redis 共享

5.1 三者互不耦合

心跳失败  ≠ cooldown 触发(健康检查不调 cooldown)
cooldown  ≠ 心跳数据更新(cooldown 写 cooldown_cache,心跳读 health_check_results)
rate limit≠ cooldown(rate limit 在 auth 层判,跟 deployment 健康无关)

5.2 实际配合的反应链

上游 deployment X 真挂了 30 分钟:

T=0      X 真挂
T=15     某用户请求 → Router 路由到 X → 5xx
         num_retries 内部接住,选别的 deployment retry → 200 OK
         但首次 X 失败已经记账(track_deployment_metrics: X:fails +1)
T=15+    继续有请求 → Router 还是可能选 X → 持续失败累积
T=15+N   当分钟 X 失败率 > 50% 且总数 ≥ 5 → cooldown X 5 秒
T=20+N   X cooldown 过期 → Router 又可能选 X → 5xx → 又 cooldown
... 周而复始

T=300    第一次后台心跳 → ping X → 失败 → 写 health_check_results.unhealthy
         但这条信息不影响路由!只影响 /health 端点显示
T=600    第二次心跳 → 还是失败 → DB 表 LiteLLM_HealthChecks 记一条状态变化
T=...    上游 X 真恢复
T=300+N  心跳 ping X → 成功 → 标记 healthy
T=...    业务请求选到 X → 200 → cooldown 不再触发

真正在 prod 防止用户撞墙的是 cooldown。心跳的作用是给运维看"哪个 deployment 不健康"。

5.3 如果你想让心跳失败也触发 cooldown

LiteLLM 没原生开关,要自己加。两个思路:

方案 A:改源码,在 _run_background_health_check 处理 unhealthy_endpoints 时调 _set_cooldown_deployments。约 5 行改动。

方案 B:写一个外部脚本,定期读 /health 端点,找 unhealthy 的 deployment,调 admin API 手动加 cooldown。

通常没必要——cooldown 基于真实业务流量已经足够准。心跳虚假失败反而会误冷 deployment。


6. cooldown 状态可观测性

6.1 没有的能力

  • ❌ 没有 /cooldown/list 这种 admin endpoint
  • /v1/model/info 不展示 cooldown 状态
  • /health 端点跟 cooldown 无关

6.2 三种可用方式

A. 直接查 Redis

.venv312/bin/python -c "
import os, redis, time
r = redis.Redis(host=os.environ['REDIS_HOST'], port=int(os.environ['REDIS_PORT']),
                password=os.environ.get('REDIS_PASSWORD'))
keys = r.keys('deployment:*:cooldown')
print(f'当前 cooldown 中的 deployment 数: {len(keys)}')
for k in keys:
    print(f'  {k.decode()}  TTL={r.ttl(k)}s')
"

B. Prometheus 指标

PromQL 查"过去 5 分钟 cooldown 触发次数":

sum by (model_name) (
    increase(litellm_deployment_cooled_down_total[5m])
)

PromQL 查"当前正在 cooldown 的 deployment":

sum by (model_name) (litellm_deployment_complete_outage)

C. 日志 grep

# 触发 cooldown 时 verbose_router_logger 会打日志
grep "Attempting to add.*to cooldown list\|Pod.*added to cooldown" <proxy-log>

需要 LITELLM_LOG=DEBUG 才能看到详细的 router 日志。


7. 常见排障 checklist

遇到 cooldown 相关问题,按顺序检查:

  • [ ] 业务请求实际收到的错误码是什么?是不是白名单内(401/404/408/429/5xx)?
  • [ ] disable_cooldowns 有没有设?
  • [ ] model_group 有几个 deployment?1 个 vs 多个行为完全不同
  • [ ] 是 wildcard model(openai/* 这种)吗?wildcard 永不冷
  • [ ] V2 还是 V1 模式?YAML 有 allowed_fails / allowed_fails_policy 吗?
  • [ ] 当分钟流量是不是 < 5?V2 路径 1.3 触发不到
  • [ ] 多 Pod 部署 + V1 模式?计数器不共享,触发会延迟
  • [ ] num_retries 是多少?retry 期间失败不写 cooldown
  • [ ] Redis 通吗?cooldown_cache 写不进 Redis 多 Pod 不共享 cooldown 列表
  • [ ] Prometheus 指标 litellm_deployment_cooled_down 有没有增长?无 = 触发逻辑有问题,有 = 触发了但可能太短没观察到

8. 完整故障复现 / 验证步骤

如果你想验证 cooldown 在你们 prod 是否正常工作:

8.1 故意制造一次失败

# 临时把某 deployment 的 api_key 改错(UI / DB),发起一个请求
curl -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  http://your-proxy/v1/chat/completions \
  -d '{
    "model": "the-broken-deployment",
    "messages": [{"role":"user","content":"test"}]
  }'
# 期望返回 401

8.2 立刻查 Redis

.venv312/bin/python -c "
import os, redis
r = redis.Redis(...)
print('keys:', r.keys('deployment:*:cooldown'))
"

→ 应该看到那个 deployment 的 id 在 cooldown 列表里。

8.3 观察后续请求

# 重复发请求,看 Router 是否还选到那个 deployment
for i in {1..10}; do
  curl -sH "Authorization: Bearer $LITELLM_MASTER_KEY" \
    http://your-proxy/v1/chat/completions \
    -d '{"model":"that-model-group","messages":[{"role":"user","content":"hi"}]}' \
    | jq '.model'
done

→ 期望 cooldown_time 内不会路由到坏 deployment。cooldown_time 后会再次尝试它。

8.4 修复

把 api_key 改回正确,等 5 秒(或你的 cooldown_time)后请求会重新成功。


下一步