首页 运维杂谈大模型推理监控告警实战:Prometheus、Grafana 与关键指标

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

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

1. 问题背景

大模型推理服务和传统 Web 服务的监控,差异比看起来大得多。传统服务盯 QPS、RT、错误率就覆盖了大部分风险;推理服务至少多三个维度:

  • 请求时长是”秒到分钟”级,传统 RT 告警(比如 p99 > 500ms)在推理服务上要么天天响要么永远不响
  • 有 GPU 这个新硬件维度:温度、降频、ECC 错误、显存碎片,任何一项异常都会直接打低吞吐,且现象常表现为”性能慢慢变差”
  • 容量模型不同:并发上限由 KV cache 池决定,”显存没满但并发上不去”是常态,传统”CPU 80% 告警”式直觉全部失效

线上真实发生过的三类典型故障,恰好对应三个监控盲区:

  1. 容量未盯:业务流量增长,KV cache 占用率长期 95%+,preempt/recompute 频发,用户侧表现为”偶发很慢”,持续两周才定位
  2. 硬件静默劣化:某张卡 ECC 错误缓慢累积 + 温度偏高降频,吞吐一周内下降 15%,因为”没有报错”一直没查
  3. 质量静默退化:某次框架升级后 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_runningsglang:num_running_requests正在解码的请求数低于 max_num_seqs长期贴顶
vllm:num_requests_waitingsglang:num_queue_reqs排队请求数0 或个位数持续增长
vllm:gpu_cache_usage_percsglang:token_usageKV cache 占用率<0.8(依负载)长期 >0.95
vllm:time_to_first_token_secondssglang:time_to_first_token_secondsTTFT 直方图p99 在 SLO 内p99 突增
vllm:time_per_output_token_secondssglang:inter_token_latency_seconds单 token 耗时直方图p95 稳定持续爬升
vllm:e2e_request_latency_secondssglang:e2e_request_latency_seconds端到端延迟与请求长度分布匹配同长度请求延迟漂移
vllm:num_prompt_tokenssglang:num_prompt_tokens累计输入 token增速与 QPS 匹配增速突变(流量异常)
vllm:num_generation_tokenssglang:num_generated_tokens累计输出 token同上吞吐骤降
-(前缀命中相关)sglang:cache_hit_rate前缀缓存命中率多轮场景 >0.5骤降(模板变了/缓存被打爆)

硬件层指标(DCGM exporter 提供,名称稳定):

指标含义正常表现异常信号
DCGM_FI_DEV_GPU_UTILGPU 利用率 %与负载匹配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_ERRORSXid 错误累计0任何增长查 dmesg 定位
DCGM_FI_DEV_CLOCK_THROTTLE_REASONS降频原因位图0非 0 持续(热/功耗/硬件)
DCGM_FI_PROF_PIPE_TENSOR_ACTIVETensor 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 告警哲学:基线、阈值、误报率

三条纪律,比任何单条规则都重要:

  1. 阈值必须来自基线:压测基线(同口径流量下的指标带)+ 上线后 2 周生产数据(各指标的 p50/p95/p99 分布)。没有基线就上的告警,前两周一定全是噪声,会毁掉整个告警体系的可信度
  2. 分级而非一刀切:warning(容量/性能开始偏离,工作时间内看)与 critical(SLO 受损/用户可感知/硬件故障,立即响应)。critical 的触发条件必须”响了就要动”,否则升级机制失效
  3. 误报率要度量:每月统计”触发但无需处理”的告警占比,>30% 说明阈值要校准。告警体系的价值 = 真报率 x 响应时效,不是规则数量

告警分组与收敛(Alertmanager 层):

  • group_by: [alertname, severity](或加 instance 前缀,看规模)
  • group_wait 30s 合并同组,repeat_interval warning 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面板查询口径(示例)
概览请求 QPSsum(rate(nginx_ingress_controller_requests[5m])) 或引擎层 request 计数器
概览TTFT p95 / p99histogram_quantile(0.95/0.99, sum(rate(_bucket[5m])) by (le))
概览TPOT p95histogram_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 高亮)
GPUECC 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 校准后定稿):

