4.4 生产故障排查与应急处置


文档摘要

4.4 生产故障排查与应急处置 清单再严密(4.3),生产事故也躲不开——区别只在于事故频率和恢复速度。我管过两年 vLLM 生产服务,经历过凌晨被叫起来救火,也见过事故从「告警闪一下」恶化到「整集群雪崩」的全过程。这一节不写「报错对照表」那种条目堆叠(速查附录那种东西救不了真事故),而是把生产里真正高频、真正让人头大的几类故障,按「现象 → 诊断 → 处置 → 根因修复」的实战流程讲透。每个故障我都尽量给真实案例,让你能对号入座。 读完之后,你应该能在事故现场(凌晨 3 点、领导盯着、用户在投诉)快速完成「现象归类 → 指标定位 → 应急止血 → 事后根因」这套动作,而不是慌乱地乱试参数。故障排查的本质是「快速收敛假设」,这一节教的就是怎么收敛。

4.4 生产故障排查与应急处置

清单再严密(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/环境问题;如果只在真实流量下复现,是容量/调度问题。这一步能省掉大量瞎猜。

```mermaid flowchart TD A[告警/投诉] --> B[第一斧: 看日志] B --> C{日志有明显报错?} C -->|是| D[按报错定位] C -->|否| E[第二斧: 看 metrics 三指标] E --> F{指标形态} F -->|waiting 涨| G[过载类] F -->|cache_usage 顶| H[池满类] F -->|preemption 涨| H F -->|正常| I[第三斧: 最小复现] D --> J[应急止血] G --> J H --> J I --> J J --> K[事后根因修复] ```

下面按故障类型展开。

故障一:CUDA out of memory(最常见,但根因五花八门)

现象:日志抛 RuntimeError: CUDA out of memory. Tried to allocate X.XX GiB,服务可能崩溃或某个请求失败。

诊断:OOM 不是单一原因,必须分清是哪一种:

  1. 启动时 OOM:权重 + 初始 KV 池就超了显存。根因是 gpu-memory-utilization 设太高,或权重比预期大(量化版本错)。处置:降低 utilization(0.95→0.85),或核对权重量化格式。
  2. 运行时偶发 OOM:跑了一阵才崩。根因是激活峰值或碎片超出预算,常见于长 prompt 突发涌入。处置:降 max-num-seqs,或开 --swap-space 给 preemption 兜底。
  3. 请求被拒但不崩服务:日志显示 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

现象:启动时报 CUDA driver version is insufficient for CUDA runtime version,服务起不来。

诊断:宿主机 NVIDIA 驱动版本低于 vLLM/PyTorch 要求的 CUDA Runtime 版本。这是纯环境问题,不是参数问题。

处置

  1. 查当前驱动:nvidia-smi | grep "Driver Version"
  2. 查需要的驱动版本:vLLM 文档或 PyTorch CUDA 版本对应表(如 CUDA 12.1 需要驱动 ≥ 530)。
  3. 升级驱动:apt install nvidia-driver-535(或对应版本),重启机器。

真实案例:某次新机器到货,开发同事用旧镜像启动,报这个错。检查发现驱动是 525,但镜像里 PyTorch 编译用了 CUDA 12.1(要 530+)。:驱动升级要重启机器,生产环境要协调维护窗口,不能临时升级。预防:机器交付时就预装达标驱动,镜像构建时锁定 CUDA 版本,二者做兼容性矩阵。

故障三:tensor parallel size 不能整除注意力头数

现象:启动报 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%,业务侧反馈「卡」。

诊断:这类故障最难,因为没有显性错误。按下面顺序排查:

  1. 是否误开 --enforce-eager:禁用 CUDA Graph 会损失 10%~20% 吞吐。检查启动参数,生产应关 eager。这是最常见的吞吐杀手,很多人调试时开了忘了关。
  2. 是否启用了 chunked prefill:长 prompt 场景不开 chunked prefill,TTFT 会爆,间接拖垮吞吐。检查 --enable-chunked-prefill
  3. GPU 利用率(nvidia-smi 的 gpu_util)是否低:如果 <60%,说明 GPU 在等数据/在调度,不是在算。可能原因:batch 太小(max-num-seqs 太低)、网络/客户端请求慢、CPU 调度瓶颈。
  4. 是否 preemption 频繁vllm:preemption_count 涨得快说明 KV 池不够,请求在换出换进,吞吐崩。处置:扩大 KV 池(提 utilization)或降并发(降 max-num-seqs)。
  5. 是否 batch 没拼起来:连续批处理依赖请求同时到达,如果客户端是顺序请求(一个完了再发下一个),batch 永远是 1,吞吐起不来。处置:客户端并发发送,或加请求队列。

真实案例:某次压测吞吐只有预期的 40%,团队调了一周参数没用。最后发现是压测工具的客户端并发数设成了 1——服务端一切正常,是压测自己没把请求并发发出来。教训:排查吞吐问题要先排除「客户端侧瓶颈」,再看服务端。GPU 利用率是分水岭——GPU 满载但吞吐低,是算力/模型问题;GPU 不满载,是调度/IO 问题,方向完全不同。

故障五:首 token 延迟(TTFT)P99 飙升

现象:业务侧反馈「点一下要等好几秒才出第一个字」,metrics 显示 vllm:time_to_first_token_seconds P99 飙到几秒。

诊断:TTFT 高几乎都是 prefill 慢或队列堆积:

  1. 未开 chunked prefill 且有长 prompt:一个 32k prompt 的 prefill 要几百毫秒,期间所有 decode 被阻塞。处置:开 --enable-chunked-prefill,调小 --max-num-batched-tokens(如 8192→4096)让 prefill 切更细。
  2. num_requests_waiting 持续 >0:请求排队等 prefill,处置是扩容或限流。
  3. CUDA Graph 重新录制:batch size 剧烈跳变会让 CUDA Graph 反复录制,每次录制耗时。处置:限制 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 长度分布调,不是默认值就完事。

故障六:响应乱码 / 输出质量突然下降

现象:服务正常返回,但输出是乱码、重复、或质量明显下降。

诊断

  1. 量化格式不匹配--quantization awq 跑了 GPTQ 权重,或反过来。验证:核对权重目录下的 quantization_config.jsonconfig.json,与启动参数一致。
  2. dtype 不匹配--dtype float16 跑 BF16 权重可能溢出,输出 NaN 或乱码。处置:用 --dtype auto 或匹配权重的 dtype。
  3. tokenizer 与模型版本不一致:模型升级了但 tokenizer 没跟上,分词错位。处置:核对 tokenizer 版本与模型匹配。
  4. 上下文超长被截断:prompt 超过 max-model-len 被截断,模型看到的是不完整上下文,输出偏题。处置:业务侧做长度校验,或提高 max-model-len。
  5. KV Cache 量化(FP8)质量损失:开了 --kv-cache-dtype fp8,超长上下文精度下降。处置:精细任务关闭 KV 量化,做 A/B 评估。

真实案例:某次模型升级后用户反馈「答非所问」,排查发现新版本模型 config 里 chat_template 变了,但前端还在用旧模板拼 prompt,模型看到的是错位输入。处置:同步前端的 chat_template。启示:模型升级时,tokenizer、chat_template、特殊 token 都要一起验证,不能只看「能跑」。

故障七:多机部署 NCCL 通信失败

现象:多机部署启动时报 NCCL 相关错误(NCCL errorNCCL communicator initialization failed),或运行中卡死。

诊断

  1. 网络不通:节点间防火墙挡了 NCCL 端口(默认动态分配的一段端口)。处置:开放节点间所有 NCCL 需要的端口范围,或固定端口。
  2. 环境变量缺失:多机要设 NCCL_SOCKET_IFNAME(指定网卡)、NCCL_IB_DISABLE(无 IB 时禁用)、VLLM_HOST_IP(节点对外 IP)。处置:用统一启动脚本(如 Ray cluster)管理这些环境变量。
  3. 驱动/CUDA 版本不一致:多机版本不一致会让 NCCL 协议失败。处置:所有节点镜像完全一致。
  4. 拓扑问题:跨 NUMA、跨交换机延迟过高。处置:把 NCCL 通信绑到同一个 NUMA/交换机域。

真实案例:某次多机部署 1 号节点能起,2 号节点起不来,报 NCCL timeout。查了一天发现是 2 号节点的防火墙规则比 1 号严格,挡了 NCCL 端口。:多机问题调试成本极高,建议先用 nccl-test 工具单独验证节点间通信,再起 vLLM。

应急处置的优先级原则

具体故障之外,更重要的一个能力是「事故现场的优先级判断」。凌晨被告警叫醒时,你的目标不是「修好根因」,而是「尽快止血恢复服务」。优先级顺序是:

  1. 保服务可用:先扩容、限流、回滚、切备用,让大部分用户能用上服务。哪怕降级(如关掉某些功能、降低 max-num-seqs 损失吞吐),也要先恢复。
  2. 保数据不丢:不要让事故扩大到数据损坏(如权重被覆盖、配置被错改)。事故现场禁止做不可逆操作。
  3. 保留现场证据:在重启/回滚前,dump 一份日志、metrics、core dump。这些是事后根因的唯一依据。:很多人急着重启救火,重启后现场没了,根因永远查不清。
  4. 根因修复:服务稳定后再深入查根因,写复盘文档,落地长期修复。

一个我反复强调的原则:事故现场禁止「试参数」。看到 OOM 就临时改参数重启,是事故扩大的常见原因——改错了服务直接起不来,原本能跑的也跑了。临时改动必须经过测试,不能在生产现场试。如果一定要临时改,先在备用实例上验证,再切流。

把故障排查能力工程化

最后说一点:故障排查能力不应该只存在于「老员工脑子里」,要工程化成团队资产。我团队里有几样东西,每个都对应一类能力沉淀:

  • 故障手册(runbook):每个常见故障一页文档,「现象 → 诊断命令 → 处置步骤」,oncall 任何同事都能照着做。
  • 告警自带诊断链接:每个告警里带上「相关 runbook 链接」「相关 metrics 大盘链接」,省去现场翻找。
  • 复盘文档库:每次 P0/P1 事故写复盘,归档到团队知识库,新人入职必读。不复盘的事故等于白踩坑
  • 定期演练:季度做一次「人为注入故障」的演练(chaos engineering),验证监控和 runbook 是否真的有效。

故障排查能力的成熟度,不在于「能不能解决」,而在于「能不能被复制」。能让新人在三个月内具备老人七成的排障能力,靠的就是这套工程化沉淀。这一节给的故障类型和三板斧,本身就是一份 runbook 的雏形——把它扩写成你团队自己的版本,是读完这一节最该做的下一步。


作者与出处
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 运行报错的菜鸟的小龙虾 转发
评论区 (0)
U