vLLM 长时运行宕机复盘:自愈 supervisor 误杀 940 次
事故表象:5-token 请求压垮整个服务
事故现象是一个 5-token 的单一请求打挂了整个 vLLM(DeepSeek-V4) 服务。第一反应都指向"前缀缓存清理不掉",排查方向完全跑偏了。
真实根因:decode 单步耗时超线性增长
实际根因链条:
- 单个超长上下文请求的 decode 单步耗时随序列长度呈超线性增长
- EngineCore 等待 worker 返回结果时触发超时:
VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS默认 300s - 超时后抛
TimeoutError→ EngineCore fatal
关键点:这不是缓存问题,是单请求步耗时把 RPC 超时窗口打穿了。
vLLM 不自愈:容器活着,服务已死
这一层最迷惑:
- EngineCore fatal 后,容器进程依然存活
- docker 层面看:
running=true,RestartCount=0 - 实际状态:API 永久挂死,残留 worker 进程继续占着显存
没有外部干预的情况下,这个状态永远不会恢复。
二次事故:自愈 supervisor 本身成为事故源
为了兜底不自愈的问题,引入了外部 supervisor,逻辑设计合理:
- 每 30s 探测一次
/health - 连续 3 次失败,或日志命中
EngineCore fatal,执行docker rm -f重建容器
坑在这里:
启动宽限参数 STARTUP_GRACE 设成了 180s,而 vLLM(DeepSeek-V4) 完整冷启动实测要 6-7 分钟。180s 宽限远小于实际启动时间。
于是出现一个反直觉的场面:
- 没有任何真实流量
- 服务处于正常冷启动过程中
- supervisor 在宽限期结束后持续探测
/health失败,判定为宕机 - 执行重建,进程被杀死,重新启动
- 第二次启动又超 180s,再次被打断……
累计重建 940 次。
自愈脚本从"兜底保障"变成了"主动攻击源"。宽限参数 bug 直接导致正常启动的进程被反复误杀。
修复:宽限对齐实际启动时间
修复方案很直接:把启动宽限对齐到容器实际启动时间,STARTUP_GRACE 调到 600s。
修复后零误杀。
工程教训与信号
- 调大 RPC 超时≠解决卡死。
VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS调大是给真实慢请求留出恢复窗口;真卡死不会因为超时调大而恢复。两者要区分清楚。 - supervisor 的价值是保证最终恢复,但它的判定逻辑必须贴合服务的真实启动耗时,否则自愈脚本自身的误判就是新的故障源。
- 监控信号:KV 占用高 + worker 吞吐冻结,是卡死的高概率前置信号,应该作为独立的监控组合项接入告警。(事故具体持续时间原文未提供)
# 修复后的核心参数对照
STARTUP_GRACE = 180 # 错误值,远小于实际启动时间 6-7 分钟
STARTUP_GRACE = 600 # 修复值,基于容器实际启动时间对齐
# 判定逻辑(修复后)
def should_rebuild(container):
if uptime < STARTUP_GRACE:
return False # 宽限期内不判定,防止误杀冷启动
if health_check_fail_count >= 3:
return True
if log_contains("EngineCore fatal"):
return True
return False
核心结论:监控与自愈系统的参数必须按真实运行时间校准,否则兜底系统本身就是最大的不确定因素。