首页 运维干货用 Prometheus + Grafana 打造 vLLM 推理服务监控大盘

用 Prometheus + Grafana 打造 vLLM 推理服务监控大盘

运维派隶属马哥教育旗下专业运维社区,是国内成立最早的IT运维技术社区,欢迎关注公众号:yunweipai
领取学习更多免费Linux云计算、Python、Docker、K8s教程关注公众号:马哥linux运维

1. 问题背景

先说一个真实的排障场景。

某公司用 vLLM 自部署了一个 7B 模型,挂在公司内部的代码助手服务后面,白天内部开发同事用着都挺正常。某天下午开始,陆续有人反馈”回复变慢了”“首字要等十几秒”。值班同学登录服务器,能看到的只有 vLLM 的访问日志:

INFO:     10.42.7.19:53214 - "POST /v1/chat/completions HTTP/1.1" 200 OK
INFO:     10.42.7.22:48910 - "POST /v1/chat/completions HTTP/1.1" 200 OK

全是 200,看不出任何问题。GPU 呢?跑了一下 nvidia-smi,利用率 98%,显存占用 91%。到底是”正常的忙”还是”快撑不住了”?排队排了多长?慢是在排队慢、prefill 慢,还是 decode 慢?没有一个能回答,因为除了日志和一次性的 nvidia-smi 快照,什么历史数据都没有。

最后花了两个小时人工压测复现,才确认是某个新接入的批量任务把并发打满,KV Cache 接近打满后调度器开始频繁抢占,TTFT(首 token 延迟)从 300ms 恶化到 10s 以上。等业务方限了速,问题恢复。但复盘时大家达成了一个共识:下次不能再靠 nvidia-smi 截图排障了。

传统 Web 服务的监控三板斧——QPS、响应时间、错误率——覆盖不了 LLM 推理服务的特殊性:

  • 延迟不是一个数字。一次推理请求的延迟至少拆成三段:排队时间、首 token 时间(TTFT)、每个后续 token 的时间(TPOT/ITL)。用户体感的”慢”可能来自任何一段,处理方式完全不同。
  • 容量不由 CPU/内存决定。vLLM 的吞吐上限主要由 KV Cache 显存和 GPU 算力决定,传统的”内存用了 60% 还很安全”完全失效。
  • 请求量不能只看 QPS。一个 100 token 的请求和一个 100K token 的长上下文请求,成本差几个数量级。真正要盯的是 token 吞吐量。
  • 请求成功不代表体验正常。请求最终 200 了,但可能在队列里等了 30 秒才轮到执行。

这篇文章要解决的就是这件事:给 vLLM 推理服务搭一套 Prometheus + Grafana 监控,从采集、存储、展示到告警一次配齐,并且讲清楚每张图怎么看、出问题时怎么用这套大盘做完整的闭环排查。

2. 适用场景

这套方案适合以下环境:

维度适用情况
推理框架vLLM(OpenAI 兼容 server,即 vllm.entrypoints.openai.api_server 方式启动)
部署形态裸金属/虚拟机 + systemd、Docker 单容器、Kubernetes Deployment 均可
实例数量单实例到多副本负载均衡均可
监控系统已有 Prometheus 直接加采集;没有的话文中给最小化搭建步骤
GPUNVIDIA GPU(GPU 侧指标用 DCGM exporter,AMD 卡文中会说明差异)

不太适合的情况:

  • 纯调商用 API(OpenAI、通义、豆包等)的团队,你的监控重心在网关侧而不是推理引擎侧;
  • 用 TGI、TensorRT-LLM、SGLang 的,思路一致,但指标名要按各自文档替换,不能照抄文中的 vllm: 前缀指标。

一个前提要说清楚:文中所有 vLLM 指标名以主流版本的实际暴露为准。vLLM 迭代很快,不同版本之间指标有过改名、增删(例如 swap 相关指标在新引擎版本里已经去掉,部分新版本用 kv_cache_usage 相关命名替代了老的 gpu_cache_usage 命名)。所以文中每个环节都强调同一件事:先 curl /metrics 拿到你环境里真实的指标清单,再对照着配,不要盲抄。

3. 核心知识点

在动手之前,把几个必须搞清楚的概念一次性讲透。后面所有配置和排查都建立在这上面。

3.1 vLLM 的请求生命周期

理解监控指标之前,先理解请求在 vLLM 里经历了什么:

客户端请求
   │
   ▼
┌─────────────────┐
│  Waiting 队列    │  ← 请求到达,但当前批次放不下(KV Cache 不够 / 达到 max_num_seqs)
└─────────────────┘
   │  调度器每个 step 决定哪些请求进入执行
   ▼
┌─────────────────┐
│  Running 集合    │  ← Prefill(处理输入 prompt)→ Decode(逐 token 生成)
└─────────────────┘
   │  生成结束 / 达到 max_tokens / 被抢占
   ▼
返回客户端

几个关键点:

  1. 调度以 step 为单位。vLLM 采用连续批处理(continuous batching),每个 decode step 都会动态调整批次里的请求,请求随时可以加入正在执行的批次,不必等整批结束。这就是 num_requests_running 是波动的、而不是固定批次大小的原因。
  2. KV Cache 是硬约束。每个在跑的请求都要占用 KV Cache 显存块(PagedAttention 按 block 分配)。KV Cache 用完时,调度器要么等新请求进不来(表现为 waiting 堆积、TTFT 飙升),要么抢占正在跑的请求(表现为个别请求延迟出现台阶式跳变)。
  3. 抢占行为分版本。老引擎版本支持把被抢占请求的 KV 换出到 CPU 内存(swap/recompute),num_requests_swapped、cpu_cache_usage_perc 就是看这个的;V1 引擎实现有变化,部分指标已不再暴露。你的版本有没有这些指标,以 /metrics 实际输出为准,没有就跳过对应面板。

3.2 vLLM 暴露指标的方式

vLLM 的 OpenAI 兼容 server 默认在同一端口的 /metrics 路径暴露 Prometheus 格式指标:

  • 默认端口 8000(–port 可改),指标路径 /metrics。个别较新版本支持为指标单独指定监听端口,是否有该参数、参数叫什么,以你环境的 python -m vllm.entrypoints.openai.api_server –help 实际输出为准;
  • 启动参数带 –disable-log-stats 时,统计日志和 Prometheus 指标一起关闭,这是最常见的”采不到数据”原因;
  • 指标内部由 prometheus_client 库实现。个别精简镜像没装齐依赖时 /metrics 会报错或为空,部署后要第一时间验证;
  • 指标默认带 model_name label,值来自 –served-model-name(没指定时是模型路径)。多模型混部时,这是区分模型的维度。

指标类型上需要区分的两类:

类型含义vLLM 中的例子查询方式
Counter单调递增累计值vllm:request_success_total、vllm:generation_tokens_total必须配 rate() 才有意义
Gauge瞬时值vllm:num_requests_running、vllm:gpu_cache_usage_perc直接查即可
Histogram分桶计数vllm:time_to_first_token_seconds 系列_bucket + rate() + histogram_quantile() 算分位数

新手最常犯的错误是直接画 counter 的原值(一条只会上跳的斜线),或者对 gauge 用 rate()(永远是 0)。记住这个映射:看”发生了多少、多快”用 counter + rate;看”现在有多少”用 gauge;看”分布/分位数”用 histogram

3.3 每个关键指标的含义

以下按用途分组解释。指标名前缀 vllm: 在原始暴露和 PromQL 中都要带上。

负载与并发:

指标类型含义关注点
vllm:num_requests_runningGauge正在执行的请求数是否顶到 max_num_seqs
vllm:num_requests_waitingGauge等待调度的请求数持续大于 0 即容量不足信号
vllm:num_requests_swappedGauge被换出的请求数(老引擎)>0 说明发生过抢占,新版本可能无此指标

内存与缓存:

指标类型含义关注点
vllm:gpu_cache_usage_percGaugeKV Cache 显存使用率,0~1长期 >0.9 意味着调度压力极大
vllm:cpu_cache_usage_percGaugeCPU 端换出缓存使用率(老引擎)同上,版本相关

KV Cache 使用率是 vLLM 最重要的单一指标,相当于传统数据库里的 buffer pool 使用率 + 连接数的合体。它逼近 1 的时候,新请求必然排队,在跑的请求可能被抢占。

延迟(histogram 族,每个都有 _bucket / _sum / _count 三个后缀):

指标含义对应体感
vllm:time_to_first_token_seconds从请求到达至吐出第一个 token“点下去多久开始出字”
vllm:time_per_output_token_seconds平均每个输出 token 耗时“出字之后流得快不快”
vllm:e2e_request_latency_seconds端到端总时长整个请求完成时间
vllm:request_queue_time_seconds在 waiting 队列里的时间慢如果集中在这段,是容量问题
vllm:request_prefill_time_secondsprefill 阶段耗时长上下文请求的算力开销集中在这
vllm:request_decode_time_secondsdecode 阶段耗时生成速度问题

queue/prefill/decode 三段之和约等于端到端延迟(不含网络)。分位数高不要只说”慢了”,要落到这三段里看慢在哪一段,这是后面排查章节的核心方法。

吞吐与结果:

