人工智能实战:大模型服务"看起来正常却突然变慢"?Prometheus + Grafana + GPU 指标构建全链路监控体系
一、问题场景:线上最怕的不是报错,而是"偶发性变慢"
在前面的系统优化中,我们已经完成了:
text
1. FastAPI 封装大模型推理接口
2. vLLM 提升并发吞吐
3. Redis 队列削峰填谷
4. 限流、熔断、降级保护系统
5. 多 GPU 提升推理能力
测试环境压测结果也比较漂亮:
text
平均延迟:1.2s
P95:2.8s
错误率:< 1%
GPU 利用率:70% ~ 85%
但真正上线后,业务侧反馈了一个很典型的问题:
text
"接口大多数时候很快,但偶尔会突然卡一下。"
"用户没有看到明确报错,但感觉系统不稳定。"
"日志里没有明显异常,但体验就是不好。"
这类问题最难排查。
因为它不是接口完全不可用,也不是服务直接崩溃,而是:
text
系统局部变慢,但你不知道慢在哪里。
一开始我也犯了一个错误:只去看应用日志。
结果日志里只有:
text
request success
request success
request success
看起来全是成功。
但用户体验并不好。
最后排查发现,问题不在接口逻辑,而在:
text
队列等待时间变长 + 某些请求 token 数过大 + GPU 短时间进入高负载
也就是说,问题发生在"链路中间",而不是最终错误日志里。
这也是大模型系统和普通 Web 系统最大的区别:
text
普通接口看错误日志,大模型系统必须看全链路指标。
二、真实问题:只看日志为什么定位不了大模型故障?
很多传统后端系统,排查问题主要看:
text
1. 应用日志
2. 错误堆栈
3. CPU / 内存
4. 慢 SQL
但大模型推理服务不一样。
一个请求进入系统后,完整链路通常是:
text
Client
↓
API Gateway
↓
参数校验
↓
Redis Queue
↓
Worker
↓
vLLM
↓
GPU
↓
返回结果
其中任何一层变慢,用户看到的都是:
text
接口响应慢
但应用日志只会告诉你:
text
最终成功或失败
它不会告诉你:
text
1. 请求在队列里等了多久
2. prompt 输入了多少 token
3. 输出生成了多少 token
4. vLLM 推理阶段耗时多少
5. GPU 利用率是否瞬间打满
6. P99 是否已经异常
7. 是否某类长文本请求拖慢了短请求
所以,排查大模型服务问题,不能只看日志,要建立一套可观测体系。
三、原因分析:大模型系统到底要监控什么?
很多人一上来就装 Prometheus、Grafana,然后采一堆指标。
但指标不是越多越好。
真正有用的指标,一定要能回答问题。
大模型服务至少要回答这 5 个问题:
text
1. 请求有没有变多?
2. 请求有没有变慢?
3. 慢在哪里?
4. GPU 是否成为瓶颈?
5. 队列是否已经积压?
所以我们需要按层设计指标。
四、指标分层设计
1. 接口层指标
接口层负责回答:
text
服务对用户表现如何?
核心指标:
text
QPS
成功率
错误率
平均延迟
P95 延迟
P99 延迟
注意,平均延迟只能看趋势,不能代表用户体验。
真正要盯的是:
text
P95 / P99
因为大模型服务经常出现:
text
平均值很好看,P99 已经爆炸
2. 请求内容指标
大模型请求不是普通 JSON 请求。
它的成本和以下因素强相关:
text
prompt 长度
input tokens
max_tokens
output tokens
同样是一次请求:
text
20 token 输入 + 64 token 输出
和:
text
2000 token 输入 + 512 token 输出
消耗完全不同。
所以必须记录:
text
输入 token 数
输出 token 数
总 token 数
3. 队列层指标
如果系统加了 Redis 队列,必须监控:
text
队列长度
任务等待时间
任务执行时间
任务失败数
队列长度非常关键。
因为它是系统过载最早出现的信号之一。
如果你只看接口响应时间,通常已经晚了。
4. 模型推理层指标
推理层需要关注:
text
模型调用耗时
生成速度 tokens/s
超时次数
失败次数
尤其是:
text
tokens/s
它比单纯的请求耗时更能反映模型真实性能。
5. GPU 层指标
GPU 层至少要监控:
text
GPU 利用率
显存使用量
显存使用比例
GPU 温度
GPU Power
但这里有个坑:
text
GPU 利用率高,不一定代表系统高效;
GPU 利用率低,也不一定代表系统空闲。
例如:
text
队列积压严重,但 Worker 没有正常消费
此时 GPU 利用率可能不高,但用户已经很慢。
所以 GPU 指标必须和队列指标、延迟指标一起看。
五、解决方案:Prometheus + Grafana + 自定义指标
这套方案的目标不是"装一个监控面板",而是构建一个可以定位问题的系统。
架构如下:
text
FastAPI / Worker
↓ 暴露 /metrics
Prometheus
↓ 采集指标
Grafana
↓ 展示趋势
Alertmanager
↓ 告警
本文先实现核心部分:
text
1. FastAPI 暴露接口指标
2. Worker 暴露任务指标
3. 采集 GPU 指标
4. Prometheus 抓取
5. Grafana 展示
6. 配置基础告警规则
六、可复现项目结构
text
llm-monitor-demo/
├── app.py
├── worker.py
├── metrics.py
├── gpu_metrics.py
├── requirements.txt
├── prometheus.yml
└── alert_rules.yml
七、安装依赖
bash
pip install fastapi uvicorn prometheus-client redis rq transformers
如果你只是复现监控逻辑,不需要真正加载大模型,可以先用 sleep 模拟推理。
八、定义统一指标 metrics.py
python
from prometheus_client import Counter, Histogram, Gauge
# 请求总数
LLM_REQUEST_TOTAL = Counter(
"llm_request_total",
"Total LLM requests",
["endpoint", "status"]
)
# 请求延迟
LLM_REQUEST_LATENCY = Histogram(
"llm_request_latency_seconds",
"LLM request latency seconds",
["endpoint"],
buckets=[0.1, 0.3, 0.5, 1, 2, 3, 5, 10, 20, 30]
)
# 输入 token
LLM_INPUT_TOKENS = Histogram(
"llm_input_tokens",
"Input tokens per request",
buckets=[32, 64, 128, 256, 512, 1024, 2048, 4096]
)
# 输出 token
LLM_OUTPUT_TOKENS = Histogram(
"llm_output_tokens",
"Output tokens per request",
buckets=[32, 64, 128, 256, 512, 1024]
)
# 队列长度
LLM_QUEUE_SIZE = Gauge(
"llm_queue_size",
"Current LLM queue size"
)
# GPU 利用率
GPU_UTILIZATION = Gauge(
"gpu_utilization_percent",
"GPU utilization percent",
["gpu_index"]
)
# GPU 显存使用
GPU_MEMORY_USED = Gauge(
"gpu_memory_used_mb",
"GPU memory used in MB",
["gpu_index"]
)
九、FastAPI 接口埋点 app.py
这里为了可复现,先用 time.sleep() 模拟模型推理。
真实项目中,把 mock_llm() 替换成你的 vLLM 或模型调用即可。
python
import time
import random
from fastapi import FastAPI, Response
from pydantic import BaseModel, Field
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
from metrics import (
LLM_REQUEST_TOTAL,
LLM_REQUEST_LATENCY,
LLM_INPUT_TOKENS,
LLM_OUTPUT_TOKENS
)
app = FastAPI(title="LLM Monitor Demo")
class ChatRequest(BaseModel):
prompt: str = Field(..., min_length=1, max_length=4000)
max_tokens: int = Field(default=128, ge=1, le=512)
def estimate_tokens(text: str) -> int:
# 简化估算:真实项目建议用 tokenizer 统计
return max(1, len(text) // 2)
def mock_llm(prompt: str, max_tokens: int):
input_tokens = estimate_tokens(prompt)
output_tokens = random.randint(32, max_tokens)
# 模拟推理耗时:输入越长、输出越多,耗时越高
cost = 0.2 + input_tokens * 0.001 + output_tokens * 0.01
time.sleep(min(cost, 5))
return {
"answer": "这是模拟的大模型回答",
"input_tokens": input_tokens,
"output_tokens": output_tokens
}
@app.post("/chat")
def chat(req: ChatRequest):
endpoint = "/chat"
start = time.time()
try:
result = mock_llm(req.prompt, req.max_tokens)
cost = time.time() - start
LLM_REQUEST_TOTAL.labels(endpoint=endpoint, status="success").inc()
LLM_REQUEST_LATENCY.labels(endpoint=endpoint).observe(cost)
LLM_INPUT_TOKENS.observe(result["input_tokens"])
LLM_OUTPUT_TOKENS.observe(result["output_tokens"])
return {
"answer": result["answer"],
"input_tokens": result["input_tokens"],
"output_tokens": result["output_tokens"],
"cost_seconds": round(cost, 3)
}
except Exception as e:
LLM_REQUEST_TOTAL.labels(endpoint=endpoint, status="error").inc()
raise e
@app.get("/metrics")
def metrics():
return Response(
generate_latest(),
media_type=CONTENT_TYPE_LATEST
)
启动服务:
bash
uvicorn app:app --host 0.0.0.0 --port 8000
访问指标:
bash
curl http://127.0.0.1:8000/metrics
可以看到类似指标:
text
llm_request_total{endpoint="/chat",status="success"} 10
llm_request_latency_seconds_bucket{endpoint="/chat",le="1.0"} 8
llm_input_tokens_bucket{le="512.0"} 10
十、GPU 指标采集 gpu_metrics.py
如果机器有 NVIDIA GPU,可以通过 nvidia-smi 获取基础指标。
python
import subprocess
from metrics import GPU_UTILIZATION, GPU_MEMORY_USED
def collect_gpu_metrics():
cmd = [
"nvidia-smi",
"--query-gpu=index,utilization.gpu,memory.used",
"--format=csv,noheader,nounits"
]
try:
output = subprocess.check_output(cmd).decode("utf-8").strip()
except Exception:
return
for line in output.splitlines():
parts = [x.strip() for x in line.split(",")]
if len(parts) != 3:
continue
gpu_index, util, memory_used = parts
GPU_UTILIZATION.labels(gpu_index=gpu_index).set(float(util))
GPU_MEMORY_USED.labels(gpu_index=gpu_index).set(float(memory_used))
接入到 FastAPI 中:
python
from gpu_metrics import collect_gpu_metrics
@app.get("/metrics")
def metrics():
collect_gpu_metrics()
return Response(
generate_latest(),
media_type=CONTENT_TYPE_LATEST
)
注意:
text
生产环境更推荐使用 NVIDIA DCGM Exporter。
这里用 nvidia-smi 是为了方便复现。
十一、Prometheus 配置 prometheus.yml
yaml
global:
scrape_interval: 5s
scrape_configs:
- job_name: "llm-service"
metrics_path: "/metrics"
static_configs:
- targets: ["host.docker.internal:8000"]
如果 Prometheus 和服务在同一台 Linux 机器上,也可以写:
yaml
targets: ["127.0.0.1:8000"]
启动 Prometheus:
bash
docker run -d \
--name prometheus \
-p 9090:9090 \
-v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \
prom/prometheus
浏览器打开:
text
http://127.0.0.1:9090
查询:
promql
llm_request_total
十二、压测生成数据
安装 locust:
bash
pip install locust
编写 locustfile.py:
python
from locust import HttpUser, task, between
import random
class LLMUser(HttpUser):
wait_time = between(0.5, 2)
@task
def chat(self):
prompts = [
"解释一下Transformer",
"请用工程师视角解释大模型部署中的KV Cache",
"写一段关于人工智能系统架构的长文,要求包含性能、稳定性、监控和部署。",
]
self.client.post("/chat", json={
"prompt": random.choice(prompts),
"max_tokens": random.choice([64, 128, 256, 512])
})
启动压测:
bash
locust -f locustfile.py --host=http://127.0.0.1:8000
十三、核心 PromQL 查询
1. QPS
promql
sum(rate(llm_request_total[1m]))
2. 错误率
promql
sum(rate(llm_request_total{status="error"}[1m]))
/
sum(rate(llm_request_total[1m]))
3. P95 延迟
promql
histogram_quantile(
0.95,
sum(rate(llm_request_latency_seconds_bucket[5m])) by (le)
)
4. P99 延迟
promql
histogram_quantile(
0.99,
sum(rate(llm_request_latency_seconds_bucket[5m])) by (le)
)
5. 输入 token 分布
promql
histogram_quantile(
0.95,
sum(rate(llm_input_tokens_bucket[5m])) by (le)
)
6. GPU 利用率
promql
gpu_utilization_percent
十四、Grafana 面板设计
一个真正有用的大模型监控面板,至少要包含以下区域:
text
1. 请求总览
- QPS
- 成功率
- 错误率
2. 延迟分布
- Avg
- P95
- P99
3. Token 分布
- Input tokens P95
- Output tokens P95
4. 队列状态
- Queue size
- Waiting time
5. GPU 状态
- GPU Util
- GPU Memory
我建议把下面两个指标放在同一行:
text
P99 延迟 + Queue Size
因为如果 P99 上升,同时 Queue Size 上升,基本可以判断:
text
系统开始排队。
如果 P99 上升,但 Queue Size 不上升,可能是:
text
模型推理本身变慢,或者某类长 token 请求变多。
十五、告警规则 alert_rules.yml
yaml
groups:
- name: llm-alerts
rules:
- alert: LLMHighErrorRate
expr: |
sum(rate(llm_request_total{status="error"}[1m]))
/
sum(rate(llm_request_total[1m])) > 0.05
for: 2m
labels:
severity: warning
annotations:
summary: "LLM error rate is too high"
- alert: LLMHighP99Latency
expr: |
histogram_quantile(
0.99,
sum(rate(llm_request_latency_seconds_bucket[5m])) by (le)
) > 10
for: 3m
labels:
severity: warning
annotations:
summary: "LLM P99 latency is too high"
- alert: LLMGPUHighMemory
expr: gpu_memory_used_mb > 22000
for: 2m
labels:
severity: warning
annotations:
summary: "GPU memory usage is high"
Prometheus 加载规则:
yaml
rule_files:
- "alert_rules.yml"
十六、验证结果:监控如何帮助定位问题?
压测时我刻意构造三类请求:
text
短 prompt + 少输出
短 prompt + 多输出
长 prompt + 多输出
从 Grafana 可以观察到:
text
1. QPS 没有明显变化
2. P99 延迟突然升高
3. Input tokens P95 同步升高
4. GPU 显存缓慢上升
这说明问题不是流量变大,而是:
text
请求结构变化导致推理成本上升。
如果只看接口日志,你看到的是:
text
请求成功
请求成功
请求成功
但通过指标可以明确定位:
text
长文本请求比例变高 → KV Cache 压力变大 → P99 上升
这就是监控系统的价值。
十七、踩坑记录
坑 1:只看平均延迟
平均延迟最容易骗人。
例如:
text
90% 请求耗时 1s
10% 请求耗时 20s
平均值看起来可能还能接受,但 P99 已经非常差。
所以大模型服务必须看:
text
P95 / P99
坑 2:只监控 GPU,不监控队列
GPU 利用率低,不代表系统没问题。
如果 Worker 挂了,队列不断堆积,GPU 可能很空,但用户请求已经无法处理。
所以必须同时看:
text
Queue Size + GPU Util + P99
坑 3:指标命名混乱
一开始我把指标写成:
text
request_count
latency
token
后来接入多个服务后完全乱掉。
建议统一前缀:
text
llm_request_total
llm_request_latency_seconds
llm_input_tokens
llm_output_tokens
坑 4:高频调用 nvidia-smi
nvidia-smi 本身有开销,不建议高频执行。
测试环境可以 5 秒采集一次。
生产环境建议:
text
NVIDIA DCGM Exporter
坑 5:没有给指标加 label
例如同一个系统里有多个模型:
text
qwen
llama
deepseek
最好给指标加:
text
model_name
route
status
否则后面无法区分到底是哪一个模型慢。
十八、适合收藏的监控 Checklist
text
接口层:
[ ] QPS
[ ] 成功率
[ ] 错误率
[ ] P95 延迟
[ ] P99 延迟
请求层:
[ ] input tokens
[ ] output tokens
[ ] max_tokens
[ ] prompt length
队列层:
[ ] queue size
[ ] waiting time
[ ] task success
[ ] task failed
模型层:
[ ] model latency
[ ] tokens/s
[ ] timeout count
GPU层:
[ ] GPU utilization
[ ] GPU memory used
[ ] GPU memory ratio
[ ] GPU temperature
十九、经验总结
这次问题给我的最大经验是:
text
大模型系统不是写完接口就结束,而是必须具备可观测能力。
普通 Web 系统慢了,可能查日志、查 SQL 就能定位。
但大模型服务慢了,问题可能在:
text
token 分布
队列等待
KV Cache
GPU 显存
推理调度
如果没有指标,你只能猜。
而线上系统最怕的就是:
text
靠猜排查问题。
二十、优化建议
后续可以继续增强:
text
1. 使用 DCGM Exporter 采集 GPU 指标
2. 接入 Alertmanager 做自动告警
3. 增加 OpenTelemetry Trace
4. 对不同模型增加 model_name 标签
5. 将队列等待时间单独统计
6. 按 token 区间统计不同请求成本
7. 监控 tokens/s 作为模型吞吐指标
一句话总结:
text
没有监控的大模型系统,就像没有仪表盘的飞机。
能飞,但你不知道什么时候会出事。