1. 问题背景
大模型推理服务和传统 Web 服务的监控,差异比看起来大得多。传统服务盯 QPS、RT、错误率就覆盖了大部分风险;推理服务至少多三个维度:
- 请求时长是”秒到分钟”级,传统 RT 告警(比如 p99 > 500ms)在推理服务上要么天天响要么永远不响
- 有 GPU 这个新硬件维度:温度、降频、ECC 错误、显存碎片,任何一项异常都会直接打低吞吐,且现象常表现为”性能慢慢变差”
- 容量模型不同:并发上限由 KV cache 池决定,”显存没满但并发上不去”是常态,传统”CPU 80% 告警”式直觉全部失效
线上真实发生过的三类典型故障,恰好对应三个监控盲区:
- 容量未盯:业务流量增长,KV cache 占用率长期 95%+,preempt/recompute 频发,用户侧表现为”偶发很慢”,持续两周才定位
- 硬件静默劣化:某张卡 ECC 错误缓慢累积 + 温度偏高降频,吞吐一周内下降 15%,因为”没有报错”一直没查
- 质量静默退化:某次框架升级后 kernel 行为变化,输出质量下降,延迟吞吐都正常,纯靠用户投诉发现
本文给出一套可直接落地的监控告警方案:基于推理引擎原生暴露的 /metrics(Prometheus 格式)+ DCGM(GPU 指标)+ 网关层指标,搭 Prometheus + Grafana + Alertmanager,配齐告警规则、通知链路、阈值校准流程和回滚方案。
方案定位:
- 不引入额外 agent,引擎侧零改动(vLLM/SGLang 新版原生 /metrics)
- 单机 Docker 与 K8s 两套部署形态都覆盖(Docker 为主示例,K8s 给关键 YAML)
- 所有指标名、阈值都标注了”以实际环境为准”的边界,不给绝对标准
2. 适用场景
本文适合:
- 已部署 vLLM/SGLang/TensorRT-LLM 推理服务,需要补监控告警体系
- 从零搭建推理平台,规划监控架构
- 已有传统监控,想理解”推理服务该额外盯什么”
- 故障复盘时想建立”指标 -> 根因”的排查路径
- 容量规划:用监控数据回答”还能加多少流量”
不适用于:
- 训练任务监控(loss/吞吐/通信效率是另一套体系,仅 GPU 层通用)
- 可观测性平台级设计(多集群联邦、日志全链路追踪等,本文只在边界处提一句)
- 旧版框架(无 /metrics 的 TensorRT-LLM 早期版本):文末 9.1 给了补救思路,但建议优先升级框架版本
读者假设:
- 会用 Prometheus 基础概念(target、job、PromQL 的 rate/histogram_quantile)
- 会基本的 Grafana 操作(建面板、选数据源)
- 推理服务已能正常运行(建议先完成《推理框架选型》与《量化部署》两文的部署与压测)
3. 核心知识点
3.1 三层指标体系
推理服务的指标分三层,排查时自上而下或自下而上都可以,但结论必须跨层交叉验证:
- 业务/网关层:QPS、5xx 率、网关侧 p99 延迟、超时率。回答”用户感知到什么”
- 引擎层:在跑/排队请求数、KV cache 占用率、TTFT/TPOT 直方图、token 吞吐、前缀缓存命中率。回答”引擎内部在发生什么”
- 硬件层:GPU 利用率、显存占用、温度、功耗、降频原因、ECC/Xid 错误。回答”物理层是否健康”
典型交叉验证示例:
- 网关 p99 升高 + 引擎排队数高 + GPU 利用率 100%:容量不足(需求侧)
- 网关 p99 升高 + 引擎排队正常 + GPU 温度高且降频:硬件问题(供给侧)
- 网关 p99 正常 + 引擎 KV 占用率持续走高:容量在缓慢逼近上限,未爆但要有预案
只看单层指标下结论是监控误判的主要来源。
3.2 关键指标清单
引擎层指标(vLLM 与 SGLang 命名有差异,且随版本变化,下表以主流版本命名为准,落地前用 curl /metrics | grep 核对一遍你的版本):
| 指标(vLLM 命名) | SGLang 对应 | 含义 | 正常表现 | 异常信号 |
|---|---|---|---|---|
| vllm:num_requests_running | sglang:num_running_requests | 正在解码的请求数 | 低于 max_num_seqs | 长期贴顶 |
| vllm:num_requests_waiting | sglang:num_queue_reqs | 排队请求数 | 0 或个位数 | 持续增长 |
| vllm:gpu_cache_usage_perc | sglang:token_usage | KV cache 占用率 | <0.8(依负载) | 长期 >0.95 |
| vllm:time_to_first_token_seconds | sglang:time_to_first_token_seconds | TTFT 直方图 | p99 在 SLO 内 | p99 突增 |
| vllm:time_per_output_token_seconds | sglang:inter_token_latency_seconds | 单 token 耗时直方图 | p95 稳定 | 持续爬升 |
| vllm:e2e_request_latency_seconds | sglang:e2e_request_latency_seconds | 端到端延迟 | 与请求长度分布匹配 | 同长度请求延迟漂移 |
| vllm:num_prompt_tokens | sglang:num_prompt_tokens | 累计输入 token | 增速与 QPS 匹配 | 增速突变(流量异常) |
| vllm:num_generation_tokens | sglang:num_generated_tokens | 累计输出 token | 同上 | 吞吐骤降 |
| -(前缀命中相关) | sglang:cache_hit_rate | 前缀缓存命中率 | 多轮场景 >0.5 | 骤降(模板变了/缓存被打爆) |
硬件层指标(DCGM exporter 提供,名称稳定):
| 指标 | 含义 | 正常表现 | 异常信号 |
|---|---|---|---|
| DCGM_FI_DEV_GPU_UTIL | GPU 利用率 % | 与负载匹配 | 100% 且吞吐不涨 |
| DCGM_FI_DEV_MEMORY_USED | 显存占用 MB | 与部署配置一致 | 缓慢爬升(泄漏/长请求堆积) |
| DCGM_FI_DEV_FB_USED | 框架显存占用 MB | 同上 | 同上 |
| DCGM_FI_DEV_GPU_TEMP | 温度 C | <75(依机房) | >85 持续,伴随降频 |
| DCGM_FI_DEV_POWER_USAGE | 功耗 W | 接近 TDP 或依负载 | 异常低(降频/故障) |
| DCGM_FI_DEV_ECC_DBE_AGG_TOTAL | 双比特 ECC 错误累计 | 0 | 任何增长都要处理 |
| DCGM_FI_DEV_ECC_SBE_AGG_TOTAL | 单比特 ECC 累计 | 少量 | 快速增长 |
| DCGM_FI_DEV_XID_ERRORS | Xid 错误累计 | 0 | 任何增长查 dmesg 定位 |
| DCGM_FI_DEV_CLOCK_THROTTLE_REASONS | 降频原因位图 | 0 | 非 0 持续(热/功耗/硬件) |
| DCGM_FI_PROF_PIPE_TENSOR_ACTIVE | Tensor Core 活跃率 | decode 场景偏低正常 | 结合 util 一起看 |
网关层(以 ingress-nginx 为例,指标名以实际网关为准):
| 指标 | 含义 | 说明 |
|---|---|---|
| nginx_ingress_controller_requests | 请求计数(含 status 标签) | 5xx 率 = 5… 占比 |
| 网关侧延迟直方图 | 含网络与排队 | 与引擎侧 p99 对比差值即中间层耗时 |
3.3 KV cache 与容量指标
KV cache 占用率(gpu_cache_usage_perc / token_usage)是推理服务最重要的容量指标:
- 占用率 = 已分配给所有在跑请求的 KV block / KV 池总 block
- 占用率趋近 1 时的引擎行为:新请求排队、长请求触发 preempt(swap 到 CPU 或 recompute),表现为 TTFT/TPOT 抖动放大
- 容量余量公式(与《推理框架选型》13 节一致):理论最大并发 = KV 池 token 数 / 单请求平均 token 占用;监控上建议把”占用率 80% 对应的并发水位”记为设计容量
观察方法:
- 稳态负载下占用率应是”平台”而非”斜坡”;持续上爬说明流量结构在变(变长/变多)
- 锯齿形波动正常(请求潮汐);锯齿幅度逐周放大 = 容量在逼近极限
- 结合
num_requests_waiting一起看:占用率高但无排队 = 健康的高水位;占用率高且有排队 = 容量不足
3.4 TTFT/TPOT、分位数与 SLO
TTFT 和 TPOT 都是直方图(histogram),监控上永远用分位数而不是均值:
- 均值会被少数长请求拉高/被多数短请求拉低,看不出用户体验
- 标准取法:TTFT 看 p95/p99,TPOT 看 p95
- PromQL:
histogram_quantile(0.99, sum(rate(<histogram>_bucket[5m])) by (le))
SLO 定义示例(数字按你的业务压测基线定,下面只是格式):
- 可用性:5 分钟内非 5xx 请求占比 >= 99.9%
- TTFT p99 < 2s(输入 2K token 以内)
- TPOT p95 < 50ms
- 错误预算:按月计算,燃尽速度(burn rate)超 2x 触发告警升级
为什么用分位数窗口而不是瞬时值:直方图的 rate 窗口(5m)已经是平滑过的,分位数之上再加 for 10m 持续条件,能滤掉压测、发布抖动等瞬时事件。
3.5 告警哲学:基线、阈值、误报率
三条纪律,比任何单条规则都重要:
- 阈值必须来自基线:压测基线(同口径流量下的指标带)+ 上线后 2 周生产数据(各指标的 p50/p95/p99 分布)。没有基线就上的告警,前两周一定全是噪声,会毁掉整个告警体系的可信度
- 分级而非一刀切:warning(容量/性能开始偏离,工作时间内看)与 critical(SLO 受损/用户可感知/硬件故障,立即响应)。critical 的触发条件必须”响了就要动”,否则升级机制失效
- 误报率要度量:每月统计”触发但无需处理”的告警占比,>30% 说明阈值要校准。告警体系的价值 = 真报率 x 响应时效,不是规则数量
告警分组与收敛(Alertmanager 层):
group_by: [alertname, severity](或加 instance 前缀,看规模)group_wait30s 合并同组,repeat_intervalwarning 4h / critical 1h- 维护 silence 模板:发布窗口、计划内压测、计划内重启,提前挂 silence,禁止”事后解释”
4. 整体思路
监控建设按四步走,每步有明确的完成标准,不要跳步:
- 第一步:接入(scrape 三层指标)
- 完成标准:Prometheus targets 全部 up,10 个关键查询(5.5 清单)都有返回值,数据连续 24h 无断点
- 第二步:观察(Grafana 面板 + 基线积累)
- 完成标准:面板覆盖 3.2 清单全部指标;连续 2 周生产数据;产出第一版”指标基线表”(每指标的 p50/p95/异常带)
- 第三步:告警(规则 + 通知链路 + on-call 流程)
- 完成标准:告警规则全部经测试触发与恢复(5.9);通知链路端到端验证通过;on-call 手册写清”每条告警第一眼查什么”
- 第四步:校准(月度复盘)
- 完成标准:每月一次复盘会(误报/漏报/阈值调整/容量余量),复盘记录存档
顺序不能乱:没接好就配告警 = 误报地狱;没基线就定阈值 = 拍脑袋数字;没通知链路就上告警 = 半夜没人醒。
监控自身的监控(meta-monitoring):
up指标本身要告警(任何 target down > 3min 即 warning)- Prometheus 存储水位(磁盘余量、series 数量)纳入面板
- 面板数据断流本身就是事故:在面板首行放”数据新鲜度”(last scrape 时间)
5. 实战步骤
5.0 环境假设
- 推理服务(已按前文部署,本机或同网段):
- vLLM:10.0.0.11:8001(/metrics 同端口)
- SGLang:10.0.0.12:8002
- TRT-LLM:10.0.0.13:8003(新版 trtllm-serve,/metrics 同端口)
- 监控机:10.0.0.20(Docker 可用,磁盘余量 >50GB)
- GPU:各推理机 A100/H100,NVIDIA 驱动 535+
- 软件版本(示例,生产钉死 tag):
- prom/prometheus:v2.53.0
- grafana/grafana:11.1.0
- prom/alertmanager:v0.27.0
- nvcr.io/nvidia/k8s/dcgm-exporter(tag 以 NGC 目录为准)
目录规划:
/etc/prometheus/
├── prometheus.yml
├── rules/
│ ├── recording.yml
│ └── alerts.yml
/etc/alertmanager/
└── alertmanager.yml
/data/prometheus/ # TSDB 存储
/data/grafana/ # Grafana 持久化
5.1 推理服务 /metrics 检查
目的:确认三个服务都暴露了预期的指标,记录你版本里的实际指标名。
# vLLM
curl -s http://10.0.0.11:8001/metrics | grep -c '^vllm:'
curl -s http://10.0.0.11:8001/metrics | grep -E 'vllm:(num_requests_running|num_requests_waiting|gpu_cache_usage_perc|time_to_first_token)' | grep -v '^#' | head -20
# SGLang
curl -s http://10.0.0.12:8002/metrics | grep -c '^sglang:'
curl -s http://10.0.0.12:8002/metrics | grep -E 'sglang:(num_running_requests|num_queue_reqs|token_usage|cache_hit_rate|time_to_first_token)' | grep -v '^#' | head -20
# TRT-LLM(新版)
curl -s http://10.0.0.13:8003/metrics | head -50
预期:每条 grep 都能列出指标行(值可为 0,但行必须存在)。 异常:
- 404:端口不对或该版本不暴露 /metrics。TRT-LLM 旧版没有现成指标(见 9.1 补救),其余框架 404 通常是参数配错(
--metrics-port类参数或默认端口不符) - 行存在但长期全 0 且无请求进来:正常;有请求但全 0:查请求是否真的打到该实例(多实例负载均衡时)
- 指标名与本文不一致:正常,版本差异。把实际名字存档,后文 PromQL 按实际名字替换
判断逻辑:这一步产出的”实际指标名清单”是后面所有 PromQL 的事实依据,不要跳过。
5.2 部署 Prometheus
mkdir -p /etc/prometheus/rules /data/prometheus
docker run -d --name prometheus \
--restart unless-stopped \
--network host \
-v /etc/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro \
-v /etc/prometheus/rules:/etc/prometheus/rules:ro \
-v /data/prometheus:/prometheus \
prom/prometheus:v2.53.0 \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/prometheus \
--storage.tsdb.retention.time=30d \
--web.enable-lifecycle
参数说明:
--web.enable-lifecycle:开启POST /-/reload,改配置免重启retention.time=30d:30 天保留,容量规划与月度复盘够用;更长要评估磁盘(见 3.4 的 series 估算思路)--network host:示例走 host 网络简化端口;K8s 环境用 Service 暴露 9090
验证:
curl -s http://127.0.0.1:9090/-/ready # 预期: READY
curl -s http://127.0.0.1:9090/api/v1/targets | python3 -m json.tool | grep -E '"(scrapeUrl|health)"'
curl -s http://127.0.0.1:9090/api/v1/label/job/values | python3 -m json.tool
预期:targets 里各 job 状态为 up(首次配置后等 2 个 scrape 周期,15s x 2)。 异常:某 target down -> 端口/防火墙/容器网络;labels 里 job 缺失 -> rule_files 或 scrape_configs 写错,docker logs prometheus 查配置报错。
5.3 抓取配置
完整 prometheus.yml 见 7.1,这里说明结构与验证节奏:
- 每个推理服务一个 job(vllm / sglang / trtllm),
metrics_path: /metrics,target 写推理机 IP + 服务端口 - dcgm-exporter 一个 job(端口 9400)
- node-exporter 一个 job(端口 9100,机器基础指标)
- 网关 exporter(如有)一个 job
- 所有 job 统一
scrape_interval: 15s;直方图密集的服务可 10s
改配置验证:
docker exec prometheus promtool check config /etc/prometheus/prometheus.yml
curl -s -X POST http://127.0.0.1:9090/-/reload
curl -s http://127.0.0.1:9090/api/v1/targets | python3 -c "import json,sys; d=json.load(sys.stdin); [print(t['labels'].get('job'), t['health']) for t in d['data']['activeTargets']]"
预期:promtool 输出 success,reload 后新 job 出现在 targets 且 30s 内转 up。
5.4 部署 dcgm-exporter(每台 GPU 机)
docker run -d --name dcgm-exporter \
--restart unless-stopped \
--network host \
--gpus all \
-e DCGM_EXPORTER_LISTEN=:9400 \
-e DCGM_EXPORTER_KPI_LIST=DCGM_FI_DEV_GPU_UTIL,DCGM_FI_DEV_MEMORY_USED,DCGM_FI_DEV_FB_USED,DCGM_FI_DEV_GPU_TEMP,DCGM_FI_DEV_POWER_USAGE,DCGM_FI_DEV_ECC_SBE_AGG_TOTAL,DCGM_FI_DEV_ECC_DBE_AGG_TOTAL,DCGM_FI_DEV_XID_ERRORS,DCGM_FI_DEV_CLOCK_THROTTLE_REASONS,DCGM_FI_DEV_SM_CLOCK \
nvcr.io/nvidia/k8s/dcgm-exporter:<以NGC目录为准的tag>
说明:
- KPI 列表是”只导出你要的字段”,能显著降 series 数量;
DCGM_FI_PROF_*类 profiling 指标部分 GPU/驱动组合不可用,首次部署先不加,验证 DEV 类全出后再按需补 --gpus all+ host 网络,多卡机的多张卡以gpu=0/1/...标签区分- 权限:DCGM 需要读 GPU 状态,普通 root 容器通常即可;出现权限报错查容器安全上下文
验证:
curl -s http://127.0.0.1:9400/metrics | grep -E 'DCGM_FI_DEV_(GPU_UTIL|MEMORY_USED|ECC_DBE)' | grep -v '^#' | head -10
curl -s http://127.0.0.1:9400/metrics | grep -c 'DCGM_FI'
预期:每卡一组指标(gpu 标签区分),GPU_UTIL 值在 0-100。 异常:容器起不来(驱动版本与镜像不匹配)、指标缺失(KPI 名拼错,对照 curl /metrics 全量输出排查)、PROF 类指标为 0 或无(硬件/驱动限制,记录即可,不影响基础监控)。
5.5 PromQL 关键查询验证清单
数据接入后,逐条执行以下查询,全部有返回值才进入面板阶段(指标名按 5.1 存档的实际名替换):
PROM=http://127.0.0.1:9090/api/v1/query
# 1. target 在线
curl -sG $PROM --data-urlencode 'query=up' | python3 -m json.tool | grep -E '"(value|__name__)"'
# 2. 输出吞吐(累计计数器做 rate)
curl -sG $PROM --data-urlencode 'query=sum(rate(vllm:num_generation_tokens[5m]))'
# 3. TTFT p99
curl -sG $PROM --data-urlencode 'query=histogram_quantile(0.99, sum(rate(vllm:time_to_first_token_seconds_bucket[5m])) by (le))'
# 4. KV cache 占用率
curl -sG $PROM --data-urlencode 'query=avg by (instance) (vllm:gpu_cache_usage_perc)'
# 5. 排队
curl -sG $PROM --data-urlencode 'query=max(vllm:num_requests_waiting)'
# 6. GPU 利用率(按卡)
curl -sG $PROM --data-urlencode 'query=DCGM_FI_DEV_GPU_UTIL'
# 7. GPU 温度
curl -sG $PROM --data-urlencode 'query=max(DCGM_FI_DEV_GPU_TEMP)'
# 8. ECC 双比特错误(应为 0)
curl -sG $PROM --data-urlencode 'query=max(DCGM_FI_DEV_ECC_DBE_AGG_TOTAL)'
# 9. 前缀缓存命中(SGLang 场景)
curl -sG $PROM --data-urlencode 'query=sglang:cache_hit_rate'
# 10. 网关 5xx 占比(如有 ingress-nginx)
curl -sG $PROM --data-urlencode 'query=sum(rate(nginx_ingress_controller_requests{status=~"5.."}[5m])) / sum(rate(nginx_ingress_controller_requests[5m]))'
每条的预期与异常:
- 第 2/3 条在无流量时可能为 NaN(没有样本的直方图分位数是空值),打几个请求后应出数
- 第 8 条任何 >0 的值都转 9.6 硬件流程,不等告警
- 第 10 条分母为 0 时 NaN:正常(无流量)
- 任一查询报 parser error:PromQL 语法或指标名问题,对照 5.1 存档修正
5.6 搭建 Grafana 看板
部署:
docker run -d --name grafana \
--restart unless-stopped \
--network host \
-v /data/grafana:/var/lib/grafana \
grafana/grafana:11.1.0
访问 http://10.0.0.20:3000,添加 Prometheus 数据源(http://10.0.0.20:9090 或容器名,依网络形态)。
看板结构(一个 dashboard 四个 row,面板名与查询口径):
| Row | 面板 | 查询口径(示例) |
|---|---|---|
| 概览 | 请求 QPS | sum(rate(nginx_ingress_controller_requests[5m])) 或引擎层 request 计数器 |
| 概览 | TTFT p95 / p99 | histogram_quantile(0.95/0.99, sum(rate(_bucket[5m])) by (le)) |
| 概览 | TPOT p95 | histogram_quantile(0.95, sum(rate(_bucket[5m])) by (le)) |
| 概览 | 5xx 率 | 5xx 计数占比(网关层) |
| 引擎 | running / waiting 请求数 | 两条线,同轴 |
| 引擎 | KV cache 占用率 | avg by (instance) (gpu_cache_usage_perc / token_usage) |
| 引擎 | 输入/输出 token 吞吐 | sum(rate(num_prompt_tokens[5m])) / sum(rate(num_generation_tokens[5m])) |
| 引擎 | 前缀缓存命中率(SGLang) | sglang:cache_hit_rate |
| GPU | 利用率(按卡,heatmap) | DCGM_FI_DEV_GPU_UTIL by (instance, gpu) |
| GPU | 显存使用(按卡) | DCGM_FI_DEV_MEMORY_USED |
| GPU | 温度(按卡) | DCGM_FI_DEV_GPU_TEMP |
| GPU | 功耗 | DCGM_FI_DEV_POWER_USAGE |
| GPU | 降频原因 | DCGM_FI_DEV_CLOCK_THROTTLE_REASONS(非 0 高亮) |
| GPU | ECC DBE 累计 | DCGM_FI_DEV_ECC_DBE_AGG_TOTAL(any >0 红色标注) |
| 容量 | 容量余量 | 1 – avg(gpu_cache_usage_perc),低于 0.2 高亮 |
| 系统 | 数据新鲜度 | 各 target 的 last scrape 时间(up 的时序) |
使用说明:
- 面板查询里的指标名必须按 5.1 存档替换;Grafana 面板字段(单位、阈值色带)依赖具体数据源与版本,按实际环境调整
- 用变量(Variables)做框架切换:定义
framework变量(vllm/sglang/trtllm),查询里用${framework}拼指标前缀,避免三套重复面板 - 阈值色带(如 KV 占用率 0.8/0.95 两档颜色)按你的基线设,不要照抄
- 看板模板存 JSON(Grafana 导出)进 git,版本化管理
5.7 记录规则(Recording Rules)
目的:把高频复杂的 PromQL 预计算成低基数序列,加速告警评估、统一口径。完整文件见 7.2,核心规则:
- record: llm:out_tps:5m
expr: sum(rate(vllm:num_generation_tokens[5m]))
- record: llm:ttft_p99:5m
expr: histogram_quantile(0.99, sum(rate(vllm:time_to_first_token_seconds_bucket[5m])) by (le))
- record: llm:tpot_p95:5m
expr: histogram_quantile(0.95, sum(rate(vllm:time_per_output_token_seconds_bucket[5m])) by (le))
- record: llm:queue_max:1m
expr: max(vllm:num_requests_waiting)
- record: llm:kv_usage:1m
expr: avg(vllm:gpu_cache_usage_perc)
- record: llm:ecc_dbe_inc_1h
expr: increase(DCGM_FI_DEV_ECC_DBE_AGG_TOTAL[1h])
说明:
- 多框架并存时按
job分组记录(by (job)或分 job 的规则文件),否则 sum 会跨框架混算 - 验证:
curl -sG $PROM --data-urlencode 'query=llm:kv_usage:1m'有值即生效 - 收益:告警规则引用记录规则后更短更稳,口径变更只改一处
5.8 告警规则
完整文件见 7.3。这里给规则清单与阈值逻辑(所有阈值是”示例起点”,必须按 5.10 校准后定稿):
| 告警 | 级别 | 条件(示例) | 逻辑 |
|---|---|---|---|
| 引擎不可达 | critical | up{job=“vllm”} == 0 for 3m | 服务挂/抓取挂,先查服务再查网络 |
| 排队堆积 | warning | llm:queue_max:1m > 4 for 5m | 4 为示例值,按”设计并发的 10%”定 |
| 排队堆积 | critical | llm:queue_max:1m > 16 for 3m | 按”设计并发的 50%”定 |
| KV 高水位 | warning | llm:kv_usage:1m > 0.85 for 10m | 容量预警线 |
| KV 高水位 | critical | llm:kv_usage:1m > 0.97 for 5m | preempt 高发线 |
| TTFT 超 SLO | critical | llm:ttft_p99:5m > 2 for 10m | SLO 受损 |
| TPOT 超 SLO | warning | llm:tpot_p95:5m > 0.05 for 10m | 体验劣化 |
| GPU 高温 | warning | max(DCGM_FI_DEV_GPU_TEMP) > 85 for 5m | 散热/负载问题 |
| GPU 降频 | warning | max(DCGM_FI_DEV_CLOCK_THROTTLE_REASONS) > 0 for 10m | 位图非 0 即有原因,查 thermal/power |
| ECC 双比特错误 | critical | llm:ecc_dbe_inc_1h > 0 | 任何增长即硬件风险,不等量级 |
| Xid 错误 | critical | increase(DCGM_FI_DEV_XID_ERRORS[10m]) > 0 | 任何增长查 dmesg |
| 5xx 率 | critical | 5xx 占比 > 0.01 for 5m | 网关层,与引擎层交叉验证 |
| 抓取失败 | warning | up{job=“dcgm-exporter”} == 0 for 5m | 监控自身健康 |
阈值设定原则(写进变更单):
- 每个阈值标注”依据”:压测哪组数据、生产哪 2 周的哪个分位
- warning 阈值 = 开始偏离正常操作区间;critical = SLO 受损或硬件风险
- 同一指标 warning/critical 之间要有”缓冲带”(如 0.85/0.97),避免同根因双响
5.9 通知链路与 on-call
Alertmanager 部署与配置(完整文件见 7.4):
mkdir -p /etc/alertmanager
docker run -d --name alertmanager \
--restart unless-stopped \
--network host \
-v /etc/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro \
prom/alertmanager:v0.27.0 \
--config.file=/etc/alertmanager/alertmanager.yml
配置要点:
- 分组:
group_by: [alertname, severity],group_wait: 30s,repeat_intervalwarning 4h / critical 1h - 路由:critical 走独立 receiver(独立频道/电话),warning 走日常频道
- 通知出口:推荐 webhook 接内部 relay 服务转发到 IM(钉钉/企微机器人)或 on-call 系统。Alertmanager 的 webhook 发的是 JSON,relay 负责格式转换;机器人 token 放 relay 侧配置并收紧权限,不写进 alertmanager.yml(该文件常被归档/提交仓库,明文 token 会扩散)
send_resolved: true:恢复通知必须开,避免”只响不消”
通知链路端到端验证(发一条测试告警):
curl -s -XPOST http://127.0.0.20:9093/api/v2/alerts \
-H 'Content-Type: application/json' \
-d '[{"labels":{"alertname":"notify-chain-test","severity":"warning"},"annotations":{"summary":"通知链路测试,忽略"}}]'
预期:30s 内 IM 收到测试告警,数分钟内收到 resolved。 异常:没收到 -> 按链路分段查:alertmanager docker logs 有无发送记录 -> relay 服务日志 -> IM 机器人是否被限频/禁用。
on-call 手册(每条告警一页,示例条目):
告警: 排队堆积 (critical)
第一眼: Grafana 引擎 Row(running/waiting + KV 占用率)
判断分支:
KV 占用率 >0.95 -> 容量不足: 确认流量是否超预期(网关 QPS 面板), 按预案扩容或限流
KV 正常 -> 查长请求: 请求长度分布(业务侧) + 引擎日志 grep preempt/abort
两者都正常 -> 查网关侧排队(网络/限流层)
升级条件: 15 分钟未定位 或 伴随 5xx 率告警
5.10 压测校准阈值
目的:用受控压测把”告警阈值”从拍脑袋变成有数据支撑的数字。
步骤:
- 按设计负载的 70%、90%、110% 三档各压测 2-4 小时(低峰窗口,不抢生产流量;生产环境用独立压测机与影子服务)
- 每档记录(面板截图 + CSV 导出):queue、KV 占用率、TTFT/TPOT p95/p99、GPU util/temp、吞吐
- 填基线表(模板):
| 指标 | 70% 负载 p95 | 90% 负载 p95 | 110% 负载 p95 | 告警阈值定稿 | 定稿依据 |
|---|---|---|---|---|---|
| queue max | |||||
| KV 占用率 | |||||
| TTFT p99 | |||||
| TPOT p95 | |||||
| GPU temp |
- 阈值落进 alerts.yml(git 提交,变更单附基线表),reload 生效
- 上线后 2 周生产数据复核一次:生产分布与压测分布的偏差 >20% 的指标,重新定阈值
- 之后每月复盘一次(误报率、漏报、阈值漂移)
6. 常用命令
指标检查:
curl -s http://10.0.0.11:8001/metrics | grep -c '^vllm:'
curl -s http://10.0.0.11:8001/metrics | grep -E 'time_to_first_token|gpu_cache_usage' | grep -v '^#'
curl -s http://10.0.0.12:8002/metrics | grep -E 'token_usage|cache_hit_rate' | grep -v '^#'
curl -s http://10.0.0.13:8003/metrics | head -50
curl -s http://127.0.0.1:9400/metrics | grep 'DCGM_FI_DEV_GPU_UTIL'
Prometheus API:
PROM=http://127.0.0.20:9090/api/v1
curl -sG $PROM/query --data-urlencode 'query=up'
curl -sG $PROM/query --data-urlencode 'query=llm:kv_usage:1m'
curl -sG $PROM/series --data-urlencode 'match[]={__name__=~"vllm:.*"}' | head -c 2000
curl -s $PROM/targets
curl -s $PROM/status | python3 -m json.tool | head -30
配置校验与 reload:
docker exec prometheus promtool check config /etc/prometheus/prometheus.yml
docker exec prometheus promtool check rules /etc/prometheus/rules/alerts.yml
curl -s -X POST http://127.0.0.20:9090/-/reload
docker exec alertmanager amtool config check /etc/alertmanager/alertmanager.yml
容器管理:
docker ps -a | grep -E 'prometheus|grafana|alertmanager|dcgm'
docker logs --tail 100 prometheus
docker logs --tail 100 alertmanager
docker stats prometheus grafana
硬件侧(配合定位):
nvidia-smi
nvidia-smi -q -d ECC,PERFORMANCE | grep -Ei 'ecc|throttle|clocks$|reason'
dmesg | grep -Ei 'nvrm|xid' | tail -20
7. 配置示例
7.1 prometheus.yml
文件位置:/etc/prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
cluster: prod-ai
rule_files:
- /etc/prometheus/rules/recording.yml
- /etc/prometheus/rules/alerts.yml
scrape_configs:
- job_name: vllm
metrics_path: /metrics
static_configs:
- targets: ["10.0.0.11:8001"]
labels:
service: llm-inference
variant: vllm
- job_name: sglang
metrics_path: /metrics
static_configs:
- targets: ["10.0.0.12:8002"]
labels:
service: llm-inference
variant: sglang
- job_name: trtllm
metrics_path: /metrics
static_configs:
- targets: ["10.0.0.13:8003"]
labels:
service: llm-inference
variant: trtllm
- job_name: dcgm-exporter
static_configs:
- targets:
- "10.0.0.11:9400"
- "10.0.0.12:9400"
- "10.0.0.13:9400"
- job_name: node-exporter
static_configs:
- targets:
- "10.0.0.11:9100"
- "10.0.0.12:9100"
- "10.0.0.13:9100"
- job_name: gateway
static_configs:
- targets: ["10.0.0.30:10254"] # ingress-nginx metrics 端口,按实际
说明:
- 自定义 labels(service/variant)是跨服务聚合查询的关键,后续
sum by (variant)类查询全靠它 - 多实例同一服务时 target 列多行,靠 instance 标签区分;不要给 target 加”用户名/会话 id”类高基数标签(series 爆炸)
7.2 recording.yml
文件位置:/etc/prometheus/rules/recording.yml
groups:
- name: llm-recording
interval: 15s
rules:
- record: llm:out_tps:5m
expr: sum by (variant) (rate(vllm:num_generation_tokens[5m]))
- record: llm:in_tps:5m
expr: sum by (variant) (rate(vllm:num_prompt_tokens[5m]))
- record: llm:ttft_p95:5m
expr: histogram_quantile(0.95, sum by (le, variant) (rate(vllm:time_to_first_token_seconds_bucket[5m])))
- record: llm:ttft_p99:5m
expr: histogram_quantile(0.99, sum by (le, variant) (rate(vllm:time_to_first_token_seconds_bucket[5m])))
- record: llm:tpot_p95:5m
expr: histogram_quantile(0.95, sum by (le, variant) (rate(vllm:time_per_output_token_seconds_bucket[5m])))
- record: llm:queue_max:1m
expr: max by (variant) (vllm:num_requests_waiting)
- record: llm:kv_usage:1m
expr: avg by (variant) (vllm:gpu_cache_usage_perc)
- record: llm:ecc_dbe_inc_1h
expr: increase(DCGM_FI_DEV_ECC_DBE_AGG_TOTAL[1h])
- record: llm:gpu_temp_max:1m
expr: max by (instance) (DCGM_FI_DEV_GPU_TEMP)
说明:
- SGLang/TRT-LLM 的等价规则复制一份改指标名与 variant 值,或按 5.1 实际名字统一
interval: 15s与 scrape 对齐;直方图分位数类规则的计算开销可忽略
7.3 alerts.yml
文件位置:/etc/prometheus/rules/alerts.yml
groups:
- name: llm-availability
rules:
- alert: LLMEngineDown
expr: up{job=~"vllm|sglang|trtllm"} == 0
for: 3m
labels:
severity: critical
annotations:
summary: "{{ $labels.job }} {{ $labels.instance }} 推理引擎不可达"
runbook: "先查服务进程(docker ps / kubectl get pod),再查网络与端口"
- alert: ScrapeDown
expr: up{job=~"dcgm-exporter|node-exporter|gateway"} == 0
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.job }} {{ $labels.instance }} 抓取失败(监控自身)"
runbook: "检查 exporter 容器与端口;监控缺失期间引擎告警失真"
- name: llm-capacity
rules:
- alert: KVCacheHigh
expr: llm:kv_usage:1m > 0.85
for: 10m
labels:
severity: warning
annotations:
summary: "KV cache 占用 {{ $value | humanize }},容量预警"
runbook: "看容量 Row:确认流量是否超预期;预案=扩容副本或收缩上下文上限"
- alert: KVCacheCritical
expr: llm:kv_usage:1m > 0.97
for: 5m
labels:
severity: critical
annotations:
summary: "KV cache 占用 {{ $value | humanize }},preempt 高发"
runbook: "看排队与 TTFT p99;确认是否需立即限流/扩容"
- alert: QueueBuildup
expr: llm:queue_max:1m > 4
for: 5m
labels:
severity: warning
annotations:
summary: "排队请求 {{ $value }}(warning 线)"
runbook: "阈值=设计并发10%(按5.10校准结果改)"
- alert: QueueBuildupCritical
expr: llm:queue_max:1m > 16
for: 3m
labels:
severity: critical
annotations:
summary: "排队请求 {{ $value }}(critical 线)"
runbook: "交叉验证:KV占用高=容量不足;KV正常=查长请求/网关排队"
- name: llm-slo
rules:
- alert: TTFTBreach
expr: llm:ttft_p99:5m > 2
for: 10m
labels:
severity: critical
annotations:
summary: "TTFT p99 {{ $value | humanize }}s 超 SLO(2s)"
runbook: "SLO 数字按压测基线改;查排队/长prompt/硬件降频三条路径"
- alert: TPOTBreach
expr: llm:tpot_p95:5m > 0.05
for: 10m
labels:
severity: warning
annotations:
summary: "TPOT p95 {{ $value | humanize }}s 超体验线(50ms)"
runbook: "看 KV 水位与 batch 规模;量化版本切换后需重新校准"
- name: llm-hardware
rules:
- alert: GPUTempHigh
expr: max(DCGM_FI_DEV_GPU_TEMP) > 85
for: 5m
labels:
severity: warning
annotations:
summary: "GPU 温度 {{ $value }}C(instance={{ $labels.instance }} gpu={{ $labels.gpu }})"
runbook: "查机房温度/风扇/负载;持续高温=降频->吞吐劣化"
- alert: GpuThrottling
expr: max(DCGM_FI_DEV_CLOCK_THROTTLE_REASONS) > 0
for: 10m
labels:
severity: warning
annotations:
summary: "GPU 降频位图非 0(instance={{ $labels.instance }} gpu={{ $labels.gpu }})"
runbook: "位图按 DCGM 文档解码(热/功耗/硬件);对照温度与功耗面板"
- alert: EccDoubleBitError
expr: llm:ecc_dbe_inc_1h > 0
for: 0m
labels:
severity: critical
annotations:
summary: "GPU 双比特 ECC 错误增长(instance={{ $labels.instance }} gpu={{ $labels.gpu }})"
runbook: "硬件风险:摘流量->报修换卡,不要带病运行"
- alert: XidError
expr: increase(DCGM_FI_DEV_XID_ERRORS[10m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "GPU Xid 错误(instance={{ $labels.instance }} gpu={{ $labels.gpu }})"
runbook: "dmesg 搜 NVRM 定位 Xid 码;常见 79=硬件故障,转硬件流程"
- alert: Gateway5xx
expr: >
sum(rate(nginx_ingress_controller_requests{status=~"5.."}[5m]))
/
sum(rate(nginx_ingress_controller_requests[5m])) > 0.01
for: 5m
labels:
severity: critical
annotations:
summary: "网关 5xx 占比超 1%"
runbook: "先确认是引擎还是网关自身;引擎侧看 LLMEngineDown 与排队"
说明:
- 所有阈值(0.85/0.97/4/16/2s/50ms/85C/1%)是示例起点,5.10 校准后替换,替换时 git commit message 写依据
for: 0m仅用于硬件类”即发即报”- runbook 字段直接对应 on-call 手册条目,告警消息里带出来
7.4 alertmanager.yml
文件位置:/etc/alertmanager/alertmanager.yml
global:
resolve_timeout: 5m
route:
receiver: im-default
group_by: [alertname, severity]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers:
- severity = "critical"
receiver: im-critical
repeat_interval: 1h
receivers:
- name: im-default
webhook_configs:
- url: http://10.0.0.50:9099/alerts
send_resolved: true
- name: im-critical
webhook_configs:
- url: http://10.0.0.50:9099/alerts/critical
send_resolved: true
说明:
10.0.0.50:9099是内部 relay 服务(把 Alertmanager JSON 转成 IM 机器人格式);没有 relay 时可用email_configs过渡- 机器人 token 在 relay 侧管理(环境变量或权限 600 的配置),不进入本文件
- 改配置后:
docker restart alertmanager(Alertmanager 无 reload API,重启秒级完成,期间新告警可能延迟分组)
7.5 K8s 形态:dcgm-exporter DaemonSet 与 Prometheus Service
文件位置:/deploy/monitoring/dcgm-exporter.yaml(示例,按集群改 namespace/镜像 tag)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: dcgm-exporter
namespace: monitoring
labels:
app: dcgm-exporter
spec:
selector:
matchLabels:
app: dcgm-exporter
template:
metadata:
labels:
app: dcgm-exporter
spec:
hostPID: true
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
containers:
- name: dcgm-exporter
image: nvcr.io/nvidia/k8s/dcgm-exporter:<以NGC目录为准的tag>
env:
- name: DCGM_EXPORTER_KPI_LIST
value: "DCGM_FI_DEV_GPU_UTIL,DCGM_FI_DEV_MEMORY_USED,DCGM_FI_DEV_FB_USED,DCGM_FI_DEV_GPU_TEMP,DCGM_FI_DEV_POWER_USAGE,DCGM_FI_DEV_ECC_SBE_AGG_TOTAL,DCGM_FI_DEV_ECC_DBE_AGG_TOTAL,DCGM_FI_DEV_XID_ERRORS,DCGM_FI_DEV_CLOCK_THROTTLE_REASONS"
- name: DCGM_EXPORTER_LISTEN
value: ":9400"
ports:
- name: dcgm
containerPort: 9400
securityContext:
runAsUser: 0
volumeMounts:
- name: proc
mountPath: /host/proc
readOnly: true
volumes:
- name: proc
hostPath:
path: /proc
---
apiVersion: v1
kind: Service
metadata:
name: dcgm-exporter
namespace: monitoring
spec:
clusterIP: None # headless,Prometheus 用 DNS SRV 或直接 pod IP 列表
selector:
app: dcgm-exporter
ports:
- name: dcgm
port: 9400
说明:
- DaemonSet 每 GPU 节点一个实例,多卡机以
gpu标签区分卡 - Prometheus 抓取 K8s target 的两种方式:静态 target 列表(小规模)或 ServiceMonitor/Relabel(大规模,以你的 Prometheus 部署形态为准,本文不展开)
- hostPath /proc + hostPID 是 DCGM 读宿主机进程/设备状态的标准做法,安全评审时按集群基线处理
7.6 Prometheus 自身(K8s 最简形态)
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: prometheus
template:
metadata:
labels:
app: prometheus
spec:
containers:
- name: prometheus
image: prom/prometheus:v2.53.0
args:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=30d
- --web.enable-lifecycle
ports:
- name: http
containerPort: 9090
volumeMounts:
- name: config
mountPath: /etc/prometheus
readOnly: true
- name: data
mountPath: /prometheus
readinessProbe:
httpGet:
path: /-/ready
port: http
volumes:
- name: config
configMap:
name: prometheus-config
- name: data
persistentVolumeClaim:
claimName: prometheus-data
---
apiVersion: v1
kind: Service
metadata:
name: prometheus
namespace: monitoring
spec:
selector:
app: prometheus
ports:
- name: http
port: 9090
targetPort: http
说明:
- 单副本起步够用;多副本/HA(remote write、Thanos 等)是规模化后的事,注意”先跑稳再扩展”
- configMap 变更流程:
kubectl apply更新 ConfigMap ->curl -X POST http://<svc>:9090/-/reload-> 验证 targets
8. 日志或指标观察方法
8.1 看板使用节奏
- 事件响应时:按 8.3 的 30 秒分诊表走
- 日常巡检(每日 1 次,5 分钟):概览 Row 全绿 -> 容量 Row 的余量值 -> GPU Row 的 ECC/温度 -> 完
- 周会:容量趋势(KV 占用率周均值曲线)、告警统计(条数/误报条数)、SLO 燃尽
8.2 曲线形态判读(经验集)
- KV 占用率:
- 平稳平台 = 健康;平台值逐周上爬 = 流量结构变化,容量规划要重做
- 深锯齿(贴顶后骤降)= 长请求周期性到达,查业务侧定时任务
- 阶梯状上移不回落 = 请求堆积或泄漏嫌疑,结合 running 数判断
- TTFT p99:
- 尖刺后迅速回落 = 单个长 prompt 或瞬时排队,看 waiting 是否同步
- 台阶式上移不回落 = 结构性变化(流量变长/并发变多/框架发布),先对变更时间线
- 全时段缓升 = 硬件劣化嫌疑(降频/温度),转 GPU Row 交叉验证
- GPU util:
- 100% 且吞吐不涨 = 小 batch/通信瓶颈/降频,分头查
- 突降 = 服务重启、流量摘除或硬件故障,先确认变更再怀疑硬件
- 前缀命中率(SGLang):
- 多轮场景应稳定在高位;骤降先查”chat template/系统提示词是否变更”,再查 KV 池被打爆
8.3 30 秒分诊表
| 现象 | 第一眼看 | 第二眼看 | 第三眼看 | 假设 |
|---|---|---|---|---|
| TTFT 突增 | 概览 TTFT 面板 | 引擎 Row 排队 | GPU 温度/降频 | 容量/长请求/硬件 |
| 5xx 升高 | 概览 5xx | 引擎是否 down | 网关自身资源 | 引擎挂/过载/网关 |
| 吞吐下降 | 概览 QPS | 请求量是否同步降 | TPOT/KV | 流量降 vs 性能降 |
| GPU 单卡 util 异常 | GPU Row heatmap | 该卡温度/功耗 | 该卡上的服务实例 | 硬件/调度不均 |
| 显存缓慢爬升 | GPU 显存曲线 | running 数 | 请求长度分布 | 泄漏/长请求堆积 |
| 命中率骤降 | 命中率面板 | 变更时间线 | KV 占用率 | 模板变更/池满 |
8.4 日志与指标的映射
引擎日志信号(grep 关键词)与指标的对应关系,排障时互相对证:
| 日志信号 | 对应指标表现 |
|---|---|
| preempt / recompute | KV 占用率贴顶 + TTFT/TPOT 抖动 |
| Aborted request | 5xx 或超时率上升,waiting 高 |
| OOM(启动期) | up 掉线 + 显存曲线断崖 |
| NCCL 报错 | 多卡 TP 场景吞吐骤降,util 单卡偏高 |
| Xid(dmesg NVRM) | ECC/Xid 告警 + 该卡 util 异常 |
原则:日志给”事件”,指标给”趋势”,根因结论必须两者对齐,单靠任一都会误判(如”Aborted”可能是客户端超时而非服务端故障,要看 waiting 与延迟分布)。
8.5 周报指标口径
- 容量余量:7 天 KV 占用率 p95(与 2 周前对比)
- 性能:TTFT/TPOT p95 周均值(分 variant)
- 告警质量:触发条数、误报条数(无需处理)、平均响应时长
- SLO:可用性百分比、错误预算剩余
- 硬件:ECC SBE/DBE 增量、温度 p95、降频时长
9. 排查路径
9.1 指标/面板没有数据
现象:Grafana 面板 No data,或 Prometheus 里查不到某指标。
排查步骤:
- 分层定位:是”没采到”还是”查错了”
curl -s $PROM/targets:target 是否 up。down -> 网络/端口/容器,先修采集- target up 但无该指标:
curl -s http://<target>/metrics | grep <指标名>在源头查
- 源头有、Prometheus 没有:
curl -s $PROM/series --data-urlencode 'match[]={__name__=~"vllm:.*"}':确认抓到的指标全集- scrape_interval 未到(新 target 首个样本要等一个周期)
- 该指标是恒 0 的 gauge:PromQL 里
max(...)结果 0 容易被误认为无数据
- 源头也没有(框架版本差异):
- vLLM/SGLang:升级镜像(钉 tag)后重启验证
- TRT-LLM 旧版无 /metrics:两条路——升级 trtllm-serve 版本(首选);临时方案在网关层补采(QPS/延迟/5xx)+ DCGM 兜底 GPU 层,引擎内部指标记为”监控盲区”列入技术债
- Grafana 层:数据源连接、变量($framework/$variant)取值是否为空、时间范围、query 语法(浏览器 console 看 PromQL 报错)
- 结论落档:修完后把”根因 + 修复 + 耗时”记入运维文档,同类问题第二次出现就是流程缺陷
9.2 TTFT 突增
现象:概览 TTFT p99 面板台阶式或尖刺式上升,业务反馈”变慢了”。
排查步骤:
- 定界:突增是全 variant 还是单一 variant?是否伴随 5xx?
- 单 variant:该 variant 的容量/发布问题;全 variant:网关层或公共依赖(DNS/存储/机房网络)
- 查排队:
llm:queue_max:1m与 TTFT 曲线对齐- 排队同步上升 -> 容量不足路径(9.3)
- 排队正常 -> 继续 3
- 查请求结构:网关侧输入长度分布(业务埋点或引擎 token 计数增速)
- 长 prompt 占比突增 -> 业务侧变化(新模板/新场景),属正常负载变化,容量要重算
- 请求结构无变化 -> 继续 4
- 查硬件:GPU 温度、降频位图、功耗
- 降频非 0 或温度 >85 -> 硬件/散热路径(9.6)
- 硬件正常 -> 继续 5
- 查变更:对最近 24-48h 的框架镜像/参数/量化版本/网关配置变更时间线
- 有变更 -> 回滚验证(回滚后 TTFT 回落即定位)
- 无变更 -> 拉引擎日志 grep preempt/abort 抽样请求 ID 复现
- 长尾分析:
histogram_quantile(0.999, ...)与 p99 对比,判断是”整体变慢”还是”尾部个别请求”(尾部问题多为长 prompt/抢占,整体变慢多为容量/硬件)
9.3 KV cache 持续高位与 preempt
现象:KV 占用率长期 >0.95,引擎日志出现 preempt/recompute,TTFT/TPOT 抖动。
排查步骤:
- 确认”高水位”的性质:稳态高(一直 0.95+)还是冲高回落(峰 0.99 谷 0.7)
- 稳态高 = 容量配置错了(按错负载设计);冲高回落 = 峰谷差过大,削峰或扩容
- 对账流量:网关 QPS 与请求长度分布 vs 容量设计假设
- 流量超假设 -> 走扩容(副本/卡)或限流预案
- 流量未超假设 -> 容量公式重算(平均 token 占用变了?上下文上限被调大了?)
- 参数对账:
--max-model-len是否被调大到超出业务真实需求(每翻倍,容量减半) - 短效手段(缓解不解决):
- 收缩上下文上限到业务 P99 长度 x 1.2(需发布,走变更)
- 网关层对超长请求限流/拒绝(保护整体体验)
- 临时降低
--max-num-seqs(压 TPOT 上限,牺牲部分吞吐)
- 长效手段:扩容副本、KV FP8(精度回归后)、模型侧量化(见量化专题文章)
- 验证:处置后 24h 观察 KV 占用率回到 <0.85 且 preempt 日志清零
9.4 吞吐下降
现象:请求量(QPS)不变,总吞吐(output tok/s)下降;或 QPS 与吞吐同步下降。
排查步骤:
- 先分两种病:
- QPS 降 + 吞吐同步降 = 需求侧变化(业务流量减少),不是故障,确认业务侧即可
- QPS 稳 + 吞吐降 = 供给侧劣化,继续 2
- 查 TPOT:TPOT p95 是否上升
- 上升 -> 单请求变慢:KV 水位(9.3)、硬件降频(9.6)、batch 组织变化(max_num_seqs 被调小?)
- 正常 -> 输出变短了?对账平均 completion 长度(num_generation_tokens / 请求数),输出变短 = 吞吐自然降(业务侧截断/模型行为变化)
- 查 GPU:util 是 100% 还是不满
- 不满且吞吐降 = 有请求进不来(排队/拒绝)或单请求变慢
- 满且吞吐降 = 算力被低效占用(9.5)
- 查变更:镜像/参数/量化版本/网关权重(流量是否被分流走)
- 查硬件:ECC/温度/降频(9.6)
- 结论必须带数据链:哪个环节变、变了多少、与哪个变更/负载变化对齐
9.5 GPU util 高但吞吐上不去
现象:DCGM util 持续 90-100%,但总吞吐低于基线 20%+。
排查步骤:
- 看 decode/prefill 结构:输入输出 token 吞吐比 vs 基线
- prefill 占比升高(长 prompt 变多)= util 高但 decode 产出少,属负载结构变化
- 结构无变化 -> 继续
- 看 batch 规模:running 请求数是否贴 max_num_seqs
- 贴顶且 util 高 = 容量设计问题(batch 够大但上下文太长,单请求吃显存)
- running 低且 util 高 = 小 batch 高占用:单请求长上下文 or TP 通信开销大
- 看 TP 通信:多卡场景下
nvidia-smi topo -m确认互联(PCIe 下 TP2 吞吐显著低于 NVLink);NCCL 日志有无重传/降速 - 看降频:
DCGM_FI_DEV_CLOCK_THROTTLE_REASONS非 0 = util 高是”假象”(实际时钟被压) - 看张量核活跃率(PROF_PIPE_TENSOR_ACTIVE 可用时):util 高但 tensor 活跃率低 = 大量时间花在非 GEMM 路径(kernel 选择/量化 fallback 嫌疑,结合 9.2 的变更对账)
- 处理按归因走:负载结构 -> 容量/网关分流;通信 -> 调度亲和(同 Pod 多卡同 NUMA)或换互联更好的机型;降频 -> 9.6
9.6 单卡高温 / ECC / Xid(硬件路径)
现象:GPUTempHigh / GpuThrottling / EccDoubleBitError / XidError 任一触发,或单卡 util/功耗曲线与同机其他卡明显偏离。
排查步骤:
- 信息收集(在摘流量之前):
nvidia-smi -q -d ECC,PERFORMANCE(该卡全量 ECC 与降频信息)dmesg | grep -Ei 'nvrm|xid' | tail -40(Xid 码与时间)- DCGM 面板截图:温度/功耗/时钟 24h 曲线
- 分级处置:
- SBE(单比特)少量增长:记录观察,纳入周巡检
- SBE 快速增长 / DBE 任何增长 / Xid 79 类:该卡视为故障件
- 故障件摘除(高风险操作,按序执行):
- 网关层将该卡上的实例权重置 0(或 K8s 驱逐该节点上对应 Pod:先
kubectl drain前先 cordon,确认流量已切再驱逐) - 验证:该实例 QPS 归零、其他实例承接后 SLO 指标正常
- 重启该卡上的推理容器(排掉软件态残留),观察 Xid 是否复现
- 网关层将该卡上的实例权重置 0(或 K8s 驱逐该节点上对应 Pod:先
- 报修:带 Xid 码、ECC 计数、时间线提交硬件工单;换卡后重跑三件套(health/冒烟/metrics)再恢复流量
- 复盘:该卡从故障到摘除的时长、期间用户影响(TTFT/5xx 数据链)、监控是否早于用户发现(这是监控价值的直接度量)
9.7 告警风暴与误报治理
现象:一次事件几十条告警刷屏,或某告警每周固定误报。
排查步骤:
- 风暴处理(事件期间):
- 先按 root cause 收敛:Alertmanager 的 silence(按 alertname+instance 精确静默次生告警),保留根因告警
- 不要 blanket silence 整个 job(会把真故障一起闷掉)
- 风暴归因(事件后 24h 内):
- 次生告警清单:哪些告警其实描述的是同一个根因(如引擎 down 连带排队/5xx/TTFT 全响)
- 收敛手段:依赖关系抑制(inhibit:LLMEngineDown 触发时抑制该实例的 QueueBuildup/TTFTBreach)——Alertmanager inhibit 规则按实际告警名配置,配置后进 git
- 重复阈值问题:同根因不同 instance 刷屏 -> group_by 加 instance 前缀或调整分组
- 固定误报治理:
- 找规律:时间规律(每天 09:00 流量启动瞬间)-> 阈值 for 时长加大或加”流量存在”前置条件(
and组合:QPS > X 才评估延迟告警,防低峰期小样本误报) - 低峰期延迟类告警天然敏感(请求少、单请求影响大),低峰用”绝对量”指标(waiting 数)替代”分位数”指标是常用手法
- 找规律:时间规律(每天 09:00 流量启动瞬间)-> 阈值 for 时长加大或加”流量存在”前置条件(
- 度量:误报率 = 无需处理告警 / 总告警,目标 <10%;每月复盘调整,调整后记入阈值台账(谁、何时、依据什么数据改的)
10. 风险提醒
- 告警规则变更是生产变更:alerts.yml 进 git,变更走评审(至少一人复核阈值依据),先预发/压测环境验证触发行为再上生产
- 监控自身故障的边界:Prometheus/Grafana 挂掉不影响推理服务运行(被监控侧零依赖),但会制造”指标盲区”——盲区期间发生的事件只能靠日志与事后数据恢复分析。ScrapeDown 告警必须有人响应
- 磁盘风险:TSDB 写满会导致 Prometheus 不可用甚至影响所在盘的其他数据。retention 按磁盘余量设(建议为监控数据预留磁盘的 50% 以上),磁盘 80% 水位告警(node-exporter 提供)
- series 基数爆炸:给 target 加高基数自定义标签(用户、会话)会让 series 数指数增长,拖垮 TSDB。自定义标签只允许低基数枚举值(variant/cluster/service)
- 通知链路的单点:relay 服务挂 = 告警静默丢失。relay 要有存活监控(一个独立于主告警链路的简单 ping/端口检查,走不同出口如邮件)
- 明文密钥:IM 机器人 token、(如用)邮件 SMTP 密码,一律放环境变量或权限 600 的受控文件,不进 git;进过 git 的按泄露处理(轮换 token)
- 压测校准的影响范围:5.10 的压测若打在共享生产集群,会挤占容量并产生告警噪声。必须:低峰窗口 + 提前 silence + 压测流量打标(便于事后从指标里分离)
- 摘流量/驱逐节点类操作(9.6):执行前确认副本余量(摘除后剩余实例能承接峰值),无余量时先扩容再摘
- 批量脚本:本文涉及的批量操作(多 target 检查、批量静默)都应带日志与 dry-run 习惯,参考前文脚本约定
11. 验证方式
监控体系上线验收清单(逐项打勾才允许下线”人工盯盘”):
- 采集层
- targets 全部 up,连续 24h 无断流(
up == 0次数为 0) - 5.5 的 10 条关键查询全部有值
- DCGM 每卡指标齐全(gpu 标签 0…N-1 全覆盖)
- targets 全部 up,连续 24h 无断流(
- 看板层
- 面板覆盖 3.2 清单全部指标,无 No data
- 变量切换(framework/variant)后所有面板正常刷新
- 手动触发一次”可见异常”(如压测打满排队),确认相关面板形态符合预期
- 告警层
- 每条 critical 告警用测试方法触发并收到通知(5.9 的测试告警法逐条过,或临时把阈值改到必然触发 + 精确 silence 窗口,验完即恢复并验证恢复通知)
- resolved 通知正常到达
- inhibit/silence 配置生效验证
- 容量与性能
- 30 天 retention 生效(
curl $PROM/status看 retention 与磁盘估算) - 记录规则全部有值
- 30 天 retention 生效(
- 流程
- on-call 手册覆盖每条告警
- 一次演练:模拟”引擎 down”(测试环境停容器),从告警响起到定位 <= 目标时长(如 10 分钟),演练记录存档
12. 回滚方案
场景一:新告警规则上线后误报严重。
- 评估影响:当前该规则组贡献的误报占比(Alertmanager API 可查活跃告警与历史,或面板统计)
- 回滚:alerts.yml git revert ->
promtool check rules->kubectl apply/docker cp 更新 ->POST /-/reload - 验证:reload 后该规则组不再触发(故意构造条件确认规则确实被移除而非静默失效)
- 教训落档:误报根因(阈值/for 时长/低峰敏感性)写入阈值台账
场景二:Prometheus 配置改错导致大面积 scrape 失败。
docker logs prometheus/ K8s Pod 日志确认配置错误行- 回滚到上一版本配置(git),reload
curl $PROM/targets确认全 up,up == 0的告警随恢复自动 resolved- 检查断流时段的告警空洞,评估期间是否有”看不见的事件”(翻引擎日志对账)
场景三:Grafana 看板改坏。
- 看板 JSON 版本化(git),回滚 = 重新导入上一版本 JSON(覆盖同名 dashboard)
- 验证核心面板数据与 PromQL 直查一致
回滚红线:
- 任何监控配置回滚都不应触碰推理服务本身(监控侧零侵入原则的验证)
- 回滚后必须验证”恢复通知”链路(告警从 firing 到 resolved 的完整闭环)
- 断流时段的告警空洞必须人工对账日志,不允许默认”那段时间没事”
13. 生产环境注意事项
版本与变更:
- 所有镜像 tag 钉死版本;升级走”预发验证 -> 低峰发布 -> 观察 24h”
- 配置(prometheus/alerts/recording/alertmanager/看板 JSON)全部 git 管理,目录化,一次变更一个 commit,message 写依据
- 告警阈值变更单独走变更单(影响的是”谁会被叫醒”,属于生产影响面)
容量与存储:
- TSDB 容量按 series 数估算:先
curl $PROM/admin/tsdb/status(或 /api/v1/status/tsdb)看当前 series 与增长速率,再外推 30 天 - 指标保留策略:默认全量 30 天;如需更长时间的低分辨率数据,评估 remote write + 二级存储(规模化后再做,不阻塞上线)
- 高基数指标定期审计:
topk(20, count by (__name__)({__name__=~".+"}))找 series 大头(按指标名聚合 series 数取前 20),逐个判断是否需要 relabel 丢弃
on-call 纪律:
- 每条 critical 告警有且只有一个”第一响应人”角色(on-call 排班),响应 SLA(如 15 分钟内认领)
- 告警响应记录模板:告警名 / 响应时长 / 定位结论 / 处置动作 / 是否误报。月度复盘的原料
- 发布窗口与压测窗口必须挂 silence(提前、精确、带理由),禁止”响了再静”
容量余量运营:
- 每周看”KV 占用率 p95 + 队列 p95″两条曲线,两者同时上爬 = 扩容窗口到了(别等 critical 才扩)
- 扩容预案常备:副本扩容命令、限流预案、上下文收缩预案,三件套写进 on-call 手册并每季度演练一次
安全:
- Prometheus/Grafana 只在内网暴露;Grafana 开基本认证,禁止匿名只读对外
- /metrics 端点本身无鉴权(多数框架如此):确保推理服务端口不暴露到不受信网络(安全组/网络策略),这是”零侵入”方案的安全边界
- relay 服务(webhook 出口)是告警内容的出口,其日志可能含业务实例名等敏感信息,按敏感日志管理
14. 常见问题(FAQ)
Q1:GPU util 100% 是不是说明服务健康?
A:恰恰相反,util 100% 只能说明”卡一直有活干”,不说明干得好。util 满 + 吞吐达标 + 延迟达标 = 健康的满载;util 满 + 吞吐不达基线 = 算力被低效占用(小 batch/长 prefill/降频/通信瓶颈)。util 必须和吞吐、延迟一起读。
Q2:vLLM 和 SGLang 的指标名不同,面板怎么做成一套?
A:两条路。推荐:用 Grafana 变量 + 按 variant 分数据源查询(每个 variant 的面板写自己的指标名,查询里按 $framework 选);进阶:在 Prometheus 层做 metric relabel(把两套名字映射成统一命名,如统一成 llm_kv_usage)再统一查询。relabel 要注意别把两套不同语义的指标强行同名(如 KV 占用率的计算口径可能略有差异),映射前逐指标核对语义。
Q3:TensorRT-LLM 旧版没有 /metrics,怎么补救?
A:优先级排序:升级框架版本获得原生 /metrics(首选,一次性成本);其次在网关层补全”业务面”指标(QPS/延迟/5xx/排队——网关能看到的)+ DCGM 补”硬件面”;引擎内部指标(KV 池、preempt)记为监控盲区,写进技术债清单并排升级计划。不建议为此写侵入式 sidecar(维护成本高于收益)。
Q4:告警阈值到底怎么定才不拍脑袋?
A:五步法:1) 压测拿到三档负载(70/90/110%)下的指标带;2) 定 SLO(与业务方书面确认);3) critical = SLO 受损线,warning = 正常操作区间边缘(通常为 SLO 线的 0.7-0.8 倍距离处);4) for 时长按”滤掉可自恢复抖动”设定(延迟类 10m,容量类 5-10m,硬件类 0);5) 上线 2 周后按生产分布复核一轮,之后每月一轮。阈值台账记录每一步的依据。
Q5:低峰期告警特别多,是不是阈值不对?
A:延迟/分位数类指标在低峰期天然敏感(样本少,单个慢请求就能拉高 p99)。三种处理:低峰时段切用绝对量指标(waiting 数、KV 占用率——与请求量弱相关);给延迟告警加前置条件(QPS 高于某值才评估,PromQL 用 and 组合);按小时段分两套阈值(Alertmanager 的 timeinterval 能力,按实际版本支持情况使用)。不建议简单把阈值调大掩盖问题。
Q6:输出质量(回答变傻)能进 Prometheus 监控吗?
A:常规 Prometheus 指标监控不了”质量”。工程上常用三层防线:1) 网关/业务侧埋点”低分回答”计数(用户点踩、下游 LLM 评审抽样打分回传),做成自定义指标进 Prometheus(自定义指标名以实际实现为准);2) 周度黄金集自动回归(见量化专题文章的评估流水线),得分进看板;3) 用户反馈通道聚合。三层都要有,Prometheus 只承载第一层。
Q7:30 天数据到底占多大磁盘?
A:经验值:中小规模(百级 series,15s 间隔)通常几个 GB;千级 series 几十 GB 量级;series 上十万里(高基数标签搞出来的)就是上百 GB 且伴随查询变慢。上线前用 topk 审计 series,扩容规划按”series 数 x 保留天数”外推,别按”感觉”。
Q8:多模型、多实例(7B/32B、vLLM/SGLang 混部)怎么组织指标?
A:靠 label 而不是靠多套 Prometheus。约定:job(框架)/ instance(实例)/ service(业务服务)/ variant(模型+量化组合)四个维度。面板用变量按 service/variant 过滤;容量表按 variant 分条。切记 variant 的值是低基数枚举(“7b-awq”、“32b-fp8”),不要写成完整模型路径(高基数)。
Q9:/metrics 没鉴权,安全吗?
A:指标本身不含请求内容(只有聚合计数与分位数),风险主要是”基础设施指纹”(端口/框架/版本)与”业务量信息”(QPS/吞吐暴露运营数据)。处置:推理服务端口限制在受信网络(安全组/K8s NetworkPolicy),不做公网暴露;多租户环境在网关层按租户隔离指标可见性(网关自建 exporter 时)。不建议为加鉴权改框架(维护成本),网络隔离是正路。
Q10:监控体系建好后,多久做一次全面复盘?
A:节奏建议:每周 15 分钟看板巡检(容量/告警统计);每月一次告警质量复盘(误报率、漏报、阈值校准、series 审计);每季度一次容量规划重算(流量结构变化)+ on-call 预案演练(引擎 down / GPU 故障 / 容量打满三个剧本)。重大变更(框架升级、量化切换、机型更换)后加做一轮基线重采。
15. 总结
推理服务的监控告警,核心是三件事:
- 三层指标交叉验证:网关层定”用户感知”,引擎层定”内部状态”,硬件层定”物理健康”。任何单层结论都不许直接处置,跨层对齐才是根因
- 阈值来自基线:压测三档 + 生产两周 + 月度校准,阈值台账记录每一步依据。告警体系的价值是真报率,不是规则数量,误报率 >30% 的体系等于没有体系
- 闭环要有演练:从告警响起 -> 分诊表 -> 处置 -> 复盘 -> 阈值/规则修订,每季度把”引擎 down / GPU 故障 / 容量打满”三个剧本走一遍。没演练过的预案等于没有预案
落地的最短路径:先接引擎 + DCGM 两层(半天)-> 看板四 Row(一天)-> 三条 critical 告警 + 通知链路(半天)-> 压测校准 + 全量告警(一周内完成)-> 月度复盘常态(长期)。

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

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




网友评论comments