指标类型含义
vllm:prompt_tokens_totalCounter累计输入 token 数
vllm:generation_tokens_totalCounter累计输出 token 数
vllm:request_success_totalCounter成功完成的请求数,带 finished_reason label(stop/length/abort 等)
vllm:request_prompt_tokensHistogram每请求输入 token 分布
vllm:request_generation_tokensHistogram每请求输出 token 分布

finished_reason=”length” 占比高,说明大量请求被 max_tokens/上下文窗口截断,这是配置或业务侧信号,不是故障;finished_reason=”abort” 占比升高通常对应客户端大量断连或超时取消,往往就是”慢到用户等不及”的前兆。

新版本还有 prefix cache 命中率相关的 counter(名称以实际环境为准),如果你的场景有大量公共 system prompt,这个指标值得配监控——命中率高能显著压低 TTFT。

3.4 GPU 本身的指标从哪里来

node_exporter 不包含 GPU 指标。GPU 侧要看,主流方案是 NVIDIA 官方的 DCGM exporter(dcgm-exporter),默认在 9400 端口暴露 /metrics,常用指标:

指标含义
DCGM_FI_DEV_GPU_UTILGPU 利用率(%)。注意:这是 kernel 级占用时间比,“有 kernel 在跑就算忙”,不等于算力用满
DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_FREE显存已用/可用(MiB)
DCGM_FI_DEV_GPU_TEMP核心温度(℃)
DCGM_FI_DEV_POWER_USAGE功耗(W)
DCGM_FI_DEV_SM_CLOCKSM 时钟(MHz)
DCGM_FI_PROF_PIPE_TENSOR_ACTIVETensor Core 流水线活跃比(profiling 指标,需要 exporter 开启对应组)

几个实践提醒:

  • DCGM_FI_DEV_GPU_UTIL 高不代表算力用满。LLM decode 阶段是访存密集型,利用率 100% 但吞吐不高是常态,要结合 token 吞吐一起看,不要单看这个指标下结论。
  • 温度逼近降频阈值(多数卡 85~90℃ 开始 throttling)时,SM_CLOCK 会掉下来、同样负载下 token 吞吐变低。这种”莫名变慢”只看 vLLM 指标看不出来,必须 GPU 指标在场。
  • dcgm-exporter 的指标分组(field group)是可配置的,默认组和 profiling 组暴露的指标集不同。具体暴露哪些,以你实际部署的 exporter 的 /metrics 输出为准,缺指标就检查它的配置。
  • K8s 环境下 dcgm-exporter 一般以 DaemonSet 部署,Pod 级 GPU 关联会带 pod / namespace label(不同部署方式 label 名有差异,如 exported_pod),配置前先看实际 label。
  • AMD 卡没有 DCGM,对应方案是 AMD 的 exporter(如 amd-smi / ROCm 相关 exporter),思路一致,指标名替换。

3.5 Prometheus 拉模型的两个工程含义

Prometheus 是 pull 模型,这带来两个直接工程后果:

  1. vLLM 实例必须在 Prometheus 可达的网络上暴露 /metrics。跨网段、跨 VPC、有防火墙的场景,安全组只放行 Prometheus 服务器到推理节点指标端口(8000/9400/9100)的 TCP,不要图省事开公网。
  2. 精度由 scrape_interval 决定。默认 1 分钟太粗:排队堆积往往是秒级尖刺,1 分钟粒度会把它抹平;但 1 秒级又没必要且浪费。LLM 推理监控建议 10~15s 采集间隔,全文示例统一用 15s。另外记住一句话:rate() 的窗口至少是 4 倍 scrape_interval,否则曲线会出现空洞——15s 间隔配 rate(…[1m]) 是下限,[5m] 比较稳。

3.6 Grafana 大盘的组织原则

大盘不是指标的大杂烩。一个能用来排障的 vLLM 大盘,按”从结论到细节”分四排:

  1. 第一排(健康概览):实例存活数、QPS、TTFT P99、KV Cache 最高水位。一眼判断”要不要紧张”。
  2. 第二排(负载):running/waiting 并发、token 吞吐(输入/输出分开)。判断”忙不忙、忙得值不值”。
  3. 第三排(延迟分解):TTFT / TPOT / E2E 分位数、queue/prefill/decode 分段延迟。判断”慢在哪一段”。
  4. 第四排(资源):GPU 利用率、显存、温度、功耗,node_exporter 的 CPU/内存/网卡。判断”瓶颈在不在硬件”。

再加一个贯穿全大盘的模板变量(实例/模型下拉框)。后面第 6 章按这个结构逐面板给 PromQL。

4. 整体实施思路

先看全景架构,明确数据从哪来、到哪去:

┌─────────────────────────── 推理节点 ───────────────────────────┐
│                                                                 │
│  vLLM server (:8000/metrics)   dcgm-exporter (:9400/metrics)   │
│         │                              │                        │
└─────────┼──────────────────────────────┼────────────────────────┘
          │ scrape (15s)                 │ scrape (15s)
          ▼                              ▼
┌────────────────────── Prometheus (:9090) ──────────────────────┐
│   job: vllm          job: gpu          job: node (node_exporter)│
│                        │                                        │
│            ┌───────────┴───────────┐                            │
│            ▼                       ▼                            │
│      Grafana (:3000)        Alertmanager (:9093) ──► 钉钉/邮件/webhook
└─────────────────────────────────────────────────────────────────┘

实施顺序固定为七步,每步都有独立验证点,任何一步验证不过就不进入下一步:

步骤内容验证点
1确认 vLLM 自身指标可访问curl /metrics 返回 vllm: 指标
2确认 GPU/主机指标可访问curl 9400/9100 返回指标
3Prometheus 添加采集配置并加载/targets 全部 UP
4数据完整性检查指标浏览器能查到关键指标
5Grafana 建大盘每个面板有数据且单位正确
6配置告警规则并联通告警通道测试告警能送达
7压测验证大盘有效性人为制造压力,曲线按预期变化

这个顺序的原则是先保证”数据在”,再谈”图好看”,最后谈”告警准”。很多团队一上来就导大盘 JSON,结果一半面板 No data,又回头查采集,浪费半天。

没有 Prometheus 的话,最小化部署(以二进制为例):

# 下载 prometheus 发行包后
groupadd --system prometheus 2>/dev/null
useradd -s /sbin/nologin -r -g prometheus prometheus 2>/dev/null
tar xvf prometheus*.linux-amd64.tar.gz -C /usr/local/
ln -s /usr/local/prometheus-*/prometheus /usr/local/bin/prometheus
ln -s /usr/local/prometheus-*/promtool /usr/local/bin/promtool
mkdir -p /etc/prometheus /var/lib/prometheus
chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus

配套 systemd 单元 /etc/systemd/system/prometheus.service

[Unit]
Description=Prometheus
Wants=network-online.target
After=network-online.target

[Service]
User=prometheus
Group=prometheus
ExecReload=/bin/kill -HUP $MAINPID
ExecStart=/usr/local/bin/prometheus \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/var/lib/prometheus \
--storage.tsdb.retention.time=30d \
--web.listen-address=0.0.0.0:9090
Restart=on-failure

[Install]
WantedBy=multi-user.target

ExecReload 这行别省,后面热加载配置要用 systemctl reload prometheus。Grafana 同理,官方仓库装包或用 docker 起一个都行,不在本文展开;生产环境强烈建议 Grafana 挂持久卷并定期备份其数据库文件(第 12 章回滚部分会用到)。

5. 环境假设与配置前检查

5.1 环境假设

本文以下述环境为例,组件版本仅示意,低一个小版本通常不影响,差异点文中会标注:

组件版本(示意)地址(示意)
GPU 服务器Ubuntu 22.04, A100-80G, 驱动 5xx10.0.1.21
vLLM0.x 系列 OpenAI server10.0.1.21:8000
dcgm-exporter容器方式10.0.1.21:9400
Prometheus2.x10.0.2.10:9090
Grafana10.x10.0.2.10:3000
Alertmanager0.2x10.0.2.10:9093

5.2 配置前检查清单

动手改任何配置之前,先把下面的检查全部跑一遍。每项给出目的、命令、预期与异常判断。

检查 1:vLLM 进程与端口监听

目的:确认服务活着、端口和预想一致。

ss -lntp | grep 8000

预期输出(进程名可能因启动方式不同而变化):

LISTEN  0  2048  0.0.0.0:8000  0.0.0.0:*  users:(("python",pid=327641,fd=60))

异常判断:没有输出 = 服务没起或端口不是 8000。先 systemctl status 或 docker ps 找到真实端口,后面的采集配置要以真实端口为准。

检查 2:指标端点内容

目的:确认指标真的在暴露,且拿到你环境的真实指标清单。

curl -s http://10.0.1.21:8000/metrics | grep '^vllm' | head -40

预期:能看到 vllm:num_requests_running、vllm:time_to_first_token_seconds_bucket 等一组行。