告警级别条件(示例)逻辑
引擎不可达criticalup{job=“vllm”} == 0 for 3m服务挂/抓取挂,先查服务再查网络
排队堆积warningllm:queue_max:1m > 4 for 5m4 为示例值,按”设计并发的 10%”定
排队堆积criticalllm:queue_max:1m > 16 for 3m按”设计并发的 50%”定
KV 高水位warningllm:kv_usage:1m > 0.85 for 10m容量预警线
KV 高水位criticalllm:kv_usage:1m > 0.97 for 5mpreempt 高发线
TTFT 超 SLOcriticalllm:ttft_p99:5m > 2 for 10mSLO 受损
TPOT 超 SLOwarningllm:tpot_p95:5m > 0.05 for 10m体验劣化
GPU 高温warningmax(DCGM_FI_DEV_GPU_TEMP) > 85 for 5m散热/负载问题
GPU 降频warningmax(DCGM_FI_DEV_CLOCK_THROTTLE_REASONS) > 0 for 10m位图非 0 即有原因,查 thermal/power
ECC 双比特错误criticalllm:ecc_dbe_inc_1h > 0任何增长即硬件风险,不等量级
Xid 错误criticalincrease(DCGM_FI_DEV_XID_ERRORS[10m]) > 0任何增长查 dmesg
5xx 率critical5xx 占比 > 0.01 for 5m网关层,与引擎层交叉验证
抓取失败warningup{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: 30srepeat_interval warning 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 压测校准阈值

目的:用受控压测把”告警阈值”从拍脑袋变成有数据支撑的数字。

步骤:

  1. 按设计负载的 70%、90%、110% 三档各压测 2-4 小时(低峰窗口,不抢生产流量;生产环境用独立压测机与影子服务)
  2. 每档记录(面板截图 + CSV 导出):queue、KV 占用率、TTFT/TPOT p95/p99、GPU util/temp、吞吐
  3. 填基线表(模板):
指标70% 负载 p9590% 负载 p95110% 负载 p95告警阈值定稿定稿依据
queue max
KV 占用率
TTFT p99
TPOT p95
GPU temp
  1. 阈值落进 alerts.yml(git 提交,变更单附基线表),reload 生效
  2. 上线后 2 周生产数据复核一次:生产分布与压测分布的偏差 >20% 的指标,重新定阈值
  3. 之后每月复盘一次(误报率、漏报、阈值漂移)

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 / recomputeKV 占用率贴顶 + TTFT/TPOT 抖动
Aborted request5xx 或超时率上升,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 里查不到某指标。

排查步骤:

  1. 分层定位:是”没采到”还是”查错了”
    • curl -s $PROM/targets:target 是否 up。down -> 网络/端口/容器,先修采集
    • target up 但无该指标:curl -s http://<target>/metrics | grep <指标名> 在源头查
  2. 源头有、Prometheus 没有:
    • curl -s $PROM/series --data-urlencode 'match[]={__name__=~"vllm:.*"}':确认抓到的指标全集
    • scrape_interval 未到(新 target 首个样本要等一个周期)
    • 该指标是恒 0 的 gauge:PromQL 里 max(...) 结果 0 容易被误认为无数据
  3. 源头也没有(框架版本差异):
    • vLLM/SGLang:升级镜像(钉 tag)后重启验证
    • TRT-LLM 旧版无 /metrics:两条路——升级 trtllm-serve 版本(首选);临时方案在网关层补采(QPS/延迟/5xx)+ DCGM 兜底 GPU 层,引擎内部指标记为”监控盲区”列入技术债
  4. Grafana 层:数据源连接、变量($framework/$variant)取值是否为空、时间范围、query 语法(浏览器 console 看 PromQL 报错)
  5. 结论落档:修完后把”根因 + 修复 + 耗时”记入运维文档,同类问题第二次出现就是流程缺陷

9.2 TTFT 突增

现象:概览 TTFT p99 面板台阶式或尖刺式上升,业务反馈”变慢了”。

排查步骤:

  1. 定界:突增是全 variant 还是单一 variant?是否伴随 5xx?
    • 单 variant:该 variant 的容量/发布问题;全 variant:网关层或公共依赖(DNS/存储/机房网络)
  2. 查排队:llm:queue_max:1m 与 TTFT 曲线对齐
    • 排队同步上升 -> 容量不足路径(9.3)
    • 排队正常 -> 继续 3
  3. 查请求结构:网关侧输入长度分布(业务埋点或引擎 token 计数增速)
    • 长 prompt 占比突增 -> 业务侧变化(新模板/新场景),属正常负载变化,容量要重算
    • 请求结构无变化 -> 继续 4
  4. 查硬件:GPU 温度、降频位图、功耗
    • 降频非 0 或温度 >85 -> 硬件/散热路径(9.6)
    • 硬件正常 -> 继续 5
  5. 查变更:对最近 24-48h 的框架镜像/参数/量化版本/网关配置变更时间线
    • 有变更 -> 回滚验证(回滚后 TTFT 回落即定位)
    • 无变更 -> 拉引擎日志 grep preempt/abort 抽样请求 ID 复现
  6. 长尾分析:histogram_quantile(0.999, ...) 与 p99 对比,判断是”整体变慢”还是”尾部个别请求”(尾部问题多为长 prompt/抢占,整体变慢多为容量/硬件)

9.3 KV cache 持续高位与 preempt

现象:KV 占用率长期 >0.95,引擎日志出现 preempt/recompute,TTFT/TPOT 抖动。

排查步骤:

  1. 确认”高水位”的性质:稳态高(一直 0.95+)还是冲高回落(峰 0.99 谷 0.7)
    • 稳态高 = 容量配置错了(按错负载设计);冲高回落 = 峰谷差过大,削峰或扩容
  2. 对账流量:网关 QPS 与请求长度分布 vs 容量设计假设
    • 流量超假设 -> 走扩容(副本/卡)或限流预案
    • 流量未超假设 -> 容量公式重算(平均 token 占用变了?上下文上限被调大了?)
  3. 参数对账:--max-model-len 是否被调大到超出业务真实需求(每翻倍,容量减半)
  4. 短效手段(缓解不解决):
    • 收缩上下文上限到业务 P99 长度 x 1.2(需发布,走变更)
    • 网关层对超长请求限流/拒绝(保护整体体验)
    • 临时降低 --max-num-seqs(压 TPOT 上限,牺牲部分吞吐)
  5. 长效手段:扩容副本、KV FP8(精度回归后)、模型侧量化(见量化专题文章)
  6. 验证:处置后 24h 观察 KV 占用率回到 <0.85 且 preempt 日志清零

9.4 吞吐下降

现象:请求量(QPS)不变,总吞吐(output tok/s)下降;或 QPS 与吞吐同步下降。

排查步骤:

  1. 先分两种病:
    • QPS 降 + 吞吐同步降 = 需求侧变化(业务流量减少),不是故障,确认业务侧即可
    • QPS 稳 + 吞吐降 = 供给侧劣化,继续 2
  2. 查 TPOT:TPOT p95 是否上升
    • 上升 -> 单请求变慢:KV 水位(9.3)、硬件降频(9.6)、batch 组织变化(max_num_seqs 被调小?)
    • 正常 -> 输出变短了?对账平均 completion 长度(num_generation_tokens / 请求数),输出变短 = 吞吐自然降(业务侧截断/模型行为变化)
  3. 查 GPU:util 是 100% 还是不满
    • 不满且吞吐降 = 有请求进不来(排队/拒绝)或单请求变慢
    • 满且吞吐降 = 算力被低效占用(9.5)
  4. 查变更:镜像/参数/量化版本/网关权重(流量是否被分流走)
  5. 查硬件:ECC/温度/降频(9.6)
  6. 结论必须带数据链:哪个环节变、变了多少、与哪个变更/负载变化对齐

9.5 GPU util 高但吞吐上不去

现象:DCGM util 持续 90-100%,但总吞吐低于基线 20%+。

排查步骤:

  1. 看 decode/prefill 结构:输入输出 token 吞吐比 vs 基线
    • prefill 占比升高(长 prompt 变多)= util 高但 decode 产出少,属负载结构变化
    • 结构无变化 -> 继续
  2. 看 batch 规模:running 请求数是否贴 max_num_seqs
    • 贴顶且 util 高 = 容量设计问题(batch 够大但上下文太长,单请求吃显存)
    • running 低且 util 高 = 小 batch 高占用:单请求长上下文 or TP 通信开销大
  3. 看 TP 通信:多卡场景下 nvidia-smi topo -m 确认互联(PCIe 下 TP2 吞吐显著低于 NVLink);NCCL 日志有无重传/降速
  4. 看降频:DCGM_FI_DEV_CLOCK_THROTTLE_REASONS 非 0 = util 高是”假象”(实际时钟被压)
  5. 看张量核活跃率(PROF_PIPE_TENSOR_ACTIVE 可用时):util 高但 tensor 活跃率低 = 大量时间花在非 GEMM 路径(kernel 选择/量化 fallback 嫌疑,结合 9.2 的变更对账)
  6. 处理按归因走:负载结构 -> 容量/网关分流;通信 -> 调度亲和(同 Pod 多卡同 NUMA)或换互联更好的机型;降频 -> 9.6

9.6 单卡高温 / ECC / Xid(硬件路径)

现象:GPUTempHigh / GpuThrottling / EccDoubleBitError / XidError 任一触发,或单卡 util/功耗曲线与同机其他卡明显偏离。

排查步骤:

  1. 信息收集(在摘流量之前):
    • nvidia-smi -q -d ECC,PERFORMANCE(该卡全量 ECC 与降频信息)
    • dmesg | grep -Ei 'nvrm|xid' | tail -40(Xid 码与时间)
    • DCGM 面板截图:温度/功耗/时钟 24h 曲线
  2. 分级处置:
    • SBE(单比特)少量增长:记录观察,纳入周巡检
    • SBE 快速增长 / DBE 任何增长 / Xid 79 类:该卡视为故障件
  3. 故障件摘除(高风险操作,按序执行):
    • 网关层将该卡上的实例权重置 0(或 K8s 驱逐该节点上对应 Pod:先 kubectl drain 前先 cordon,确认流量已切再驱逐)
    • 验证:该实例 QPS 归零、其他实例承接后 SLO 指标正常
    • 重启该卡上的推理容器(排掉软件态残留),观察 Xid 是否复现
  4. 报修:带 Xid 码、ECC 计数、时间线提交硬件工单;换卡后重跑三件套(health/冒烟/metrics)再恢复流量
  5. 复盘:该卡从故障到摘除的时长、期间用户影响(TTFT/5xx 数据链)、监控是否早于用户发现(这是监控价值的直接度量)

9.7 告警风暴与误报治理

现象:一次事件几十条告警刷屏,或某告警每周固定误报。

排查步骤:

  1. 风暴处理(事件期间):
    • 先按 root cause 收敛:Alertmanager 的 silence(按 alertname+instance 精确静默次生告警),保留根因告警
    • 不要 blanket silence 整个 job(会把真故障一起闷掉)
  2. 风暴归因(事件后 24h 内):
    • 次生告警清单:哪些告警其实描述的是同一个根因(如引擎 down 连带排队/5xx/TTFT 全响)
    • 收敛手段:依赖关系抑制(inhibit:LLMEngineDown 触发时抑制该实例的 QueueBuildup/TTFTBreach)——Alertmanager inhibit 规则按实际告警名配置,配置后进 git
    • 重复阈值问题:同根因不同 instance 刷屏 -> group_by 加 instance 前缀或调整分组
  3. 固定误报治理:
    • 找规律:时间规律(每天 09:00 流量启动瞬间)-> 阈值 for 时长加大或加”流量存在”前置条件(and 组合:QPS > X 才评估延迟告警,防低峰期小样本误报)
    • 低峰期延迟类告警天然敏感(请求少、单请求影响大),低峰用”绝对量”指标(waiting 数)替代”分位数”指标是常用手法
  4. 度量:误报率 = 无需处理告警 / 总告警,目标 <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. 验证方式

监控体系上线验收清单(逐项打勾才允许下线”人工盯盘”):

  1. 采集层
    • targets 全部 up,连续 24h 无断流(up == 0 次数为 0)
    • 5.5 的 10 条关键查询全部有值
    • DCGM 每卡指标齐全(gpu 标签 0…N-1 全覆盖)
  2. 看板层
    • 面板覆盖 3.2 清单全部指标,无 No data
    • 变量切换(framework/variant)后所有面板正常刷新
    • 手动触发一次”可见异常”(如压测打满排队),确认相关面板形态符合预期
  3. 告警层
    • 每条 critical 告警用测试方法触发并收到通知(5.9 的测试告警法逐条过,或临时把阈值改到必然触发 + 精确 silence 窗口,验完即恢复并验证恢复通知)
    • resolved 通知正常到达
    • inhibit/silence 配置生效验证
  4. 容量与性能
    • 30 天 retention 生效(curl $PROM/status 看 retention 与磁盘估算)
    • 记录规则全部有值
  5. 流程
    • on-call 手册覆盖每条告警
    • 一次演练:模拟”引擎 down”(测试环境停容器),从告警响起到定位 <= 目标时长(如 10 分钟),演练记录存档

12. 回滚方案

场景一:新告警规则上线后误报严重。

  1. 评估影响:当前该规则组贡献的误报占比(Alertmanager API 可查活跃告警与历史,或面板统计)
  2. 回滚:alerts.yml git revert -> promtool check rules -> kubectl apply/docker cp 更新 -> POST /-/reload
  3. 验证:reload 后该规则组不再触发(故意构造条件确认规则确实被移除而非静默失效)
  4. 教训落档:误报根因(阈值/for 时长/低峰敏感性)写入阈值台账

场景二:Prometheus 配置改错导致大面积 scrape 失败。

  1. docker logs prometheus / K8s Pod 日志确认配置错误行
  2. 回滚到上一版本配置(git),reload
  3. curl $PROM/targets 确认全 up,up == 0 的告警随恢复自动 resolved
  4. 检查断流时段的告警空洞,评估期间是否有”看不见的事件”(翻引擎日志对账)

场景三:Grafana 看板改坏。

  1. 看板 JSON 版本化(git),回滚 = 重新导入上一版本 JSON(覆盖同名 dashboard)
  2. 验证核心面板数据与 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 告警 + 通知链路(半天)-> 压测校准 + 全量告警(一周内完成)-> 月度复盘常态(长期)。

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

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

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

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

下一篇:

网友评论comments

发表回复

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

暂无评论

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