08. 监控菜谱:可验证的 Prometheus 规则、PromQL、Dashboard 面板
谁该读这一篇? 准备搭 vLLM 生产监控的 SRE / 平台工程师;oncall 要预定义告警规则的同学;已经看完 SLO 章节,需要"动手版"的人。
前置阅读:
05-slo-and-observability.md(SLO 模型 + 4 大金信号——本节是它的"动手版",假设你已经理解 TTFT/TPOT 含义)。耗时: 约 20 分钟。
学完能: 1. 从当前
/metrics与源码生成指标清单,按 4 大金信号分桶。 2. 把本节模板替换为自己的 label/bucket/SLO,并用promtool验证。 3. 按"延迟 / 容量 / 缓存 / 故障"4 块组织 Grafana dashboard。 4. 在 oncall 30 秒内通过 3-5 个核心 panel 判断"系统现在到底怎么了"。当前复核(
b23bd73f540175f9e117eaee5029cd7d8df63964): Prometheus exposition 会给 counter 加_total,histogram 使用_bucket/_count/_sum;聚合时保留le,跨副本按目标标签聚合。示例阈值必须由本服务基线/SLO替换,复制 YAML 前先在 staging 用promtool和真实/metrics验证。
不解释为什么要监控(05-slo-and-observability.md 已讲)。这章给需在 staging 验证的工件模板。
1. vLLM 暴露的 Prometheus 指标全表
源码:vllm/vllm/v1/metrics/loggers.py 的 Gauge/Counter/Histogram 注册。指标前缀:vllm:
按用途分类:
1.1 Latency(histogram)
| Metric | 含义 |
|---|---|
vllm:time_to_first_token_seconds |
TTFT |
vllm:inter_token_latency_seconds |
相邻 token 间隔(ITL) |
vllm:request_time_per_output_token_seconds |
TPOT(per request 聚合) |
vllm:e2e_request_latency_seconds |
TTLT(不在上面 grep 出来,但 logger 里有) |
vllm:request_queue_time_seconds |
请求在 WAITING 队列的时间 |
vllm:request_prefill_time_seconds |
prefill 阶段时长 |
vllm:request_decode_time_seconds |
decode 阶段时长 |
vllm:request_inference_time_seconds |
inference 总时长 |
1.2 Traffic / Throughput
| Metric | 含义 |
|---|---|
vllm:num_requests_running (gauge) |
当前 running 请求数 |
vllm:num_requests_waiting (gauge) |
当前等待请求数 |
vllm:num_requests_waiting_by_reason (gauge) |
按等待原因分桶(KV/encoder/...) |
vllm:prompt_tokens_total (counter) |
累计 prompt token 数 |
vllm:prompt_tokens_by_source_total (counter) |
按当前 source enum 分 |
vllm:prompt_tokens_cached_total (counter) |
local + external cache 命中的 prompt token |
vllm:generation_tokens_total (counter) |
累计 generated token 数 |
vllm:iteration_tokens_total (histogram) |
每次 engine step 处理的 token 数分布 |
vllm:request_success_total (counter) |
按 finished_reason 分桶 |
1.3 Saturation(资源压力)
| Metric | 含义 |
|---|---|
vllm:kv_cache_usage_perc (gauge) |
KV cache 占用率(0-1) |
vllm:num_preemptions_total (counter) |
累计抢占次数 |
vllm:engine_sleep_state (gauge) |
引擎睡眠状态(多副本 elastic 用) |
1.4 Cache 命中
| Metric | 含义 |
|---|---|
vllm:prefix_cache_queries_total (counter) |
prefix cache 查询 token 数 |
vllm:prefix_cache_hits_total (counter) |
prefix cache 命中 token 数 |
vllm:external_prefix_cache_queries_total (counter) |
外部 KV connector 查询 token 数 |
vllm:external_prefix_cache_hits_total (counter) |
外部命中 token 数 |
vllm:mm_cache_queries_total (counter) |
多模态 encoder cache 查询 item 数 |
vllm:mm_cache_hits_total (counter) |
多模态 encoder cache 命中 item 数 |
1.5 KV block 生命周期(histogram,需配置非零 --kv-cache-metrics-sample)
| Metric | 含义 |
|---|---|
vllm:kv_block_lifetime_seconds |
block 从分配到回收的存活时间 |
vllm:kv_block_idle_before_evict_seconds |
block 空闲多久被踢 |
vllm:kv_block_reuse_gap_seconds |
同一 hash block 两次复用间隔 |
vllm:request_prefill_kv_computed_tokens |
prefill 实算(未命中)的 token |
1.6 Perf / Roofline 估算
源码锚点:vllm/v1/metrics/perf.py · PerfMetricsProm
源码:vllm/v1/metrics/perf.py 的 PerfMetricsProm。这些指标不是硬件计数器,而是 vLLM 按模型结构、scheduled token 和阶段估算的每 GPU FLOPs / bytes,用来近似算 MFU / MBU(见 10-gpu-utilization-and-tail-latency.md)。
| Metric | 含义 |
|---|---|
vllm:estimated_flops_per_gpu_total (counter) |
每 GPU 估算累计 FLOPs |
vllm:estimated_read_bytes_per_gpu_total (counter) |
每 GPU 估算累计读取字节 |
vllm:estimated_write_bytes_per_gpu_total (counter) |
每 GPU 估算累计写入字节 |
1.7 故障
| Metric | 含义 |
|---|---|
vllm:corrupted_requests_total (counter) |
仅在 VLLM_COMPUTE_NANS_IN_LOGITS 启用时,logits 含 NaN 的请求 |
1.8 配置 / Info
| Metric | 含义 |
|---|---|
vllm:cache_config_info (gauge) |
block_size / num_blocks 等配置 |
vllm:lora_requests_info (gauge) |
当前激活 LoRA 数 |
vllm:request_prompt_tokens (histogram) |
单请求 prompt 长度分布 |
vllm:request_generation_tokens (histogram) |
单请求 generation 长度分布 |
vllm:request_max_num_generation_tokens (histogram) |
最大 generation token |
vllm:request_params_n (histogram) |
请求 n 参数分布 |
vllm:request_params_max_tokens (histogram) |
请求 max_tokens 分布 |
2. PromQL 公式模板(先核对标签与基线)
2.1 SLO 公式
# TTFT p99,按 model 维度
histogram_quantile(0.99,
sum by (model_name, le) (
rate(vllm:time_to_first_token_seconds_bucket[5m])
)
)
# TPOT p99
histogram_quantile(0.99,
sum by (model_name, le) (
rate(vllm:request_time_per_output_token_seconds_bucket[5m])
)
)
# 错误率(按 finish_reason)
sum by (model_name) (rate(vllm:request_success_total{finished_reason="abort"}[5m]))
/
sum by (model_name) (rate(vllm:request_success_total[5m]))
2.2 容量公式
# 队列深度 p95(waiting 数的 5min p95)
quantile_over_time(0.95, vllm:num_requests_waiting[5m])
# KV cache 利用率(瞬时)
vllm:kv_cache_usage_perc
# 抢占速率(每秒抢占次数)
rate(vllm:num_preemptions_total[5m])
# 每 step 平均 token 数(GPU 利用率代理)
histogram_quantile(0.5,
sum by (le) (rate(vllm:iteration_tokens_total_bucket[5m]))
)
# 吞吐:generated token / 秒
sum by (model_name) (rate(vllm:generation_tokens_total[1m]))
2.3 Prefix cache 命中
# Block-level 命中率(5min 滑动)
sum by (model_name) (rate(vllm:prefix_cache_hits_total[5m]))
/
clamp_min(sum by (model_name) (rate(vllm:prefix_cache_queries_total[5m])), 1e-9)
# Token-level 节省比例
sum(rate(vllm:prompt_tokens_cached_total[5m]))
/
clamp_min(sum(rate(vllm:prompt_tokens_total[5m])), 1e-9)
2.4 多副本 / DP 健康度
# 实例间 QPS 偏差(max / min)
max by (model_name) (rate(vllm:request_success_total[1m]))
/
clamp_min(min by (model_name) (rate(vllm:request_success_total[1m])), 1e-9)
# 单实例 KV 利用率比集群中位数高 30%(routing 不均)
vllm:kv_cache_usage_perc
> on(model_name) (1.3 * quantile by(model_name)(0.5, vllm:kv_cache_usage_perc))
2.5 近似 MFU / MBU
下面把 <peak_flops_per_gpu> 与 <peak_bytes_per_second> 留作部署参数;它们必须来自当前 GPU SKU、精度/稀疏模式与实测/厂商规格,不能把某个 H100 数字硬编码到通用 dashboard。
# 每 GPU 估算 TFLOPS
rate(vllm:estimated_flops_per_gpu_total[1m]) / 1e12
# 近似 MFU
rate(vllm:estimated_flops_per_gpu_total[1m]) / <peak_flops_per_gpu>
# 每 GPU 估算带宽 GB/s
(
rate(vllm:estimated_read_bytes_per_gpu_total[1m])
+ rate(vllm:estimated_write_bytes_per_gpu_total[1m])
) / 1e9
# 近似 MBU
(
rate(vllm:estimated_read_bytes_per_gpu_total[1m])
+ rate(vllm:estimated_write_bytes_per_gpu_total[1m])
) / <peak_bytes_per_second>
读法:
- decode 场景 MBU 低、running 也低:batch 太小或流量被打散。
- decode 场景 MBU 低、running 不低:查 CUDA Graph / async scheduling / CPU bubble。
- decode 场景 MBU 高、TPOT 仍高:多半是 KV 墙或通信墙。
- prefill 场景 MFU 高、TTFT 仍高:prefill compute-bound 到顶,考虑量化、加 prefill 池或 P/D 分离。
3. 告警规则模板(staging 验证后使用)
以下 YAML 用演示阈值展示语法,不可直接部署;先替换阈值、labels 和 for,再运行 promtool check rules:
groups:
- name: vllm-slo
interval: 30s
rules:
- alert: VLLMTTFTHigh
expr: |
histogram_quantile(0.99,
sum by (model_name, le) (rate(vllm:time_to_first_token_seconds_bucket[5m]))
) > 1.0
for: 5m
labels:
severity: warning
annotations:
summary: "TTFT p99 > 1s for {{ $labels.model_name }}"
description: "Sustained 5 minutes. Check: queue depth, KV pressure, GPU sat."
- alert: VLLMTPOTHigh
expr: |
histogram_quantile(0.99,
sum by (model_name, le) (rate(vllm:request_time_per_output_token_seconds_bucket[5m]))
) > 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "TPOT p99 > 100ms for {{ $labels.model_name }}"
- alert: VLLMAbortRateHigh
expr: |
sum by (model_name) (rate(vllm:request_success_total{finished_reason="abort"}[5m]))
/ sum by (model_name) (rate(vllm:request_success_total[5m]))
> 0.01
for: 10m
labels:
severity: critical
annotations:
summary: "Abort rate > 1% on {{ $labels.model_name }}"
- name: vllm-capacity
interval: 30s
rules:
- alert: VLLMQueueDepthHigh
expr: vllm:num_requests_waiting > 50
for: 3m
labels:
severity: warning
- alert: VLLMKVCacheNearFull
expr: vllm:kv_cache_usage_perc > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "KV cache > 90% on {{ $labels.instance }}"
description: "Correlate with preemption, queue and SLO before mitigation."
- alert: VLLMPreemptionsSpiking
expr: rate(vllm:num_preemptions_total[5m]) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "Preempt rate > 0.5/s on {{ $labels.instance }}"
- alert: VLLMIdleNoTraffic
expr: |
rate(vllm:request_success_total[10m]) == 0
and vllm:engine_sleep_state == 0
for: 15m
labels:
severity: info
annotations:
summary: "{{ $labels.instance }} has had no traffic in 15min but is awake"
- name: vllm-cache
interval: 60s
rules:
- alert: VLLMPrefixCacheHitDrop
expr: |
(
sum by (model_name) (rate(vllm:prefix_cache_hits_total[10m]))
/ sum by (model_name) (rate(vllm:prefix_cache_queries_total[10m]))
) < 0.3
for: 30m
labels:
severity: info
annotations:
summary: "Prefix cache hit < 30% for 30min on {{ $labels.model_name }}"
description: "Workload pattern shifted? Block size? Cache evicted?"
- name: vllm-failure
rules:
# 仅在 VLLM_COMPUTE_NANS_IN_LOGITS 启用时存在
- alert: VLLMCorruptedRequests
expr: increase(vllm:corrupted_requests_total[10m]) > 0
labels:
severity: critical
annotations:
summary: "Corrupted requests detected on {{ $labels.instance }}"
description: "KV cache integrity failure. Check NCCL / disaggregated path."
- alert: VLLMScrapeDown
expr: up{job="vllm"} == 0
for: 2m
labels:
severity: critical
部署:保存为 vllm-rules.yml,引入 Prometheus 的 rule_files。
4. Grafana Dashboard 骨架(5 行 15 panel)
| 行 | Panel | PromQL |
|---|---|---|
| 行 1 · SLO | TTFT p50/p90/p99 多折线 | 上面 §2.1 |
| TPOT p50/p90/p99 多折线 | 上面 §2.1 | |
| Error rate(堆叠面积) | abort / length / stop 分桶 | |
| 行 2 · Throughput | Generated token/s | rate(vllm:generation_tokens_total[1m]) |
| RPS(按 finish_reason 堆叠) | rate(vllm:request_success_total[1m]) |
|
| iteration tokens histogram(heatmap) | vllm:iteration_tokens_total_bucket |
|
| 行 3 · Capacity | Running vs Waiting(双线) | gauge |
| KV cache 利用率(gauge / instant) | vllm:kv_cache_usage_perc |
|
| Preemption rate | rate(vllm:num_preemptions_total[5m]) |
|
| 行 4 · Cache | Prefix cache 命中率(block / token 两线) | §2.3 |
| MM cache 命中率 | hits / queries | |
| KV block lifetime p50/p99 | vllm:kv_block_lifetime_seconds_bucket |
|
| 行 5 · Roofline | Estimated TFLOPS / GPU | rate(vllm:estimated_flops_per_gpu_total[1m]) / 1e12 |
| Estimated GB/s / GPU | (rate(read) + rate(write)) / 1e9 |
|
| Approx MFU / MBU | §2.5 |
关键设计原则:
- 时间窗覆盖 scrape/evaluation lag 和 SLO 窗口;按实际低基数 labels 分组
- 用 Repeat by instance 实现多 GPU pod 切换
- 颜色统一:错误红、延迟黄、容量蓝、cache 绿
5. Oncall 30 秒清单
被告警拉起后,按顺序看这 5 个 panel:
1. "TTFT p99 是不是飙了?" → SLO 行第 1 panel
2. "TPOT p99 是不是飙了?" → SLO 行第 2 panel
3. "队列是不是积压了?" → Capacity 行第 1 panel
4. "KV 满了吗?preempt 多吗?" → Capacity 行第 2/3 panel
5. "scrape 还在吗?" → grafana 顶部 + Prometheus targets
按这 5 个的状态走根因树:
flowchart TD
A[告警拉起] --> B{TTFT 高?}
B -->|是| C{队列深?}
C -->|是| D[容量不够<br/>查 RPS 是否暴涨<br/>查 KV 是否满]
C -->|否| E[请求级问题<br/>查长 prompt / chunked prefill]
B -->|否| F{TPOT 高?}
F -->|是| G{preempt 多?}
G -->|是| H[KV 满了<br/>扩容或减 max_num_seqs]
G -->|否| I{cache 命中率掉?}
I -->|是| J[workload pattern 变了<br/>查 prefix cache]
I -->|否| K[batch 配置或 chunked size 不当]
F -->|否| L{error rate 高?}
L -->|是| M[查 corrupted_requests<br/>查 finish_reason 分布]
L -->|否| N[假告警/抖动]
classDef root fill:#fef3c7,stroke:#b45309;
classDef action fill:#eff5ff,stroke:#2563eb;
class A,B,C,F,G,I,L root;
class D,E,H,J,K,M,N action;
6. 把 metric 接到云厂商监控
vLLM 默认在 /metrics 暴露 Prometheus 格式:
# 标准 Prometheus scrape config
- job_name: vllm
scrape_interval: 15s
static_configs:
- targets: ['vllm-pod-1:8000', 'vllm-pod-2:8000', ...]
# 或者用 kubernetes_sd_configs 自动发现
云厂商接入:
- AWS CloudWatch: 通过 OTel Collector + CloudWatch exporter
- GCP: GCP Managed Service for Prometheus 直接 scrape
- Datadog: Datadog Agent 配 OpenMetrics check 抓
/metrics - 阿里云 ARMS: Prometheus 监控直接接入
OpenTelemetry trace(更细粒度):
- 开启:
--otlp-traces-endpoint <collector> - 源码:当前 V1
output_processor.py记录llm_requestspan,并以 attributes 表示 queue/prefill/decode 等时间 - 适合追"这一个请求为什么慢"——配合 metric 看"宏观趋势"
7. 推荐的最小可用监控栈
# docker-compose.yml(开发/测试用)
services:
prometheus:
image: prom/prometheus:<pinned-version-or-digest>
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- ./vllm-rules.yml:/etc/prometheus/vllm-rules.yml
ports: ["9090:9090"]
grafana:
image: grafana/grafana:<pinned-version-or-digest>
ports: ["3000:3000"]
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
volumes:
- ./grafana-dashboards:/var/lib/grafana/dashboards
- ./grafana-provisioning:/etc/grafana/provisioning
alertmanager:
image: prom/alertmanager:<pinned-version-or-digest>
ports: ["9093:9093"]
vllm:
image: vllm/vllm-openai:<validated-version-or-digest>
command: --model facebook/opt-125m --port 8000
ports: ["8000:8000"]
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 30s
rule_files:
- vllm-rules.yml
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
scrape_configs:
- job_name: vllm
static_configs:
- targets: ['vllm:8000']
启动:docker-compose up -d → Grafana http://localhost:3000,加 Prometheus 数据源 http://prometheus:9090,导入 §4 的 dashboard。
8. 已知陷阱
| 陷阱 | 怎么发现 | 怎么避免 |
|---|---|---|
| histogram bucket 不覆盖目标边界 | TTFT p99 落在最高 bucket | 当前锁定 vLLM 没有通用 bucket CLI;改用可配置的 gateway/OTel SLI 或提交受测配置变更 |
| 标签维度爆炸(per-request labels) | Prometheus 内存 OOM | 不要给 metric 加 request_id、按 model_name + finish_reason 即可 |
| metric reset on restart | counter 增长突然回 0 | 用 increase() 而非 rate() 的差值,或在 PromQL 用 resets() |
多副本下 vllm:kv_cache_usage_perc 平均掩盖热点 |
单副本可能 100% 但平均 60% | 看 max by (instance) 或开 instance label |
| 没区分 model_name | 多模型部署混在一起 | 启动加 --served-model-name <name>,metric 自带 label |
小结
- 当前 vLLM 暴露的指标随配置变化;启动后从
/metricsinventory,并按 latency / traffic / saturation / cache / kv lifecycle / failure / info 分类。 - 4 大 SLO 必看:TTFT p99、TPOT p99、abort rate、queue depth。
- 给出需替换阈值并经
promtool/staging 验证的告警模板和 dashboard 骨架。 - Oncall 30 秒按 5 panel 顺序看 + 用根因树定位。
- 接云厂商监控通过 OTel 或厂商专属 Prometheus 服务。
自检
- 写出"prefix cache 命中率(按 token 计)"的 PromQL,结果应是 0-1 之间的比例。
- 一个 32-pod 集群中某个 pod
kv_cache_usage_perc=0.95,其他 0.4 左右。哪条告警会触发?还应该查什么 metric? histogram_quantile算 TTFT p99 持续返回最高 bucket 值,说明什么?怎么修?- 给一个"oncall 早晨拿到 TTFT 飙到 3s 的告警"的场景,按 §5 根因树写出排查命令链。
下一步
- 想理解为什么这样定 SLO:
05-slo-and-observability.md(理论层)。 - 想理解 metric 怎么从代码注入:锁定 submodule 的
vllm/v1/metrics/loggers.py(Prometheus 注册)、vllm/v1/metrics/stats.py(统计聚合)。 - 想做端到端 trace:查看
vllm/v1/engine/output_processor.py的llm_requestspan attributes,并串 Gateway/EPP spans。 - 想看合成故障 case 怎么用监控定位:
07-incident-playbook.md。 - 想做容量规划:
04-autoscaling-and-capacity.md。