异常判断分三种:

  • 返回 404 或连接被拒:vLLM 版本过老(极老版本没有 /metrics)或启动时带了 --disable-log-stats。检查启动命令行:
   bashcat /proc/$(pgrep -f 'vllm.entrypoints' | head -1)/cmdline | tr '\0' ' '
  • 返回 200 但没有任何 vllm: 行:检查 prometheus_client 是否在 vLLM 环境内(python -c "import prometheus_client; print(prometheus_client.__version__)"),精简镜像可能缺依赖。
  • 只有 python_*process_* 等运行时指标:同上的依赖问题或统计被禁用。

把这条命令的完整输出留存一份,它就是本次实施的”指标基线清单”,后面 Grafana 面板用到的每个指标名都要能在这份清单里找到,找不到的面板直接删或者改名。

检查 3:网络连通性(在 Prometheus 服务器上执行)

目的:确认采集端能到达被采端,避免配完才发现网络不通。

curl -s -o /dev/null -w '%{http_code}\n' http://10.0.1.21:8000/metrics
curl -s -o /dev/null -w '%{http_code}\n' http://10.0.1.21:9400/metrics
curl -s -o /dev/null -w '%{http_code}\n' http://10.0.1.21:9100/metrics

预期均为 200。返回 000 一般是 TCP 不通(安全组/防火墙未放行),用 nc -zv 10.0.1.21 8000 进一步确认;返回 403 检查反向代理层是不是把 /metrics 挡住了。

检查 4:推理节点的出方向访问控制

目的:确认放行策略只针对监控服务器。如果节点上有 iptables/firewalld:

iptables -L -n | head -30
firewall-cmd --list-all 2>/dev/null

需要放行的是:Prometheus 服务器 IP → 8000/9400/9100。示例(firewalld,按源 IP 精确放行):

firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.2.10/32" port port="9100" protocol="tcp" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.2.10/32" port port="9400" protocol="tcp" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.2.10/32" port port="8000" protocol="tcp" accept'
firewall-cmd --reload

风险提醒:修改防火墙属于高风险操作,做错会切断业务流量。执行前记录当前规则(iptables-save > ~/iptables-backup-$(date +%F).rules),改完立刻从 Prometheus 服务器重跑检查 3 验证,同时从业务侧验证推理接口正常。云主机改的是安全组,逻辑相同:只授权监控机 IP,端口 8000 是业务端口本身已放行,9400/9100 只对监控机开,永远不对 0.0.0.0/0 开。

检查 5:Prometheus 侧资源余量

目的:新增 job 前先确认现有存储和抓取负载有余量,避免采集把监控系统自己压垮。

# 当前活跃时间序列数
curl -s 'http://localhost:9090/api/v1/status/tsdb' | python3 -c "import sys,json;d=json.load(sys.stdin);print('active series:',d['data']['headStats']['numSeries'])"
# 磁盘余量
df -h /var/lib/prometheus

经验估算:单个 vLLM 实例贡献的时间序列在几百条量级(histogram 的 bucket 是大头),加 dcgm-exporter(每 GPU 几十条)和 node_exporter(几百到上千条),单节点增量约 1~2k series。按每 series 每 scrape 约 1~2 字节/分钟的粗估,50 个节点、15s 间隔、保 30 天,磁盘预留至少 50GB 起。规模再大就该考虑 VictoriaMetrics/Thanos 了,文中不展开。

6. 实战步骤

步骤 1:以可监控的方式运行 vLLM

目的:确保 vLLM 启动参数没有关掉指标,并让指标端口稳定可预期。

裸机/systemd 启动命令示例:

python -m vllm.entrypoints.openai.api_server \
  --model /data/models/qwen2.5-7b-instruct \
  --served-model-name qwen2.5-7b \
  --host 0.0.0.0 --port 8000 \
  --max-model-len 16384 \
  --gpu-memory-utilization 0.90

要点:

  • 不要加 --disable-log-stats。有些人嫌日志吵把它关了,代价是监控一起没了。要降日志量应该去降日志级别,不是关统计。
  • --served-model-name 建议固定指定。它决定指标里 model_name label 的值,不指定的话 label 里是一长串本地路径,大盘变量下拉框会很难看。
  • --gpu-memory-utilization 影响 KV Cache 的总量:这个值越大,KV Cache 块越多、能同时容纳的并发越高,但显存 OOM 风险也越大。监控里 KV Cache 使用率的分母由它决定,调这个参数前后,大盘上的使用率含义会变,复盘时别忘了这一点。

Docker 启动时注意端口映射别漏:

docker run -d --name vllm --gpus all \
  -p 8000:8000 \
  -v /data/models:/data/models \
  --restart unless-stopped \
  vllm/vllm-openai:latest \
  --model /data/models/qwen2.5-7b-instruct \
  --served-model-name qwen2.5-7b \
  --max-model-len 16384

验证

curl -s http://localhost:8000/metrics | grep -c '^vllm'

预期:返回一个大于 50 的整数(具体数量随版本变化)。如果为 0,回到第 5 章检查 2 处理,不要继续往下走。

步骤 2:部署 GPU 与主机指标采集

目的:vLLM 指标只能告诉你”引擎视角”的状态,硬件视角必须补上。

GPU 侧用 dcgm-exporter,Docker 方式最省事:

docker run -d --name dcgm-exporter \
  --gpus all --pid=host \
  -p 9400:9400 \
  --restart unless-stopped \
  nvcr.io/nvidia/k8s/dcgm-exporter:latest
curl -s http://localhost:9400/metrics | grep DCGM_FI_DEV_GPU_UTIL

预期:能看到 DCGM_FI_DEV_GPU_UTIL{gpu="0",...} 数值 的行。如果报 “Failed to initialize DCGM”,一般是宿主机 DCGM 服务或驱动版本问题:宿主机 nvidia-smi -q 正常但 exporter 起不来时,尝试宿主机执行 nv-hostengine 相关服务的重启,或换用与驱动版本匹配的 dcgm 版本。

主机侧装 node_exporter(二进制):

tar xvf node_exporter-*.linux-amd64.tar.gz -C /usr/local/
useradd -s /sbin/nologin -r nodeexp 2>/dev/null
ln -s /usr/local/node_exporter-*/node_exporter /usr/local/bin/node_exporter

systemd 单元 /etc/systemd/system/node_exporter.service

[Unit]
Description=Node Exporter

[Service]
User=nodeexp
ExecStart=/usr/local/bin/node_exporter --web.listen-address=:9100
Restart=on-failure

[Install]
WantedBy=multi-user.target

启动并验证:

systemctl daemon-reload
systemctl enable --now node_exporter
curl -s http://localhost:9100/metrics | head -5

K8s 环境的差异:vLLM Pod 用 PodMonitor/ServiceMonitor(装了 prometheus-operator 的话)或 Pod annotation 方式采集;dcgm-exporter 用官方 DaemonSet。核心指标名不变,变的只是发现方式,见步骤 3 的 K8s 配置分支。

步骤 3:配置 Prometheus 采集

目的:把三类 job 挂进 Prometheus,并做到配置可维护。

采用 file_sd_configs 文件服务发现管理 target,好处是加减推理节点只改 JSON 文件,不动主配置,也方便 Ansible 下发。

主配置 /etc/prometheus/prometheus.yml 关键片段:

global:
  scrape_interval:15s
evaluation_interval:15s

