03 · 降低心跳成本¶
本文系统讲清楚:心跳到底烧什么、按什么因子放大,以及哪些手段能把它压下去。
1. 心跳成本的三个独立因子¶
flowchart LR
Total["每小时心跳<br/>请求数"]
F1["① 频率<br/>3600 / TTL"]
F2["② 模型数<br/>未禁用心跳的 deployment"]
F3["③ Pod 放大<br/>多 Pod fallback"]
F4["④ 单 deployment 内放大<br/>retry × wildcard fallback"]
F1 --> Total
F2 --> Total
F3 --> Total
F4 --> Total
style F1 fill:#fef
style F2 fill:#eff
style F3 fill:#fee
style F4 fill:#ffe
公式(量级估算):
举例:本项目 prod 早期配置(TTL=300、28 deployment、9 Pod、无 wildcard、健康模型 retry≈0):
观察值是 242/5 分钟,与公式一致(242 ≈ 252,差几个属于失败被 timeout 吃掉)。
2. 压制手段(按 ROI 排序)¶
2.1 ★★★ 拉大 TTL(频率因子)¶
最简单、收益最大、不改代码不改模型配置:
# k8s deployment env
- name: DEFAULT_SHARED_HEALTH_CHECK_TTL
value: "1800" # 30 分钟
- name: DEFAULT_SHARED_HEALTH_CHECK_LOCK_TTL
value: "120"
改完必须 rolling restart 所有 Pod(02 §2.2)。
效果:5min → 30min,请求量 ÷ 6。
📌 注意:这个手段只对 SHARED 模式有效。如果你
redis_usage_cache=None走 DIRECT 路径,需要改 YAML 的health_check_interval(默认 300)。最快验证当前走哪条路径见 04 §1.
2.2 ★★★ 给贵模型加 disable_background_health_check(模型数因子)¶
model_list 里的 deployment 不是每个都需要每 30 分钟主动巡检。一份合理的策略:
| 模型类型 | 心跳策略 |
|---|---|
| 关键链路、调用量大、便宜 | 保留心跳,正常巡检 |
| 备用模型、调用量小、便宜 | 保留心跳 |
| 贵模型(gpt-4o / Claude Opus / Gemini Pro) | 禁用心跳或改打便宜替代 |
| 通配符模型 | 禁用心跳(扇出代价不可控) |
| 测试模型、灰度模型 | 禁用心跳 |
YAML:
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
model_info:
disable_background_health_check: true
UI 录入的模型直接 SQL 批量改:
UPDATE "LiteLLM_ProxyModelTable"
SET model_info = jsonb_set(
coalesce(model_info, '{}'::jsonb),
'{disable_background_health_check}',
'true'::jsonb
)
WHERE model_name IN (
'gpt-4o', 'gpt-4o-2024-08-06',
'claude-opus-4-7', 'claude-sonnet-4-6',
'gemini-2-5-pro'
);
效果:参与轮询的 deployment 数从 28 → 10,请求量 × 10/28 ≈ ÷ 2.8。
改完不需要重启 proxy,下一个 tick 自动生效(01 §2 关键事实#2)。
2.3 ★★ 让贵模型改打便宜替代(单次成本因子)¶
如果"完全跳过"风险不可接受(你确实想知道这个 deployment 是否还活着),用 health_check_model 把心跳指向同 endpoint 的便宜模型:
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_base: https://eastus.openai.azure.com
api_key: os.environ/AZURE_KEY
model_info:
mode: chat
health_check_model: gpt-4o-mini # 同 endpoint 上的 mini
前提:health_check_model 必须能用同样的 api_base / api_key 访问,且它的可用性能反映原模型可用性。Azure / Bedrock 等同 endpoint 多模型场景非常合适。
效果:单次心跳成本从 ~\(0.001(gpt-4o)→ ~\)0.00003(gpt-4o-mini),单次成本 ÷ 30。
2.4 ★★ 用 embedding mode 心跳(单次成本因子)¶
embedding 调用通常比 chat 调用便宜 1-2 个数量级,且某些 provider 的 embedding 端点稳定性能反映 chat 端点。
model_list:
- model_name: my-chat-model
litellm_params:
model: openai/gpt-4o
model_info:
mode: embedding
health_check_model: text-embedding-3-small
⚠️ 注意:这是个 hack。如果 chat 服务挂了但 embedding 服务还活着,你会得到误报"healthy"。仅推荐对成本极敏感、对探测精度可放松的场景。
2.5 ★ 把通配符模型禁用心跳(扇出因子)¶
01 §5.4 提到,wildcard openai/* 一次心跳最多触发 3 个 fallback 模型 × 4 次 retry = 12 次真实调用。
如果 model_list 里有通配符 deployment,建议直接:
model_list:
- model_name: openai-anything
litellm_params:
model: openai/*
model_info:
disable_background_health_check: true # ★ 别让它自动 ping
通配符模型一般是给用户自由路由用的,自己跳过心跳损失不大。
2.6 ★ 减少 num_retries 的放大(retry 因子)¶
litellm_settings.num_retries: 3 是给业务请求设的——失败时自动重试,不希望透传错误。但心跳不需要这个语义:失败就是失败,下个周期再试。
目前 LiteLLM 没有"per-call 关闭 num_retries"的官方开关。两个折衷办法:
方案 A:改源码(侵入性小)¶
litellm/proxy/health_check.py:99-105 改成:
task = run_with_timeout(
litellm.ahealth_check(
{**model["litellm_params"], "num_retries": 0}, # ★ 心跳强制 0 重试
mode=mode,
prompt=DEFAULT_HEALTH_CHECK_PROMPT,
input=["test from litellm"],
),
timeout,
)
效果:失败 deployment 单次心跳调用从 4 次 → 1 次。如果你有 5 个不健康 deployment,每次 burst 减少 15 次请求。
方案 B:保持现状¶
如果你的 deployment 平时都很健康(极少 retry),num_retries 的放大可以忽略,不必动源码。
2.7 ★ 缩短 health_check_timeout(间接降低成本)¶
收益不是减少请求数,而是: - 缓慢/挂死的 deployment 会更快被判失败 - leader pod 的总耗时变短,减少 follower fallback 触发概率(04 多-pod-fallback-放大) - 整体 health check 周期更短,DB / 内存负载更低
3. 常见误区¶
3.1 ❌ "health_check_details: false 能省钱"¶
_health_endpoints.py:1657 的 details 只控制 /health API 返回的字段(隐去 messages / api_key / 等),和真实 ping 数量完全无关。
改成 false 唯一的好处是 _save_background_health_checks_to_db 写入的 JSON 更小。
3.2 ❌ "把 health_check_interval 设得很大就行"¶
在 SHARED 模式下,health_check_interval 控制的是 tick 间隔,不是真实 ping 间隔。tick 间隔大了反而响应延迟变长:
- 假设
health_check_interval=300、TTL=1800 - cache 在 T+1800 过期
- 但要等到下一个 tick(最坏 T+2100,多了 5 分钟空档)才会真正打模型补缓存
建议组合:health_check_interval=30(小)、TTL=1800(大)。
3.3 ❌ "use_shared_health_check: true 就一定走 SHARED 路径"¶
实际还要 redis_usage_cache != None。后者要求 litellm_settings.cache: true 且 cache_params.type: redis 且 Redis 真的连得上(01 §4)。
经常踩的坑:
| 表象 | 实际 |
|---|---|
YAML 写了 use_shared_health_check: true |
manager 是否创建还要看 redis_usage_cache |
redis=True ping 通了 |
那是脚本里的连接测试,不代表 litellm.cache.cache 是 RedisCache |
启动日志没有 _init_cache 报错 |
不一定 OK,要看 [HC-DEBUG-E] 类型的明确确认 |
排查方法见 04 §3.
3.4 ❌ "增加 retry 能让心跳结果更准"¶
心跳的目的是 判断当前可用性。retry 反而模糊了"瞬时不可用"和"可用"的边界——有些 deployment 第一次失败、第二次成功,本质是部分不健康,retry 后被记成 healthy 反而误导。
把心跳的 num_retries 设 0 是更诚实的做法。
4. 推荐组合¶
4.1 成本敏感型(推荐生产基线)¶
目标:把心跳成本压到原来的 1/40 量级。
# proxy_server_config.yaml
litellm_settings:
cache: true
cache_params:
type: redis
host: os.environ/REDIS_HOST
port: os.environ/REDIS_PORT
password: os.environ/REDIS_PASSWORD
general_settings:
background_health_checks: true
use_shared_health_check: true
health_check_interval: 30
health_check_details: false
# k8s deployment
env:
- name: DEFAULT_SHARED_HEALTH_CHECK_TTL
value: "1800"
- name: DEFAULT_SHARED_HEALTH_CHECK_LOCK_TTL
value: "180" # 必须大于一次完整 health check 总耗时,prod 28 模型实测 12-30s
-- DB: 给所有贵模型禁用心跳
UPDATE "LiteLLM_ProxyModelTable"
SET model_info = jsonb_set(coalesce(model_info, '{}'::jsonb),
'{disable_background_health_check}', 'true'::jsonb)
WHERE model_name IN (
'gpt-4o', 'gpt-4-turbo',
'claude-opus-4-7',
'gemini-2-5-pro'
);
预期收益(vs 默认配置 28 deployment + 9 Pod + TTL=300,且本 fork 已修复 fallback 放大 bug):
| 因子 | 原(修复前 + 默认 TTL) | 新(修复 + TTL=1800 + 贵模型跳过) | 倍率 |
|---|---|---|---|
| 频率(/小时) | 12 | 2 | × 1/6 |
| deployment 数 | 28 | 10(贵模型跳过) | × 10/28 |
| Pod 放大 | 9(fallback bug) | 1(已修复) | × 1/9 |
| 总计 | ~3024 / 小时 | ~20 / 小时 | ÷ ~150 |
🔧 修复说明:本 fork 已把 follower 抢锁失败后的
sleep 2 + 自打改成polling 到 lock_ttl 超时,消除 N×deployment 放大。详见 04 §5。
4.2 极简型(无后台心跳)¶
故障感知完全靠业务请求自然发现 + Router cooldown 机制。
适用场景: - 流量足够大,每个 deployment 每分钟都有真实请求经过 - 上游 SLA 高,不需要自己额外巡检
5. wildcard 的隐性放大¶
health_check_helpers.py:36-46 对通配符模型的处理:
if len(cheapest_models) > 1:
fallback_models = cheapest_models[1:] # 选 2 个 fallback
model_params["model"] = cheapest_models[0]
model_params["fallbacks"] = fallback_models
model_params["max_tokens"] = 10
await acompletion(**model_params)
acompletion 会按 fallbacks 顺序尝试,每个失败的尝试还会被 num_retries 放大:
flowchart LR
Start[一次 wildcard 心跳]
M1[尝试 cheapest_models 0]
M2[失败 → 尝试 fallback 1]
M3[失败 → 尝试 fallback 2]
Start --> M1
M1 -- 失败 + 3 次 retry --> R1[共 4 次调用]
M1 -- 成功 --> S1[共 1 次调用]
R1 --> M2
M2 -- 失败 + 3 次 retry --> R2[再 4 次]
M2 -- 成功 --> S2[累计 4+1=5 次]
R2 --> M3
M3 -- 任意 + 3 次 retry --> R3[最多再 4 次<br/>累计 12 次]
最坏情况一个 wildcard deployment 一次心跳产生 12 次真实模型调用。
最佳建议:通配符模型直接 disable_background_health_check: true。心跳本来就不适合用通配符——首选不一定能反映"用户实际会用到"的子模型可用性。
下一步¶
- 改了配置没效果:04-troubleshooting.md §1-2
- 真实案例完整复盘:04-troubleshooting.md §实战案例