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。
如果是 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¶
设了就完全没 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 敏感:
或者给这个 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: 3 或 allowed_fails_policy),prod 多 Pod 部署。明明某 deployment 全局失败超过 3 次了,但没 cooldown。
4.2 根因¶
failed_calls 是 InMemoryCache,不是 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 触发次数":
PromQL 查"当前正在 cooldown 的 deployment":
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)后请求会重新成功。