rule_files:
-/etc/prometheus/rules/*.yml

alerting:
alertmanagers:
    -static_configs:
        -targets: ['localhost:9093']

scrape_configs:
-job_name:vllm
    file_sd_configs:
      -files:
          -/etc/prometheus/targets/vllm-*.json
        refresh_interval:30s

-job_name:gpu
    file_sd_configs:
      -files:
          -/etc/prometheus/targets/gpu-*.json
        refresh_interval:30s

-job_name:node
    file_sd_configs:
      -files:
          -/etc/prometheus/targets/node-*.json
        refresh_interval:30s

target 文件 /etc/prometheus/targets/vllm.json(多实例就列多条,label 按机房/卡型打,供大盘分组过滤):

[
  {
    "targets":["10.0.1.21:8000","10.0.1.22:8000"],
    "labels":{
      "service":"llm-inference",
      "idc":"idc-a"
    }
}
]

/etc/prometheus/targets/gpu.json

[
  {
    "targets": ["10.0.1.21:9400", "10.0.1.22:9400"],
    "labels": { "service": "llm-inference" }
  }
]

/etc/prometheus/targets/node.json

[
  {
    "targets": ["10.0.1.21:9100", "10.0.1.22:9100"],
    "labels": { "service": "llm-inference" }
  }
]

K8s 分支:如果 Prometheus 是 prometheus-operator 管的,给 vLLM Pod 加 ServiceMonitor 更规范;如果是原生 kubernetes_sd_configs,用 annotation 发现,参考配置:

scrape_configs:
  -job_name:vllm-k8s
    kubernetes_sd_configs:
      -role:pod
    relabel_configs:
      -source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action:keep
        regex:"true"
      # 用 annotation 里的端口替换 __address__ 的端口部分,保留 Pod IP
      -source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
        action:replace
        regex:([^:]+)(?::\d+)?
        replacement:$1:$2
        target_label:__address__
      -source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
        action:replace
        target_label:__metrics_path__
        regex:(.+)
      -source_labels: [__meta_kubernetes_pod_name]
        action:replace
        target_label:pod

对应 Pod annotation:

metadata:
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "8000"
    prometheus.io/path: "/metrics"

生效方式

# 先语法校验,永远在 reload 之前跑
promtool check config /etc/prometheus/prometheus.yml
systemctl reload prometheus

promtool check config 输出 SUCCESS 才 reload;reload 失败 Prometheus 会保留旧配置继续跑,不会挂,但你要立刻修正。file_sd 的 target JSON 改完不需要 reload,最多 30s(refresh_interval)自动生效。

验证

curl -s http://localhost:9090/api/v1/targets | python3 -c "
import sys, json
d = json.load(sys.stdin)
for t in d['data']['activeTargets']:
    print(t['labels'].get('job'), t['scrapeUrl'], t['health'], t['lastError'])
"

预期:三行均为 up 且 lastError 为空。异常处理见第 9 章排查路径 A。

步骤 4:数据完整性核对

目的:在开画之前确认每个要用的指标确实进了库。Grafana 面板 No data 的成因九成在这一步没核对。

在 Prometheus 的 Status → Targets 里点进 vllm job 的 target,或用 API 拉全量指标名去重:

curl -s http://localhost:9090/api/v1/label/__name__/values | python3 -c "
import sys, json
names = json.load(sys.stdin)['data']
print('\n'.join(n for n in names if n.startswith('vllm')))
"

对照第 3.3 节的指标表逐个勾。同时验证”数据在动”而不是”只有名字”——随便选一个 counter 连续查两次:

curl -s 'http://localhost:9090/api/v1/query?query=vllm:prompt_tokens_total' | python3 -m json.tool | grep value
sleep 30
curl -s 'http://localhost:9090/api/v1/query?query=vllm:prompt_tokens_total' | python3 -m json.tool | grep value

两次数值有增长(有流量的前提下)才算采集闭环完成。如果 target 是 UP 但一条 vllm: 时序都查不到,通常是 scrape 配置里加了 relabel 把指标过滤了,或者代理层改写过响应体。

步骤 5:Grafana 大盘搭建

Grafana 侧先把数据源加上:Connections → Data sources → Prometheus → URL 填 http://10.0.2.10:9090,Save & test 必须显示成功。

下面逐面板给出配置。每个面板四个要素:PromQL、单位/画法、这张图回答什么问题、异常长什么样。面板类型未注明的默认 Time series。所有查询里 [5m] 窗口配合 15s 采集间隔使用;实例过滤统一用后面的 $instance 变量。

5.1 健康概览排

P1 实例存活数(Stat)

count(up{job=~"vllm.*"} == 1)
  • 阈值配色:等于总实例数绿色,减少变黄/红。
  • 回答:“死了几个实例”。这是值班第一眼看的面板。
  • 注意 job 用正则 vllm.* 是为了兼容裸机 vllm 和 K8s vllm-k8s 两种 job 名。

P2 请求速率(Stat + 迷你图)

sum(rate(vllm:request_success_total{instance=~"$instance"}[5m]))
  • 单位:reqps
  • 回答:“现在每秒完成多少请求”。
  • 注意这是完成速率不是到达速率,积压时完成速率会失真地低,要和 waiting 一起看。

P3 TTFT P99(Stat)

histogram_quantile(0.99,
  sum by (le) (rate(vllm:time_to_first_token_seconds_bucket{instance=~"$instance"}[5m]))
)
  • 单位:s
  • 回答:“最差用户体验有多差”。P99 而不是平均值:LLM 场景长尾很厚,均值会骗人。
  • 异常:突然抬升一个数量级,基本对应排队或 prefill 恶化,看第三排定位。

P4 KV Cache 最高水位(Stat)

max(vllm:gpu_cache_usage_perc{instance=~"$instance"})
  • 单位:percentunit(指标本身是 0~1,Grafana 用 percentunit 自动显示 92% 这种格式)。
  • 回答:“容量红线摸到没有”。>0.9 持续出现就该处理(扩容或限流),这是 vLLM 版的”磁盘快满了”。
  • 若你的版本此指标名不存在,按实际清单换成对应 KV Cache 指标(如 kv cache usage 相关命名),含义相同。

5.2 负载排

P5 并发构成:running vs waiting(Time series)

sum by (instance) (vllm:num_requests_running{instance=~"$instance"})
sum by (instance) (vllm:num_requests_waiting{instance=~"$instance"})
  • 第二条查询 Legend 设为 waiting {{instance}},并给 waiting 系列配黄色。
  • 回答:“实例是被喂饱了还是在挨饿”。
  • 典型形态解读:
    • running 平稳 + waiting 常年 0:健康,或容量过剩;
    • running 顶到平台值 + waiting 有锯齿:调度饱和,进来的请求都在等,扩容信号;
    • waiting 长期贴 0 但 running 很低:不是引擎的瓶颈,去查客户端/网关/网络。

P6 token 吞吐(Time series,两条输入输出)

sum(rate(vllm:prompt_tokens_total{instance=~"$instance"}[5m]))
sum(rate(vllm:generation_tokens_total{instance=~"$instance"}[5m]))
  • 单位:short 或自定义 tokens/s
  • 回答:“真正搬了多少货”。同样 QPS 下,输入 token 吞吐暴涨往往是有人开始灌长上下文/RAG 长 prompt,KV Cache 压力将先于 QPS 报警。
  • 输出吞吐几乎而输入吞吐涨:批量离线任务接入的典型指纹。

P7 完成原因占比(Pie chart 或 Percent stacked)

sum by (finished_reason) (rate(vllm:request_success_total{instance=~"$instance"}[10m]))
  • 回答:“请求都怎么结束的”。
  • length 占比突然升高:用户 prompt 或生成长度撞上了 max_model_len/max_tokens 限制,业务侧调整信号;
  • abort 升高:客户端等不及在断连,通常和 TTFT 恶化同步出现,这是最有先行价值的告警素材之一。

5.3 延迟分解排

P8 TTFT 分位数族(Time series,三条查询)

histogram_quantile(0.50, sum by (le) (rate(vllm:time_to_first_token_seconds_bucket{instance=~"$instance"}[5m])))
histogram_quantile(0.90, sum by (le) (rate(vllm:time_to_first_token_seconds_bucket{instance=~"$instance"}[5m])))
histogram_quantile(0.99, sum by (le) (rate(vllm:time_to_first_token_seconds_bucket{instance=~"$instance"}[5m])))
  • 单位 s,Legend 分别 P50 / P90 / P99
  • 回答:“首字延迟的分布形状”。P50 和 P99 同涨多半是全局变慢(算力/排队);只有 P99 涨,多半是少量超长请求在挤兑批次。

P9 TPOT P99(Time series)

histogram_quantile(0.99,
  sum by (le) (rate(vllm:time_per_output_token_seconds_bucket{instance=~"$instance"}[5m]))
)
  • 单位:s(TPOT 通常在几十毫秒量级,Grafana 会按数值自动缩写显示为 ms)。
  • 回答:“出字速度”。TPOT 恶化而 TTFT 正常,方向是 decode 阶段——batch 内 KV 访问压力、显存带宽、或被抢占后重算。

P10 分段延迟均值对比(Time series,三条查询)

sum(rate(vllm:request_queue_time_seconds_sum{instance=~"$instance"}[5m]))
  / sum(rate(vllm:request_queue_time_seconds_count{instance=~"$instance"}[5m]))
sum(rate(vllm:request_prefill_time_seconds_sum{instance=~"$instance"}[5m]))
  / sum(rate(vllm:request_prefill_time_seconds_count{instance=~"$instance"}[5m]))
sum(rate(vllm:request_decode_time_seconds_sum{instance=~"$instance"}[5m]))
  / sum(rate(vllm:request_decode_time_seconds_count{instance=~"$instance"}[5m]))
  • 回答:“总延迟被哪一段吃掉”。这是排查章节最依赖的一张图。
  • 判读规则:queue 段涨 → 容量/调度问题;prefill 段涨 → 请求变长或 prefill 算力不足;decode 段涨 → 生成阶段压力。三段都不涨但 E2E 涨 → 时间花在引擎外(网关、网络、客户端),去查接入层。
  • 个别版本没有这三项直方图(版本相关,见步骤 4 的清单核对),没有就把这张图删掉,用 TTFT−queue 之类的组合近似,或直接靠 P8/P9 定位。

5.4 资源排

P11 GPU 利用率(Time series)

avg by (gpu) (DCGM_FI_DEV_GPU_UTIL{instance=~"$instance"})
  • 单位 percent(DCGM 这个指标本身已是 0~100,用 percent 不是 percentunit)。
  • label 名 gpu 以实际 exporter 为准,K8s DaemonSet 模式下可能是 id/UUID 加 pod 关联 label,先在指标浏览器确认。
  • 回答:“卡有没有活”。低利用率 + 高 waiting 是反直觉组合,出现时优先怀疑调度器被大请求阻塞或客户端打不满。

P12 GPU 显存(Time series)

DCGM_FI_DEV_FB_USED{instance=~"$instance"}
  • 单位:mibibytes(DCGM 该指标单位是 MiB)。
  • 和 P4 的区别:FB_USED 包含模型权重 + KV Cache + 激活值,是物理显存;KV Cache 水位只是引擎视角的缓存池占用。物理显存接近卡容量而 KV 水位不高,说明 gpu_memory_utilization 或权重本身占了大头,出 OOM 风险找前者,吞吐问题找后者。

P13 GPU 温度与功耗(Time series,双 Y 轴)

DCGM_FI_DEV_GPU_TEMP{instance=~"$instance"}
DCGM_FI_DEV_POWER_USAGE{instance=~"$instance"}
  • 回答:“有没有热降频”。温度爬过 85℃ 且 SM_CLOCK(可加第四条查询)回落,同时 token 吞吐下降,就是降频导致的慢,跟流量无关。机房空调故障的早期发现位。

P14 主机网络与负载(Time series)

rate(node_network_receive_bytes_total{instance=~"$node",device!="lo"}[5m])
node_load5{instance=~"$node"}
  • 单位分别 Bpsshort
  • 回答:“瓶颈是否根本不在 GPU”。多模态大文件上传时网卡打满、CPU 被 tokenizer 占爆(超长 prompt 的 tokenization 在 CPU 上开销不小)都在这张图现形。

步骤 6:告警规则

目的:把大盘上最重要的几条曲线变成”不用人看”的告警。原则:只告”需要人立刻行动的”,容量趋势类走周报而不是告警。

规则文件 /etc/prometheus/rules/vllm-alerts.yml

groups:
  -name:vllm-availability
    rules:
      -alert:VLLMInstanceDown
        expr:up{job=~"vllm.*"}==0
        for:1m
        labels:
          severity:critical
        annotations:
          summary:"vLLM 实例 {{ $labels.instance }} 采集失败"
          description:"连续 1 分钟抓取失败。先区分是进程挂了还是仅监控链路断了:curl 业务接口 /v1/models 判断。"

      -alert:VLLMQueueBacklog
        expr:maxby(instance)(vllm:num_requests_waiting)>32
        for:5m
        labels:
          severity:warning
        annotations:
          summary:"{{ $labels.instance }} 等待队列持续堆积"
          description:"waiting 超过 32 已持续 5 分钟。结合 KV Cache 水位与 running 数判断是容量问题还是异常流量。阈值 32 需按你的业务基线调整。"

-name:vllm-capacity
    rules:
      -alert:VLLMKVCachePressure
        expr:vllm:gpu_cache_usage_perc>0.9
        for:5m
        labels:
          severity:warning
        annotations:
          summary:"{{ $labels.instance }} KV Cache 水位持续 >90%"
          description:"调度器即将无法接纳新请求。评估:限流、降低单实例 max_num_seqs、扩容。"

      -alert:VLLMPreemptionRisk
        expr:vllm:num_requests_swapped>0
        for:2m
        labels:
          severity:warning
        annotations:
          summary:"{{ $labels.instance }} 出现请求换出/抢占"
          description:"KV Cache 已不足以满足所有在跑请求。注意:此规则仅适用于仍暴露 swapped 指标的版本,新版引擎请删除此规则或用 KV 水位告警替代。"

-name:vllm-latency
    rules:
      -alert:VLLMTTFTPHigh
        expr:|
          histogram_quantile(0.99,
            sum by (le) (rate(vllm:time_to_first_token_seconds_bucket[5m]))
          ) > 5
        for:5m
        labels:
          severity:warning
        annotations:
          summary:"集群 TTFT P99 持续高于 5s"
          description:"查看延迟分解面板确认慢在 queue/prefill/decode 哪一段。5s 阈值必须按业务 SLA 调整,本文示例值不构成建议。"

      -alert:VLLMAbortRateHigh
        expr:|
          sum(rate(vllm:request_success_total{finished_reason="abort"}[5m]))
            / sum(rate(vllm:request_success_total[5m])) > 0.05
        for:5m
        labels:
          severity:warning
        annotations:
          summary:"abort 完成占比 >5%"
          description:"通常是客户端超时断连,往往是延迟恶化的先行指标。"

-name:gpu-health
    rules:
      -alert:GPUHighTemp
        expr:DCGM_FI_DEV_GPU_TEMP>87
        for:5m
        labels:
          severity:warning
        annotations:
          summary:"GPU {{ $labels.gpu }} 温度持续 >87℃"
          description:"接近降频阈值,检查机房制冷/风扇。持续升温将导致吞吐自动下降。"

      -alert:GPUMemoryNearCapacity
        expr:|
          DCGM_FI_DEV_FB_USED /
          (DCGM_FI_DEV_FB_USED + DCGM_FI_DEV_FB_FREE) > 0.97
        for:10m
        labels:
          severity:info
        annotations:
          summary:"GPU 显存物理占用 >97%"
          description:"vLLM 预留模式下这是预期内的常态(权重+KV Cache 预留),仅在你混部了多进程时才有行动意义。"

几条工程判断写在这:

  • VLLMInstanceDown 的告警文案里强调”先 curl 业务接口”,因为 up==0 有两种世界:进程真死了(要救火),或只是 Prometheus 到节点的网络/防火墙动了(不用救火但要修监控)。告警内容里引导第一诊断动作,能省值班同学一半时间。
  • VLLMKVCachePressure 用 vllm:gpu_cache_usage_perc > 0.9 不加 sum,逐实例评估,summary 里能直接定位是哪台。
  • 阈值全部是示例量级。上线第一步:拉两周历史数据画分位数,把阈值钉在”基线 P95 之外”的位置,否则要么天天误报要么永远不报。
  • GPU 显存那条默认 info 级,原因说明写在 description 里,避免值班群刷屏。

生效

promtool check rules /etc/prometheus/rules/vllm-alerts.yml
systemctl reload prometheus
# 确认规则已被加载
curl -s http://localhost:9090/api/v1/rules | python3 -c "import sys,json;d=json.load(sys.stdin);print([g['name'] for g in d['data']['groups']])"

Alertmanager 最小路由示例 /etc/alertmanager/alertmanager.yml

global:
  resolve_timeout:5m

route:
receiver:default
group_by: [alertname, instance]
group_wait:30s
group_interval:5m
repeat_interval:4h
routes:
    -matchers:
        -severity="critical"
      receiver:oncall-critical
      repeat_interval:30m

receivers:
-name:default
    webhook_configs:
      -url:http://10.0.2.10:8060/dingtalk/webhook/send
        send_resolved:true
-name:oncall-critical
    webhook_configs:
      -url:http://10.0.2.10:8060/dingtalk/webhook/critical
        send_resolved:true

注意 webhook 地址只是占位示例,按你实际的告警网关(如 PrometheusAlert、AlertManager 钉钉/企微桥接服务)替换。告警网关的凭据不要写在 alertmanager.yml 明文提交进 git,用环境变量注入或配置管理工具收敛。

校验并生效:

amtool check-config /etc/alertmanager/alertmanager.yml
systemctl reload alertmanager

验证告警链路:不要等真故障来验证告警。主动制造一条:

# 临时停一台 vLLM 验证 critical 链路(选择无流量实例,业务低峰执行)
curl -s -X POST http://localhost:9093/api/v2/alerts -H 'Content-Type: application/json' -d '[{
  "labels": {"alertname": "VLLMInstanceDown", "instance": "test-only", "severity": "critical"},
  "annotations": {"summary": "测试告警,收到请忽略"}
}]'

这是注入测试告警,不是真停服务,零风险,链路通了再考虑要不要做真实演练。

步骤 7:大盘变量与收尾

目的:让大盘支持实例/模型过滤,避免几十张图各查各的。

Dashboard Settings → Variables 添加:

变量名Query说明
instancelabel_values(vllm:num_requests_running, instance)实例下拉,多选 + All
modellabel_values(vllm:num_requests_running, model_name)模型下拉,多模型混部时必备
nodelabel_values(node_uname_info, instance)主机指标面板用的实例列表

变量勾选 Multi-value + Include All。所有查询里的 {instance=~"$instance"} 写法对多值自动展开成正则,无需额外处理。

两个收尾动作:

  1. annotations 挂发布事件:如果有 CI/CD 或部署平台,把 vLLM 实例重启/配置变更事件以 Grafana Annotation 形式写入,大盘上会显示竖线。”延迟变化点是不是发布点”这类判断 30 秒就有答案。API 方式:
curl -s -X POST http://localhost:3000/api/annotations \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $GRAFANA_TOKEN" \
  -d '{"dashboardId":null,"panelId":null,"time":'$(date +%s%3N)',
       "text":"deploy: qwen2.5-7b v0.6.3 -> v0.8.5","tags":["deploy"]}'

(Grafana 版本不同 annotation API 字段有差异,新版走 annotations 数据集方式,以实际版本文档为准。)

  1. 大盘 JSON 导出入库:Dashboard settings → JSON Model 复制保存进配置仓库,走 git 管理。这是 Grafana 最简单的版本回滚手段,第 12 章依赖它。

7. 常用命令速查

按使用频率排序,全部为可直接执行的真实命令。监控机 IP、节点 IP 按环境替换。

指标端点类:

# 查看 vLLM 全量指标(原始)
curl -s http://<vllm-ip>:8000/metrics > /tmp/vllm-metrics.txt && wc -l /tmp/vllm-metrics.txt

# 只看核心 gauge(当前瞬时状态速查)
curl -s http://<vllm-ip>:8000/metrics | grep -E '^vllm:(num_requests|gpu_cache|cpu_cache)'

# 指标端点响应耗时(排除 /metrics 本身变慢导致的采集超时)
curl -s -o /dev/null -w 'http_code=%{http_code} time_total=%{time_total}s\n' http://<vllm-ip>:8000/metrics

# GPU exporter 指标
curl -s http://<gpu-ip>:9400/metrics | grep -E 'DCGM_FI_DEV_(GPU_UTIL|FB_USED|GPU_TEMP|POWER_USAGE)'

# node_exporter 存活
curl -s -o /dev/null -w '%{http_code}\n' http://<node-ip>:9100/metrics

Prometheus 运维类:

# 配置/规则语法校验(任何 reload 前必跑)
promtool check config /etc/prometheus/prometheus.yml
promtool check rules /etc/prometheus/rules/vllm-alerts.yml

# 热加载
systemctl reload prometheus

# target 健康状态一览
curl -s http://localhost:9090/api/v1/targets | python3 -c "
import sys,json
for t in json.load(sys.stdin)['data']['activeTargets']:
    print(t['labels'].get('job'), t['scrapeUrl'], t['health'], t.get('lastError',''))"

# 即席查询(等价于 UI 输入框)
promtool query instant http://localhost:9090 'max(vllm:gpu_cache_usage_perc)'

# 查询某指标近 1 小时是否存在(返回时序数)
promtool query range http://localhost:9090 'vllm:num_requests_waiting' --since=1h --step=1m | head

# 当前活跃时间序列总量
curl -s http://localhost:9090/api/v1/status/tsdb | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['headStats']['numSeries'])"

# 各 job 序列数(定位谁在膨胀基数)
curl -s http://localhost:9090/api/v1/status/tsdb | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['headStats']['seriesByLabels'][:5])"

# TSDB 快照备份(需启动参数 --web.enable-admin-api)
curl -X POST http://localhost:9090/api/v1/admin/tsdb/snapshot

告警类:

# 当前触发中的告警
curl -s http://localhost:9093/api/v2/alerts | python3 -c "
import sys,json
for a in json.load(sys.stdin):
    print(a['status']['state'], a['labels'].get('alertname'), a['labels'].get('instance'))"

# 注入测试告警验证通道(零风险)
amtool alert add VLLMInstanceDown instance=test-only severity=critical \
  --alertmanager.url=http://localhost:9093

# 静默某实例告警 2 小时(发布窗口用)
amtool silence add alertname=VLLMInstanceDown instance=10.0.1.21:8000 \
  --duration=2h --comment='vLLM 升级窗口' --author=ops \
  --alertmanager.url=http://localhost:9093

K8s 环境补充:

# 确认 Pod annotation 生效
kubectl -n llm get pods -o jsonpath='{range .items[*]}{.metadata.name}{"  "}{.metadata.annotations.prometheus\.io/scrape}{"\n"}{end}'

# ServiceMonitor 方式时确认端点对象
kubectl -n llm get servicemonitors,endpoints 2>/dev/null

# 从 Prometheus Pod 内直接验证抓取(排除网络路径差异)
kubectl -n monitoring exec -it sts/prometheus-k8s-0 -c prometheus -- \
  wget -qO- http://<vllm-pod-ip>:8000/metrics | grep -c '^vllm'

vLLM 进程侧:

# 确认启动参数(重点看有没有 --disable-log-stats)
ps -eo pid,cmd | grep 'vllm.entrypoints' | grep -v grep

# vLLM 统计日志(与 Prometheus 互为交叉验证,见第 8 章)
journalctl -u vllm --since "10 min ago" | grep -i throughput | tail -20

# GPU 现场快照(与 DCGM 指标交叉验证)
nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu,power.draw --format=csv

8. 日志与指标观察方法

8.1 vLLM 统计日志:免费的第二数据源

vLLM 默认每秒在 stderr 打一行吞吐统计(不同版本字段略有增减,以实际输出为准):

INFO 09-22 14:31:05 [metrics.py:412] Avg prompt throughput: 812.6 tokens/s, Avg generation throughput: 155.3 tokens/s, Running: 12 reqs, Waiting: 0 reqs, GPU KV cache usage: 63.1%, Prefix cache hit rate: 38.4%

这一行和 Prometheus 指标是同一套引擎内部数据渲染出来的,价值有两个:

  1. 交叉验证。大盘显示 KV Cache 63%、Running 12,而日志同一时刻说 63.1%/12 reqs,说明采集链路可信;长期对不上,优先怀疑中间有 relabel/多实例聚合口径问题。
  2. 无监控环境的一手排障材料。systemd 部署时:
journalctl -u vllm -f | grep -oE 'Running: [0-9]+ reqs|Waiting: [0-9]+ reqs|GPU KV cache usage: [0-9.]+%'

Docker 部署时 docker logs vllm --since 10m 2>&1 | tail -20

日志观察的三个习惯:

  • Waiting 字段出现非零就是事件起点。用它的时间戳去对齐 Prometheus 曲线,比盯着大盘肉眼找拐点快得多。
  • Prefix cache hit rate 骤降往往先于 TTFT 上升出现(缓存失效 → prefill 重做),比如网关侧改了 system prompt、或流量模型换了一批新会话。
  • 统计日志停在某一秒不再刷新:引擎主循环被卡(CUDA 错误、死锁、进程僵死)的强信号,比端口探测更早发现”活着但不动了”的实例。

8.2 访问日志观察

Uvicorn 访问日志配合网关日志,主要做指标做不了的事:按客户端维度归因。

# 找出过去一小时贡献请求最多的客户端
journalctl -u vllm --since "1 hour ago" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+ - "POST' \
  | cut -d: -f1 | sort | uniq -c | sort -rn | head -10

看到某个 IP 占了 80% 请求量、而 Prometheus 显示集群 waiting 堆积,归因链就闭合了:单客户端流量激增导致容量不足,下一步动作是和流量方谈限流或扩容,而不是无差别重启。

8.3 用指标做趋势观察的三个固定动作

  1. 周同比:Grafana 时间范围选 7 days ago,看 token 吞吐曲线上周同一天的位置,判断业务增长速率,为下一次扩容留提前量。
  2. 空载基线:记录集群无流量时 GPU 利用率、功耗的基线值。基线本身异常(比如空载利用率 40%)说明有隐形流量或异常进程。
  3. 发布对照:每次模型/引擎版本发布后 24 小时,用 annotation 竖线前后对比 TTFT/TPOT 分位数曲线,量化发布影响,这是推理引擎升级评估最重要的证据。

9. 排查路径

排查分两类:一类是监控体系自己出问题(大盘没数据、曲线异常但服务正常),另一类是用这套大盘排查服务问题。两类都要有固定路径,避免凭感觉。

9.1 大盘没数据:从数据流向后逐级收敛

原则:数据流经 vLLM 端点 → Prometheus 抓取 → 存储 → Grafana 查询,断在哪级,前面各级都是好的。从最靠近数据源的一端开始验证最省时间。

现象:某面板或全盘无数据
  │
  ├─ 1. curl <vllm>:8000/metrics 有 vllm: 指标吗?
  │     否 → 服务侧问题:进程挂了 / --disable-log-stats / 依赖缺失。看 9.1.1
  │     是 ↓
  ├─ 2. Prometheus /targets 里该 target 是 UP 吗?
  │     否 → 采集链路问题:网络/防火墙/端口配错/路径配错。看 9.1.2
  │     是 ↓
  ├─ 3. Explore 里能查到该指标吗?
  │     否 → relabel 误删指标名 / job 配错。看 9.1.3
  │     是 ↓
  ├─ 4. 面板查询里有数据但图上是 No data?
  │        → Grafana 侧:数据源选错、变量 $instance 未包含该实例、
  │          时间范围不对、查询里 label 拼写差异。

9.1.1 端点没有 vLLM 指标

# 进程在不在、启动参数里有没有禁用开关
ps -eo pid,cmd | grep 'vllm.entrypoints' | grep -v grep
# /metrics 的原始返回
curl -s http://localhost:8000/metrics | head -20

判断逻辑:进程消失 → 直接走服务故障流程(看 journalctl 最后 100 行找崩溃原因);进程在但 404 → 确认启动参数;返回 200 无 vllm 前缀 → python -c "import prometheus_client" 检查依赖。修复后等一个 scrape 周期再看 targets。

9.1.2 target DOWN

# 从 Prometheus 服务器复现抓取,看具体错误
curl -sv http://<vllm-ip>:8000/metrics -o /dev/null 2>&1 | tail -5
  • Connection refused:端口/进程问题,回 9.1.1;
  • 超时:安全组/防火墙,从监控机 nc -zv 验证端口,找网络修规则(改防火墙按高风险流程走);
  • 401/403:有认证代理,Prometheus 配置里补 basic_auth/authorization,凭据用文件引用不写明文;
  • UI target 里 lastError 字段永远先看。

9.1.3 target UP 但查不到指标名

/metrics 内容正常、target UP,指标浏览器里没有 vllm: 名字 → 九成是 relabel 配置动了 __name__ 或加了错误的 metric_relabel_configs。逐条检查 scrape_configs 里是否有 drop/keep 规则命中了你的指标,改完 reload。

9.2 有数据但曲线不对劲

曲线表现最可能原因验证方法
counter 面板画成一路上跳的斜线忘了 rate()查询里包 rate(...[5m])
rate 曲线锯齿状断续rate 窗口 ≤ 2× scrape_interval窗口放大到 ≥4× 间隔(15s 间隔用 ≥1m)
rate 曲线周期性归零服务在周期性重启查 process_start_time_seconds{job="vllm"} 是否在重置,或查 uptime
gauge 恒为 0 而日志说不是 0指标名版本不匹配(查了一个已改名/废弃的指标)按步骤 4 的清单核对真实指标名
分位数等于某个固定值不动查询里 sum by (le) 漏了 le修正聚合维度
只有部分实例有数据file_sd target 没加新实例 / 变量 All 未展开targets 数与实际实例数对账

9.3 用大盘排查服务变慢:完整闭环案例

用第 1 章背景里的真实事件完整走一遍闭环,每一步都写清依据。

① 现象:值班收到告警 VLLMTTFTPHigh(TTFT P99 > 5s 持续 5 分钟),用户侧反馈开始出字慢。

② 初步判断(只看第一、三排面板,60 秒内)

  • P1 实例存活:4/4,没有实例死亡 → 排除单点故障;
  • P4 KV Cache 水位:峰值实例 96%,其余 40% 左右 → 疑似单实例过载而非全局过载;
  • P10 分段延迟:queue 段从 0.2s 涨到 8s,prefill/decode 平稳 → 慢在排队,不在算力。

初步结论(待验证):流量在负载不均下把一台实例的调度容量打满,新请求全部排队。

③ 命令检查

# 该实例当前瞬时状态直接问引擎
curl -s http://10.0.1.22:8000/metrics | grep -E '^vllm:(num_requests|gpu_cache)'

输出确认:num_requests_waiting 41gpu_cache_usage_perc 0.96,与大盘一致。

# 该实例上是否出现抢占
curl -s http://10.0.1.22:8000/metrics | grep 'vllm:num_requests_swapped'

输出 vllm:num_requests_swapped 3 → 已发生换出,个别长请求会被重算,解释了部分用户”出字中间卡一下”的次级现象。

④ 关键指标复核(回答”为什么会压到一台”)

  • 按实例拆 QPS:
sum by (instance) (rate(vllm:request_success_total[5m]))

10.0.1.22 承接了 78% 的到达请求,其余三台合计 22% → 网关负载均衡失效或会话粘滞配置问题。

  • 按客户端归因(8.2 节命令):该实例访问日志 top1 客户端 IP 占 60%,且其请求的输入 token 巨大(vllm:request_prompt_tokens 直方图整体右移)→ 一台过载 = 负载不均 + 单一客户端长上下文流量叠加。

⑤ 根因定位

依据链:TTFT P99 告警 → queue 段占总延迟 90%(P10)→ 单实例 waiting/KV 双高(③)→ QPS 分布 78/22(④)→ 网关侧某后端健康检查抖动导致连接被钉在单台(网关日志确认)→ 叠加某业务方上线长文档批量解析任务。根因两个:网关负载均衡异常(直接原因)+ 无客户端级限流(放大因素)。

⑥ 修复方案(按风险和速度排序)

  1. 立即:对 top 客户端在网关侧临时限流(影响范围:仅该业务方的批量任务,实时业务不受影响);
  2. 当天:修复网关健康检查配置,恢复四台均分(变更窗口内灰度:先改权重观察 10 分钟再全量);
  3. 一周内:网关增加 per-key 限流与长上下文请求单独排队策略;评估把 max_num_seqs 与实例数按压测结果重调。

⑦ 验证结果:修复 1 后 15 分钟,waiting 从 41 → 0,KV 水位 96% → 58%;修复 2 后 QPS 分布回到 26/25/25/24,TTFT P99 回到 0.4s。大盘截图归档进事件单。

⑧ 回滚预案(本案当时准备的):若网关权重灰度后出现新异常(某实例再次被钉住),立刻把网关配置回滚为事件前版本(配置有 git 记录,nginx -t 校验后 reload,10 秒级回滚);限流规则误伤实时业务时,按 key 白名单即时豁免。

⑨ 复盘总结:三条改进——KV Cache 水位告警阈值从 0.95 收紧到 0.9(争取处置时间);新增按实例 QPS 偏斜度告警(max/avg > 2 持续 5 分钟);批量类任务统一走独立的低优先级模型副本,和实时业务物理隔离。

从这个案例提炼的通用决策树

服务变慢
  ├─ P1 实例数 < 预期?          → 走实例故障排查(进程/主机/GPU 掉卡)
  ├─ waiting 高 & KV 水位高?    → 容量/调度问题:查流量来源、负载分布、限流
  ├─ waiting 低 & queue 段低?   → 算力问题:
  │     ├─ prefill 段高  → 请求变长?prefix cache 命中率跌?(P6 + 日志)
  │     └─ decode 段高   → GPU 降频(P13 温度/时钟)?访存压力?
  └─ 三段都平稳但 E2E 高?       → 引擎外:网关、网络、客户端超时配置

10. 风险提醒

这套体系整体是旁路只读的,对推理服务本身侵入极低,但仍有几个必须点名的风险位:

10.1 采集对推理服务的影响(低但存在)

  • /metrics 抓取是轻量 HTTP 请求,单次毫秒级,15s 间隔可以忽略。真正要防的是手滑把 scrape_interval 配成 1s 并且实例数上百——引擎端点线程池和监控服务器自己都会难受。原则:LLM 场景 scrape_interval 不低于 10s。
  • 若给 vLLM 前置了带鉴权的网关,确认网关放行 /metrics 且该路径不占用业务限流额度。

10.2 时间序列基数失控

  • vLLM 指标本身基数可控(每实例数百 series),失控通常来自两类操作:给 target 加了高基数自定义 label(比如把请求级 ID、完整 pod IP 列表之类塞进 label),或者用 metric_relabel_configs 时写错正则导致意外保留/丢弃。所有 relabel 变更在预发先跑 promtool check config + 观察 status/tsdb 的 numSeries 变化再上生产。
  • numSeries 突增(单 job 一夜翻倍)本身就是告警素材,基数失控的下一步永远是存储和查询性能雪崩。

10.3 防火墙与安全组变更

第 5.2 节检查 4 已强调:改防火墙/安全组前备份现有规则,只对监控机 IP 定向放行指标端口,改完双向验证(监控链路 + 业务链路),且必须有回滚文件在手。这是本流程里唯一会直接动生产网络策略的环节,放在低峰窗口做。

10.4 告警风暴与错误阈值

  • 阈值拍脑袋(比如全网关统一 TTFT>5s)会制造误报,误报三次值班就会开始忽略第四条——告警信用是消耗品。上线节奏:先挂 warning 级别静默跑两周收集触发分布,再定 critical。
  • 维护/发布窗口必须配合 Alertmanager silence(第 7 章命令),否则每次例行重启都会全组告警。

10.5 敏感信息面

  • /metrics 无鉴权,内容包含模型名称、吞吐规模,属于可被外部利用的业务情报。永远不对公网暴露 8000 的 /metrics 路径(可以在网关层禁用该路径的外部访问)、9090、9093、3000。
  • Grafana admin 密码、告警网关 webhook token、Prometheus 若启用 basic_auth 的凭据:走密钥管理或权限 600 的本地文件,不进 git,不出现在截图里。
  • Alertmanager 的 amtool 静默、删除操作会影响值班感知,批量清理 silence 前先列出将到期的条目确认。

10.6 明确的高风险操作对照

本方案正常流程里不包含破坏性命令。但以下衍生操作按高风险管理,执行前必须有备份与回滚:

操作风险前置要求
删除 Prometheus 数据目录/缩短 retention 立即回收历史数据不可恢复,基线分析断档先做 TSDB snapshot
docker system prune 顺手清理可能删掉 dcgm-exporter 镜像/卷加 -f 前先 docker ps -a 确认范围
重启生产 vLLM 实例(为修监控参数)在途请求全部中断从 LB 摘除→等待 draining→再重启
kubectl delete pod 重建以刷新 annotation有状态风险低但会中断在途请求确认副本数>1,加 silence
Alertmanager 配置 reload静默/路由全量变化,可能漏报备份旧配置,amtool check 后执行
批量修改 target 文件(脚本下发)一个 JSON 语法错误导致全部 target 不加载file_sd 会拒绝坏文件,先在测试文件验证;下发脚本打印 diff 并保留旧文件副本

11. 验证方式

监控建完不压测等于没建。以下是完整验收清单,按顺序执行。

11.1 静态验收

# 1. 配置与规则全部通过校验
promtool check config /etc/prometheus/prometheus.yml && promtool check rules /etc/prometheus/rules/*.yml && amtool check-config /etc/alertmanager/alertmanager.yml

# 2. 三个 job 全部 target UP
curl -s http://localhost:9090/api/v1/targets | grep -c '"health":"up"'

# 3. 告警规则已加载且无解析错误
curl -s http://localhost:9090/api/v1/rules | python3 -c "import sys,json;d=json.load(sys.stdin);errs=[r for g in d['data']['groups'] for r in g['rules'] if r.get('health')!='ok'];print('bad rules:',len(errs))"

11.2 端到端数据验收

制造一段可控流量,观察大盘各排是否如期响应。vLLM 仓库自带压测脚本(老版本路径为 benchmarks/benchmark_serving.py,新版本提供 vllm bench serve 子命令,按你的版本选择):

# 示意命令,参数以脚本实际 --help 为准
python benchmark_serving.py \
  --backend vllm \
  --base-url http://10.0.1.21:8000 \
  --model /data/models/qwen2.5-7b-instruct \
  --dataset sharegpt \
  --num-prompts 500 \
  --request-rate 8

压测期间逐项核对(这就是验收用例):

检查项预期
P2 请求速率从 0 抬升到接近 --request-rate 设定值
P5 并发running 上升,waiting 是否出现取决于压力档位
P6 token 吞吐输入/输出吞吐同步抬升
P8/P10TTFT 上升可见;加大 request-rate 后 queue 段出现
P11/P12 GPU利用率、KV 水位随压力上升
告警压到 KV>0.9 时 VLLMKVCachePressure 应触发并在告警网关收到通知

压测本身也有风险:--request-rate 从低往高阶梯加(2→4→8→16),不要在业务时段对生产实例直接打高档位;若必须生产验证,选择灰度实例并提前 silence。压测结束确认 waiting 回落到 0、KV 水位回落到基线,才算环境恢复。

11.3 告警演练

# 1. 注入测试告警,确认送达(第 6/7 章命令)
# 2. 真实触发一次低危告警:临时把 VLLMKVCachePressure 阈值调到 0.01
#    reload 后观察告警进入 firing→送达,改回并 reload
# 3. 验证恢复通知:告警解除后 webhook 收到 resolved
# 4. 验证 silence:发布窗口内 critical 告警被正确压制

演练记录进验收单:告警从引擎状态变化到值班收到通知的端到端延迟 = evaluation_interval(15s) + for(5m) + group_wait(30s) + 网关投递,对告警类基本在 6 分钟内,值班 SOP 里要写清这个数字,避免”告警怎么这么慢”的误解。需要更快的路径(TTFT 尖刺类)把 for 缩到 2m 或加一条不等 for 的 info 级预警。

12. 回滚方案

监控体系回滚的总原则:监控故障不反噬业务。所有变更点都有独立回滚路径,互不牵连。

变更回滚方法耗时
Prometheus 主配置旧版本配置文件覆盖 + systemctl reload prometheus(reload 失败时旧配置本就还在运行)1 分钟
target 文件file_sd 目录内文件按版本目录切换(建议 targets/active/ + targets/backup/ 两目录,切换只改 symlink)秒级
告警规则规则文件 git revert + reload1 分钟
误报告警不碰配置,直接 amtool silence 压制,再择机调阈值秒级
防火墙放行iptables-restore < ~/iptables-backup-xxx.rules 或安全组回滚到快照1 分钟
Grafana 大盘JSON 从 git 恢复导入;Grafana 自带 Save 历史版本可回退分钟级
Grafana 整体备份的 grafana.db + provisioning 目录恢复10 分钟
vLLM 启动参数(为监控改的那部分)若因排查禁用类问题引入了参数变更,恢复原参数文件重启实例;该操作按”从 LB 摘除→draining→重启→回挂”流程执行分钟级/实例

Grafana 备份动作(写进例行 crontab):

# /usr/local/bin/grafana-backup.sh
#!/usr/bin/env bash
set -euo pipefail
SRC="/var/lib/grafana"
DEST="/backup/grafana/$(date +%F)"
mkdir -p "$DEST"
rsync -a --exclude=plugins "$SRC/" "$DEST/"
gzip -f "$DEST/grafana.db"
find /backup/grafana -name '*.db.gz' -mtime +14 -delete

脚本审查要点:路径变量带引号;排除 plugins 减小体积;自动清理 14 天前备份——注意这个 -delete 只作用于专用备份目录下匹配模式的路径,改动本脚本时务必保持这个边界,不要把范围扩散到数据源目录。

Prometheus 数据回滚没有”回滚”一说,只有灾难重建:TSDB snapshot 定期落到对象存储,重建环境时按 snapshot 恢复或直接接受历史丢失(监控数据 30 天以上价值的团队,应该升级到远端存储方案而不是靠 snapshot)。

13. 生产环境注意事项

  1. 监控自身的高可用:Prometheus 是单点时,它挂了没人告警。最低成本方案:给 Prometheus/Grafana/Alertmanager 所在主机配 node_exporter + 一台极简的”监控监控”(哪怕是台小机器跑 blackbox_exporter 探测 /-/healthy)。规模大则 Prometheus 双副本同配置对采。
  2. 保留期与成本:LLM 指标基数不小,30 天默认值先跑一个月看磁盘增速再定 retention;长期容量趋势只需要 5 分钟降采样数据,考虑 recording rules 预聚合关键查询(P8 的分位数族是典型:record: cluster:ttft_p99:5m 类规则能显著降低查询开销)。
  3. 变更纪律:prometheus.yml、rules、targets、alertmanager.yml、Grafana 大盘 JSON 五类文件全部进 git,任何变更走 PR + promtool/amtool check 的 CI 门禁。监控配置裸改没记录,事后追责和回滚都无从谈起。
  4. 时钟一致性:vLLM 日志时间戳和 Prometheus 时间轴对齐是交叉验证的前提,推理节点必须统一 NTP/chrony 校时,偏差超过 10s 的节点,日志对曲线就是误导。
  5. 多模型/多版本矩阵:混部多个模型时,大盘变量必须含 model 维度;不同 vLLM 版本混跑时指标名可能不一致(同一面板部分实例有数据部分没有),处理方式是先统一引擎版本再谈大盘兼容,而不是在 PromQL 里堆 or 兜底——兜底查询是排障时的陷阱,会让人误以为数据齐全。
  6. K8s 特有的坑:Pod 重建后 IP 变,file_sd 硬写 Pod IP 的方式在 K8s 必挂,必须走服务发现;StatefulSet 换 name、Deployment 滚动后 instance label 变化会导致历史曲线断裂,用稳定的 pod/deployment label 聚合可缓解;节点 GPU 掉卡时 dcgm-exporter 可能带病运行(该卡指标消失而非报错),用”GPU 数量 per node”的 count 告警兜底。
  7. runbook 挂进告警:每条告警的 annotation 里放对应排查小节的内部链接(第 9 章决策树截图也行)。值班同学半夜收到的不是指标名,是下一步动作。
  8. 容量规划闭环:大盘的 P4/P5/P6 三张图按周导出,和扩容评审绑定:KV 水位周峰值持续 >0.85、或 waiting 出现频率每周 >N 次即触发扩容评审。监控数据不进决策流程,大盘三个月后就会没人看。

14. 总结

回到最初的问题:vLLM 这种 LLM 推理服务,监控到底该怎么做。

指标体系上,抓住四条主线就够了:并发(running/waiting 看调度压力)、KV Cache 水位(看容量红线)、延迟三段分解(TTFT/queue/prefill/decode 看慢在哪)、token 吞吐(看真实负载)。GPU 指标是第四视角,负责回答”硬件在干什么”。任何单一指标都不足以下结论——利用率 98% 可能满载也可能访存空转,KV 96% 配 waiting=0 可能就是正常高水位,结论必须来自多指标交叉的证据链。

工程落地上的几个硬知识点:vLLM 的 --disable-log-stats 会连指标一起关;指标名跨版本有演化,永远以环境的 /metrics 输出为基线;PromQL 三件套纪律(counter 配 rate、分位数别忘 sum by (le)、rate 窗口 ≥4× 抓取间隔);node_exporter 没有 GPU 指标,GPU 靠 DCGM exporter;防火墙只定向放行监控机;阈值先观察基线再钉,别拍。

流程上的顺序不能乱:验端点 → 验网络 → 上采集 → 验入库 → 画大盘 → 配告警 → 压测验收 → 演练。每一步有独立验证点和回滚位。监控体系回滚的底线是它与业务解耦——它坏了最多瞎,不能把业务搞挂。

最后说一句价值观层面的:大盘的终点不是”好看”,而是被用在三件事上——值班处置(runbook 链接进告警)、发布评估(annotation 竖线对照分位数曲线)、容量决策(周水位数据驱动扩容评审)。做到这三件事被数据驱动,这套监控就回本了;只是挂在墙上,它迟早变成没人看的壁纸。

大模型推理监控告警实战:Prometheus、Grafana 与关键指标插图

马哥教育《大模型应用与工程实践》课程,全面培养学员独立设计、部署和运维生产级LLM推理系统的核心工程能力,想系统学习的宝子,扫码咨询。

大模型推理监控告警实战:Prometheus、Grafana 与关键指标插图1

本文链接:https://www.yunweipai.com/archives/49438

网友评论comments

发表回复

您的电子邮箱地址不会被公开。

暂无评论

Copyright © 2012-2022 YUNWEIPAI.COM - 运维派 京ICP备16064699号-6
扫二维码
扫二维码
返回顶部