4.4 生产故障排查与应急处置 清单再严密(4.3),生产事故也躲不开——区别只在于事故频率和恢复速度。我管过两年 vLLM 生产服务,经历过凌晨被叫起来救火,也见过事故从「告警闪一下」恶化到「整集群雪崩」的全过程。这一节不写「报错对照表」那种条目堆叠(速查附录那种东西救不了真事故),而是把生产里真正高频、真正让人头大的几类故障,按「现象 → 诊断 → 处置 → 根因修复」的实战流程讲透。每个故障我都尽量给真实案例,让你能对号入座。 读完之后,你应该能在事故现场(凌晨 3 点、领导盯着、用户在投诉)快速完成「现象归类 → 指标定位 → 应急止血 → 事后根因」这套动作,而不是慌乱地乱试参数。故障排查的本质是「快速收敛假设」,这一节教的就是怎么收敛。
清单再严密(4.3),生产事故也躲不开——区别只在于事故频率和恢复速度。我管过两年 vLLM 生产服务,经历过凌晨被叫起来救火,也见过事故从「告警闪一下」恶化到「整集群雪崩」的全过程。这一节不写「报错对照表」那种条目堆叠(速查附录那种东西救不了真事故),而是把生产里真正高频、真正让人头大的几类故障,按「现象 → 诊断 → 处置 → 根因修复」的实战流程讲透。每个故障我都尽量给真实案例,让你能对号入座。
读完之后,你应该能在事故现场(凌晨 3 点、领导盯着、用户在投诉)快速完成「现象归类 → 指标定位 → 应急止血 → 事后根因」这套动作,而不是慌乱地乱试参数。故障排查的本质是「快速收敛假设」,这一节教的就是怎么收敛。
具体故障千差万别,但排查的「套路」是固定的。任何一次事故,我都是按这三板斧走:
第一斧:看 vLLM 启动日志和运行日志。vLLM 启动时打印的关键信息——权重占用显存、KV Cache 池 block 数、实际生效的 TP/EP、是否启用了 CUDA Graph——是你判断「配置是否符合预期」的第一手证据。运行日志里的 WARNING/ERROR 是事故的直接线索。坑:默认日志级别可能不够,事故复盘后建议把日志级别调成 INFO 或 DEBUG 一段时间,捕获更多上下文。
第二斧:看 /metrics 黄金三指标。Prometheus 抓的 vLLM 指标里,下面三个是任何事故的「体温计」:
vllm:num_requests_waiting(排队数):持续 >0 说明过载或慢。vllm:gpu_cache_usage_perc(KV 占用率):逼近 1.0 说明池将满,可能触发抢占。vllm:preemption_count(抢占次数):累计上涨说明池不够,正在换出 KV。这三个指标的形态(突刺、缓涨、抖动)能立刻把故障范围收敛到「过载 / 池满 / 调度异常」三类之一。
第三斧:最小复现。剥离业务复杂度,用最小 batch(1~2 个请求)+ 最短 prompt("hi")+ 同样的启动参数复现。如果最小用例也复现,是 vLLM/环境问题;如果只在真实流量下复现,是容量/调度问题。这一步能省掉大量瞎猜。
下面按故障类型展开。
现象:日志抛 RuntimeError: CUDA out of memory. Tried to allocate X.XX GiB,服务可能崩溃或某个请求失败。
诊断:OOM 不是单一原因,必须分清是哪一种:
gpu-memory-utilization 设太高,或权重比预期大(量化版本错)。处置:降低 utilization(0.95→0.85),或核对权重量化格式。max-num-seqs,或开 --swap-space 给 preemption 兜底。KV cache exhausted, request rejected。这是 vLLM 主动拒绝(不是 OOM crash),属于「健康降级」。处置:扩容或限流,不是改 utilization。真实案例:某次凌晨告警,服务每 10 分钟崩一次重启。看日志是运行时 OOM,看 metrics 发现 gpu_cache_usage_perc 在崩前 30 秒从 0.7 飙到 0.98——某个用户发了几个 30k+ 的超长 prompt,瞬间把池吃爆。应急止血:把 max-model-len 从 32768 砍到 8192(业务侧加了长度校验)。根因修复:在网关层加 prompt 长度预校验,超长请求直接拒绝,不让它进 vLLM。
经验法则:生产里把 utilization 设到 0.850.90,把 --swap-space 设 48GB,把 max-model-len 砍到业务峰值而非模型上限——这三件事做了,OOM 概率下降一个量级。
现象:启动时报 CUDA driver version is insufficient for CUDA runtime version,服务起不来。
诊断:宿主机 NVIDIA 驱动版本低于 vLLM/PyTorch 要求的 CUDA Runtime 版本。这是纯环境问题,不是参数问题。
处置:
nvidia-smi | grep "Driver Version"。apt install nvidia-driver-535(或对应版本),重启机器。真实案例:某次新机器到货,开发同事用旧镜像启动,报这个错。检查发现驱动是 525,但镜像里 PyTorch 编译用了 CUDA 12.1(要 530+)。坑:驱动升级要重启机器,生产环境要协调维护窗口,不能临时升级。预防:机器交付时就预装达标驱动,镜像构建时锁定 CUDA 版本,二者做兼容性矩阵。
现象:启动报 ValueError: Total number of attention heads (N) must be divisible by tensor parallel size (M)。
诊断:经典低级错误。模型 num_attention_heads 是固定的(比如 96),TP 只能取能整除的值(1/2/3/4/6/8/12/24/48/96),TP=5、TP=7 这种永远用不了。
处置:换 TP 值为能整除的(最常用 2/4/8)。检查模型 config 的 num_attention_heads,确认可选 TP。
真实案例:某同事在 10 卡机器上想 TP=10(图省事用满卡),报这个错。改成 TP=5?也不行(96 不能被 5 整除)。最终用 TP=8 留 2 卡空着,或改用 PP=2 × TP=5 的组合(PP 不要求整除头数)。**经验**:TP 的取值要预先算好「能整除头数」的合法集合,避免部署时撞墙。
现象:服务正常跑,没报错,但吞吐只有预期的 30%~50%,业务侧反馈「卡」。
诊断:这类故障最难,因为没有显性错误。按下面顺序排查:
--enforce-eager:禁用 CUDA Graph 会损失 10%~20% 吞吐。检查启动参数,生产应关 eager。这是最常见的吞吐杀手,很多人调试时开了忘了关。--enable-chunked-prefill。max-num-seqs 太低)、网络/客户端请求慢、CPU 调度瓶颈。vllm:preemption_count 涨得快说明 KV 池不够,请求在换出换进,吞吐崩。处置:扩大 KV 池(提 utilization)或降并发(降 max-num-seqs)。真实案例:某次压测吞吐只有预期的 40%,团队调了一周参数没用。最后发现是压测工具的客户端并发数设成了 1——服务端一切正常,是压测自己没把请求并发发出来。教训:排查吞吐问题要先排除「客户端侧瓶颈」,再看服务端。GPU 利用率是分水岭——GPU 满载但吞吐低,是算力/模型问题;GPU 不满载,是调度/IO 问题,方向完全不同。
现象:业务侧反馈「点一下要等好几秒才出第一个字」,metrics 显示 vllm:time_to_first_token_seconds P99 飙到几秒。
诊断:TTFT 高几乎都是 prefill 慢或队列堆积:
--enable-chunked-prefill,调小 --max-num-batched-tokens(如 8192→4096)让 prefill 切更细。num_requests_waiting 持续 >0:请求排队等 prefill,处置是扩容或限流。max-num-seqs 取值范围,或临时开 eager 排查。真实案例:某 RAG 业务 TTFT 飙到 8 秒,用户无法忍受。排查发现召回模块拼出来的 prompt 普遍 15k+,prefill 慢。开了 chunked prefill + 把 max-num-batched-tokens 设 4096,TTFT P99 降到 800ms。启示:长 prompt 业务,chunked prefill 不是可选项是必选项,且 max-num-batched-tokens 要按 prompt 长度分布调,不是默认值就完事。
现象:服务正常返回,但输出是乱码、重复、或质量明显下降。
诊断:
--quantization awq 跑了 GPTQ 权重,或反过来。验证:核对权重目录下的 quantization_config.json 或 config.json,与启动参数一致。--dtype float16 跑 BF16 权重可能溢出,输出 NaN 或乱码。处置:用 --dtype auto 或匹配权重的 dtype。max-model-len 被截断,模型看到的是不完整上下文,输出偏题。处置:业务侧做长度校验,或提高 max-model-len。--kv-cache-dtype fp8,超长上下文精度下降。处置:精细任务关闭 KV 量化,做 A/B 评估。真实案例:某次模型升级后用户反馈「答非所问」,排查发现新版本模型 config 里 chat_template 变了,但前端还在用旧模板拼 prompt,模型看到的是错位输入。处置:同步前端的 chat_template。启示:模型升级时,tokenizer、chat_template、特殊 token 都要一起验证,不能只看「能跑」。
现象:多机部署启动时报 NCCL 相关错误(NCCL error、NCCL communicator initialization failed),或运行中卡死。
诊断:
NCCL_SOCKET_IFNAME(指定网卡)、NCCL_IB_DISABLE(无 IB 时禁用)、VLLM_HOST_IP(节点对外 IP)。处置:用统一启动脚本(如 Ray cluster)管理这些环境变量。真实案例:某次多机部署 1 号节点能起,2 号节点起不来,报 NCCL timeout。查了一天发现是 2 号节点的防火墙规则比 1 号严格,挡了 NCCL 端口。坑:多机问题调试成本极高,建议先用 nccl-test 工具单独验证节点间通信,再起 vLLM。
具体故障之外,更重要的一个能力是「事故现场的优先级判断」。凌晨被告警叫醒时,你的目标不是「修好根因」,而是「尽快止血恢复服务」。优先级顺序是:
一个我反复强调的原则:事故现场禁止「试参数」。看到 OOM 就临时改参数重启,是事故扩大的常见原因——改错了服务直接起不来,原本能跑的也跑了。临时改动必须经过测试,不能在生产现场试。如果一定要临时改,先在备用实例上验证,再切流。
最后说一点:故障排查能力不应该只存在于「老员工脑子里」,要工程化成团队资产。我团队里有几样东西,每个都对应一类能力沉淀:
故障排查能力的成熟度,不在于「能不能解决」,而在于「能不能被复制」。能让新人在三个月内具备老人七成的排障能力,靠的就是这套工程化沉淀。这一节给的故障类型和三板斧,本身就是一份 runbook 的雏形——把它扩写成你团队自己的版本,是读完这一节最该做的下一步。