外观
可观测性(observability)落地,是把「服务到底怎么样」变成随时可回答的工程能力:指标(metrics)、日志(logs)、追踪(traces)三支柱齐备,且告警精准不吵。 为什么值得动手?因为没有观测的推理服务就像不带仪表盘开车——模型漂移、延迟劣化、显存泄漏这些故障不会崩溃报警,只会让用户先你一步感受到「变慢了、变蠢了」。这篇把从零到生产级观测的每一步配好代码与配置:指标埋点、抓取、GPU 采集、Grafana 面板、SLO 燃烧率告警、结构化日志与链路追踪,最后给一份上线后 30 天观察清单。理论背景与 SLO 定义见 监控与可观测性。
落地目标一句话
指标不缺、日志可查、告警不吵——黄金四指标(RPS、延迟、错误率、饱和度)齐备,告警只在「需要人处理」时响。
一、步骤一:指标埋点(Prometheus 客户端)
用 prometheus-client 为推理服务埋三类黄金指标:请求计数、延迟直方图、错误计数。这是 从零部署一个模型 里 metrics 模块的完整版。
python
# app/metrics.py —— 指标定义集中在一个文件,全项目引用
from prometheus_client import Counter, Histogram, Gauge
# 请求计数:labels 区分模型与版本,方便按版本对比
REQUESTS = Counter("predict_requests_total", "推理请求总数",
["model", "version"])
ERRORS = Counter("predict_errors_total", "推理失败数",
["model", "version", "error_type"])
# 延迟直方图:bucket 按业务延迟量级设计,覆盖 P99 目标
LATENCY = Histogram("predict_latency_seconds", "推理延迟",
["model", "version"],
buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5))
# 排队长度:饱和度信号,Gauge 每 5 秒采样一次即可
QUEUE = Gauge("predict_queue_depth", "当前排队请求数")labels 设计的三条规范(label 是高危设计点,加错全是坑):
- cardinality 必须低:label 取值组合数 × 时间序列 = 存储爆炸。
user_id、request_id这类高基数取值禁止进 label,只能进日志; - 只放「查询时要用」的维度:
model、version、error_type、region是合理的;临时调试字段不放进生产指标; - 标签集合全局一致:所有指标用同一套
model/version标签,告警和面板才能跨指标 join。
埋点位置在服务入口与出口(见 从零部署一个模型 的 main.py),用中间件/装饰器统一处理,别在业务代码里到处 inc():
python
# app/middleware.py —— 统一埋点
import time
from starlette.middleware.base import BaseHTTPMiddleware
from app.metrics import REQUESTS, ERRORS, LATENCY
class MetricsMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
t0 = time.perf_counter()
try:
resp = await call_next(request)
except Exception:
ERRORS.labels(model="mobilenetv2", version="v1",
error_type="exception").inc()
raise
LATENCY.labels(model="mobilenetv2", version="v1").observe(
time.perf_counter() - t0)
REQUESTS.labels(model="mobilenetv2", version="v1").inc()
return resppython
# app/main.py —— 暴露 /metrics 给 Prometheus 抓取
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
from fastapi import Response
@app.get("/metrics")
def metrics():
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)二、步骤二:服务发现与抓取
2.1 简单场景:static_configs
yaml
# prometheus.yml
global:
scrape_interval: 15s # 抓取间隔;延迟敏感的服务可调 10s
evaluation_interval: 30s # 告警规则评估间隔
scrape_configs:
- job_name: inference-api
static_configs:
- targets: ["api:8000"] # compose/Docker 网络内用服务名
labels: { env: prod }
- job_name: dcgm # GPU 指标,见步骤三
static_configs:
- targets: ["dcgm-exporter:9400"]2.2 K8s 场景:ServiceMonitor
K8s 里服务实例是动态的,用 ServiceMonitor 声明式发现(配合 Prometheus Operator / kube-prometheus-stack)。服务打上 prometheus.io/scrape: "true" 注解后自动被抓取:
yaml
# ServiceMonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: inference-api
spec:
selector:
matchLabels:
app: inference-api # 匹配 Service 的 selector
endpoints:
- port: http # Service 里声明的端口名
path: /metrics
interval: 15s
namespaceSelector:
any: true抓取失败是最隐蔽的监控事故
Prometheus 抓不到目标时通常不报错、只丢数据——面板慢慢变空,告警静默失效。上线后第一周每天看一次 up 指标(up{job="inference-api"} == 1),这是「监控本身活着」的唯一证据。
三、步骤三:GPU 指标采集(dcgm-exporter)
推理服务在 GPU 上跑时,GPU 利用率与显存水位是容量与故障判断的核心信号(配合 压测与容量规划 的「GPU 打满 vs CPU 打满」分析)。标准做法是 NVIDIA DCGM exporter:
bash
# 启动 DCGM exporter,暴露 :9400/metrics(GPU 利用率、显存、温度、功率)
docker run -d --gpus all --rm --cap-add SYS_ADMIN \
--name dcgm-exporter \
nvcr.io/nvidia/dcgm-exporter:3.3.5-ubuntu22.04关键指标(DCGM 自带命名):
text
# GPU 利用率(%),按设备分组
DCGM_FI_DEV_GPU_UTIL
# 显存已用(bytes)
DCGM_FI_DEV_FB_USED
# GPU 温度与功率
DCGM_FI_DEV_GPU_TEMP
DCGM_FI_DEV_POWER_USAGE不用容器时,nvidia-smi 的 --query-gpu 也能输出类似数据,但没有 Prometheus 格式,要自己用 node_exporter 的 textfile collector 桥接,或用 nvidia_gpu_exporter 这类社区 exporter。显存问题的排查还关联 显存泄漏与 OOM 坑④。
四、步骤四:Grafana 面板
Grafana 连接 Prometheus 数据源后,建一个「推理服务总览」dashboard,五个面板按序排布:
| 面板 | PromQL | 看什么 |
|---|---|---|
| RPS | sum(rate(predict_requests_total[1m])) by (model) | 流量趋势,配合发布看流量变化 |
| P50/P99 延迟 | histogram_quantile(0.99, sum by (le, model) (rate(predict_latency_seconds_bucket[5m]))) | P99 是否在 SLO 内 |
| 错误率 | sum(rate(predict_errors_total[5m])) / sum(rate(predict_requests_total[5m])) | 5xx/推理异常占比 |
| GPU 利用率 | avg(DCGM_FI_DEV_GPU_UTIL) | GPU 打满/空闲 |
| 排队数 | predict_queue_depth | 饱和度,队列增长是雪崩前兆 |
histogram_quantile 的 P99 算法有统计误差,bucket 越密越准,但要平衡存储。设计规范:bucket 覆盖「P50 到 10×P99 目标」的范围,例如目标 P99=100ms,bucket 从 5ms 到 2.5s 按约 2 倍间隔递增(如上面的 buckets 数组)。
五、步骤五:告警规则(SLO 燃烧率 + 分级)
5.1 燃烧率告警(burn rate)—— 告警不吵的关键
「阈值告警」(延迟 > 500ms 就告警)在毛刺时狂响、在缓慢劣化时沉默,是告警风暴的根源。SLO 燃烧率是按「SLO 预算消耗速度」告警:只有在一段时间内错误预算消耗得够快才触发。原理见 监控与可观测性。
yaml
# prometheus-alerts.yml
groups:
- name: inference-slo
rules:
# 30 天 SLO 99.9%(允许 0.1% 错误)
# 燃烧率 14.4(5 分钟窗口):约 2 小时内烧完 2% 预算 → page
- alert: HighErrorRatePage
expr: |
sum(rate(predict_errors_total[5m]))
/ sum(rate(predict_requests_total[5m]))
> 0.1 * 14.4
labels:
severity: page
annotations:
summary: "错误率燃烧率超 14.4,2 小时烧光 2% 预算"
# 燃烧率 1.0(1 小时窗口):30 天内烧完预算 → warn,进值班队列
- alert: ErrorBudgetWarn
expr: |
sum(rate(predict_errors_total[1h]))
/ sum(rate(predict_requests_total[1h]))
> 0.1
labels:
severity: warn
annotations:
summary: "1 小时错误率已超 SLO,正在消耗错误预算"5.2 分级与路由
| 级别 | 触发 | 动作 | 目标 |
|---|---|---|---|
| page | 燃烧率 ≥ 14.4(高倍率快速烧预算) | 电话/IM 立即拉人 | 持续故障 2 小时内介入 |
| warn | 燃烧率 ≥ 1(缓慢消耗) | 进值班队列,30 分钟内响应 | 避免预算被缓慢磨光 |
| info | 各类边缘情况(GPU 温度高、队列增长) | 只记录,不打扰 | 用于复盘 |
告警不吵的标准:每人每周 page ≤ 1 次,warn 可接受但要有「确认即关」的流程。page 连响三周,说明阈值或容量规划有问题,不是运维「能扛」。
六、步骤六:日志与追踪
6.1 结构化日志
日志从「给人看的散文」改成「给机器解析的 JSON」,才能被日志平台(Loki/ELK)索引、过滤、join 指标:
python
# app/logging_conf.py —— JSON 结构化日志
import json, logging, sys
class JsonFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
"ts": self.formatTime(record),
"level": record.levelname,
"logger": record.name,
"msg": record.getMessage(),
"request_id": getattr(record, "request_id", None), # 关联追踪
"model": getattr(record, "model", None),
}, ensure_ascii=False)
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().handlers[0].setFormatter(JsonFormatter())结构化日志的隐藏价值是 request_id:日志、指标、追踪共用同一个 request_id,排查「P99 劣化的是哪类请求」时三支柱就能对起来。
6.2 OpenTelemetry 链路追踪
推理请求往往跨多跳:网关 → 服务 → 数据库/特征服务 → 推理引擎。OpenTelemetry(OTel)统一采集跨服务调用链:
python
# 最小接入:自动插桩 FastAPI + 手动记录推理段
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4318")))
FastAPIInstrumentor.instrument_app(app)
# 推理函数内部加自定义 span,量化「引擎内耗时」
tracer = trace.get_tracer("inference")
def predict_with_trace(images):
with tracer.start_as_current_span("model_inference") as span:
result = engine.run(images)
span.set_attribute("engine", "onnxruntime")
span.set_attribute("batch_size", images.shape[0])
return resultOTel 与 Jaeger/Grafana Tempo 对接后,能可视化「P99 劣化的请求慢在哪一跳」。完整的可观测性架构(Collector、Loki、Tempo)是另一套工程,量力而行——小团队先做到指标 + 日志,追踪在跨服务调用出现后再补。
七、上线后 30 天观察清单
指标建好只是开始,上线后第一个月是「校准期」:
| 时间 | 观察内容 | 动作 |
|---|---|---|
| 第 1 周 | up 指标稳定为 1;面板无空数据 | 修抓取问题;和压测数字对齐 |
| 第 2 周 | 基线 vs 压测:线上 P99 是否≈压测 P99 | 差距 > 2 倍 → 查环境差异 |
| 第 2 周 | 告警实际触发频率 | 太吵 → 调燃烧率/阈值;太静 → 故意注入故障验证 |
| 第 4 周 | 错误预算消耗速度 | 月底预算 > 80% → 说明 99.9% SLO 选得合理 |
| 第 4 周 | 容量水位(GPU/排队数趋势) | 接近余量 → 触发扩容预案 |
故意注入一次故障(game day)
上线后 30 天内主动做一次「演练」:停掉一个 worker、注入 5% 错误,看告警是否准时、值班是否知道怎么办。没验证过的告警不是告警,是心理安慰。
八、常见坑
- 指标口径不一致:RPS 有的按秒有的按分钟,延迟有的含排队有的不含,面板数字互相矛盾。解法:每个指标写注释定义口径,面板标题带单位。
- 直方图 bucket 设计失误:bucket 只到 200ms,P99 目标 100ms 却测不出 2s 的劣化——bucket 上限要覆盖 10 倍于 P99 目标。
- 告警风暴:阈值告警在毛刺时狂响。解法:改燃烧率告警 + 加静默规则(维护窗口)。
- 监控没监控自己:Prometheus 挂了没人知道。解法:对
up、Prometheus 自身资源加告警。 - 日志是散文:无法检索和统计。解法:JSON 结构化 + request_id。
检查清单
- [ ] 黄金四指标埋点完成,labels 低基数且全局一致;
- [ ]
/metrics可被抓取,up == 1有告警; - [ ] GPU 指标(利用率/显存)可见;
- [ ] Grafana 五面板就绪,P99 用直方图计算而非 gauge;
- [ ] SLO 燃烧率告警分级(page/warn),无阈值毛刺告警;
- [ ] 日志 JSON 化含 request_id;跨服务场景 OTel 接入;
- [ ] 30 天观察清单排期,含一次故障演练。
延伸阅读
- 监控与可观测性 —— 黄金指标、SLO、燃烧率的理论基础
- 压测与容量规划 —— 压测时服务端指标怎么采、容量怎么定
- 发布策略:灰度与回滚 —— 灰度期间的监控项与自动回滚条件
- 从零部署一个模型 —— 最小部署闭环里的监控环节
- 常见陷阱与反模式 —— 「无监控裸奔」等监控缺失事故
- 性能指标与调优 —— 延迟/吞吐指标定义与调优
参考资料
- prometheus/client_python:https://github.com/prometheus/client_python
- Prometheus 查询语言文档:https://prometheus.io/docs/prometheus/latest/querying/basics/
- Prometheus Alertmanager:https://prometheus.io/docs/alerting/latest/alertmanager/
- NVIDIA DCGM exporter:https://github.com/NVIDIA/dcgm-exporter
- Grafana 官方文档:https://grafana.com/docs/grafana/latest/dashboards/
- OpenTelemetry Python 文档:https://opentelemetry.io/docs/languages/python/