Skip to content

可观测性落地:Prometheus + Grafana + 告警的完整配置

本页速览 把推理服务接入 Prometheus + Grafana 的完整落地:自定义指标(请求数/延迟直方图/错误率)、GPU 指标采集、SLO 燃烧率告警规则与日志追踪,附上线后 30 天观察清单。

可观测性(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 是高危设计点,加错全是坑):

  1. cardinality 必须低:label 取值组合数 × 时间序列 = 存储爆炸。user_idrequest_id 这类高基数取值禁止进 label,只能进日志;
  2. 只放「查询时要用」的维度modelversionerror_typeregion 是合理的;临时调试字段不放进生产指标;
  3. 标签集合全局一致:所有指标用同一套 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 resp
python
# 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看什么
RPSsum(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 result

OTel 与 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% 错误,看告警是否准时、值班是否知道怎么办。没验证过的告警不是告警,是心理安慰。

八、常见坑

  1. 指标口径不一致:RPS 有的按秒有的按分钟,延迟有的含排队有的不含,面板数字互相矛盾。解法:每个指标写注释定义口径,面板标题带单位。
  2. 直方图 bucket 设计失误:bucket 只到 200ms,P99 目标 100ms 却测不出 2s 的劣化——bucket 上限要覆盖 10 倍于 P99 目标。
  3. 告警风暴:阈值告警在毛刺时狂响。解法:改燃烧率告警 + 加静默规则(维护窗口)。
  4. 监控没监控自己:Prometheus 挂了没人知道。解法:对 up、Prometheus 自身资源加告警。
  5. 日志是散文:无法检索和统计。解法:JSON 结构化 + request_id。

检查清单

  • [ ] 黄金四指标埋点完成,labels 低基数且全局一致;
  • [ ] /metrics 可被抓取,up == 1 有告警;
  • [ ] GPU 指标(利用率/显存)可见;
  • [ ] Grafana 五面板就绪,P99 用直方图计算而非 gauge;
  • [ ] SLO 燃烧率告警分级(page/warn),无阈值毛刺告警;
  • [ ] 日志 JSON 化含 request_id;跨服务场景 OTel 接入;
  • [ ] 30 天观察清单排期,含一次故障演练。

延伸阅读

参考资料