2. Prometheus基础

在容器化推理服务的可观测性体系中,Prometheus 是事实上的标准监控方案。要设计一套高质量的指标采集系统,首先必须深刻理解 Prometheus 的核心设计哲学:指标(Metric)是带标签(Label)的时间序列数据。本章将围绕数据模型、四种核心指标类型、Pull 与 Push 两种采集模式的适用边界展开,帮助你建立从“理解概念”到“设计采集方案”的完整认知链路。

Prometheus 核心概念关系图 数据模型 指标名 + 标签集 = 时间序列 样本值 float64 + 毫秒时间戳 标签: __name__, instance, job 维度组合决定序列数量 示例: http_requests_total{code="200"} 四种指标类型 Counter: 只增不减(请求数) Gauge: 可增可减(当前温度) Histogram: 分位数+累计计数 Summary: 客户端计算分位数 选择依据: 数据性质与查询需求 采集模式 Pull: Prometheus主动拉取 Pushgateway: 短期任务推送 服务发现: k8s_sd, file_sd Exporter: 指标暴露端点 选择依据: 任务生命周期与网络模型 PromQL 查询 rate(counter[5m]) | gauge | histogram_quantile(0.95, ...) | 标签匹配与聚合 容器化推理服务典型应用 • 推理请求QPS与延迟分布(Histogram) • GPU利用率与显存占用(Gauge) • 模型版本切换计数(Counter) • 批处理任务推送指标(Pushgateway)

2.1 数据模型:时间序列与标签体系

Prometheus 将所有监控数据统一建模为带标签的时间序列。一条时间序列由指标名称(Metric Name)和一组键值对标签(Label Set)唯一标识。指标名称用于描述被监控对象的一般特征,例如 http_requests_total 表示 HTTP 请求总数;标签则用于区分同一指标下的不同维度,例如 method="POST"、status="500"。每个时间序列由一系列样本点组成,每个样本点包含一个 float64 数值和一个毫秒精度的时间戳。

核心原理:标签是 Prometheus 数据模型的灵魂。同一指标名配合不同标签组合,会形成完全独立的时间序列。例如 inference_latency_seconds{model="resnet50", version="v2"} 与 inference_latency_seconds{model="resnet50", version="v1"} 是两个不同的序列。在设计指标时,标签的基数(Cardinality)必须严格控制——高基数标签(如用户 ID、请求 ID)会导致序列数量爆炸,压垮存储与查询。

判断方法:在设计新指标时,先问三个问题:这个指标是计数型还是测量型?需要哪些标签来区分不同维度?标签的取值集合是否有限且可控?例如,推理服务的模型版本标签取值有限(v1、v2、v3),适合作为标签;而客户端 IP 取值无限,不适合作为标签。

数据模型要素说明示例
指标名称描述被监控对象,需符合 [a-zA-Z_:][a-zA-Z0-9_:]*inference_requests_total
标签键值对,用于维度区分model="yolov5", device="cuda:0"
样本时间戳 + 数值1680000000000 → 1024
序列指标名 + 标签集唯一确定inference_requests_total{model="yolov5"}

案例拆解:假设你为推理服务定义了一个指标 inference_batch_size,标签为 model 和 instance。如果模型数量为 5,实例数量为 10,则最多产生 50 条时间序列。但如果误将 request_id 作为标签,每处理一个请求就产生一条新序列,几分钟内就会产生数百万条序列,导致 Prometheus 内存溢出。因此,标签设计必须遵循“低基数”原则。

常见误区:将指标名称当作“变量名”使用,例如为每个模型单独定义 model_a_requests、model_b_requests。正确做法是使用同一个指标名 inference_requests_total,通过标签 model="a" 和 model="b" 区分。这样便于使用 PromQL 进行跨模型聚合与对比。

2.2 四种指标类型:Counter、Gauge、Histogram、Summary

Prometheus 客户端库提供四种核心指标类型,每种类型对应不同的数据语义和查询模式。选择正确的类型是采集方案设计的关键一步。

2.2.1 Counter(计数器)

核心原理:Counter 是单调递增的计数器,只增不减(除非系统重启)。典型场景包括:请求总数、错误总数、任务完成数。Counter 的绝对值通常没有直接意义,需要配合 rate() 或 increase() 函数计算速率或增量。

# 定义 Counter 指标
inference_requests_total = Counter('inference_requests_total', 'Total inference requests')
# 使用
inference_requests_total.labels(model='resnet50').inc()

判断方法:如果指标的值“只会增加,不会减少”,且你关心的是“在一段时间内发生了多少次”,则选择 Counter。例如,累计推理请求数、累计超时次数。

2.2.2 Gauge(仪表盘)

核心原理:Gauge 表示可增可减的瞬时值,反映当前状态。典型场景包括:当前 GPU 利用率、当前队列长度、当前内存使用量。Gauge 可以直接查询当前值,也可以使用 delta() 计算一段时间内的变化量。

gpu_utilization = Gauge('gpu_utilization_percent', 'GPU utilization', ['device'])
gpu_utilization.labels(device='cuda:0').set(87.5)

判断方法:如果指标的值“上下波动”,且你关心的是“当前是多少”,则选择 Gauge。例如,当前推理服务的工作线程数、当前温度。

2.2.3 Histogram(直方图)

核心原理:Histogram 用于观测值的分布统计,例如请求延迟、响应大小。它在客户端计算累计计数(_bucket)、总和(_sum)和总次数(_count),服务端通过 histogram_quantile() 函数计算分位数。Histogram 需要预先定义桶(Bucket)边界。

inference_latency = Histogram('inference_latency_seconds', 'Inference latency', 
                              buckets=[0.01, 0.05, 0.1, 0.5, 1.0, 2.5, 5.0])
inference_latency.observe(0.23)

判断方法:如果你需要计算“P95 延迟”或“延迟分布”,且无法在客户端预知分位数,则选择 Histogram。桶边界的设计需要根据业务经验调整,桶太粗则分位数不准确,桶太细则增加开销。

2.2.4 Summary(摘要)

核心原理:Summary 在客户端直接计算分位数,并暴露为 _quantile 系列指标。它不依赖预定义桶,但无法在服务端进行聚合计算。Summary 适用于“已知需要哪些分位数”且“单实例部署”的场景。

inference_latency_summary = Summary('inference_latency_summary_seconds', 'Inference latency summary',
                                    quantiles=[0.5, 0.9, 0.99])
inference_latency_summary.observe(0.23)

判断方法:如果客户端明确知道需要哪些分位数(如 0.5、0.9、0.99),且不需要跨实例聚合分位数,则选择 Summary。但要注意,Summary 无法通过 PromQL 进行分位数聚合,多实例部署时只能使用平均值。

类型语义典型场景查询方式聚合能力
Counter只增不减请求总数、错误数rate(), increase()支持 sum()
Gauge可增可减GPU 利用率、队列长度直接查询支持 avg(), max()
Histogram分布统计延迟、响应大小histogram_quantile()支持 sum() 后重新计算分位数
Summary客户端分位数单实例已知分位数直接查询分位数不支持分位数聚合

案例拆解:某推理平台需要监控模型推理延迟。团队最初使用 Summary 并配置了 0.5、0.9、0.99 分位数。后来服务扩展到 10 个实例,发现无法通过 PromQL 计算所有实例的 P99 延迟。切换到 Histogram 后,使用 histogram_quantile(0.99, sum(rate(inference_latency_seconds_bucket[5m])) by (le)) 成功计算出全局 P99。这个案例说明:在多实例场景下,Histogram 是更优选择。

经验提醒:对于推理服务,建议将延迟指标设计为 Histogram,桶边界覆盖 10ms 到 10s 范围;将 GPU 利用率设计为 Gauge;将累计请求数设计为 Counter。这样既满足监控需求,又保持查询灵活性。

2.3 Pull 模型与 Pushgateway 的适用场景

Prometheus 默认采用 Pull 模型:Prometheus 服务器主动从目标端点(如 /metrics)拉取指标。这种模式要求目标服务必须可被 Prometheus 网络访问,且持续运行。对于容器化推理服务,Pull 模型是首选方案。

核心原理:Pull 模型通过服务发现机制(如 Kubernetes 的 kubernetes_sd_configs)动态获取目标列表,周期性拉取指标。这种模式天然支持健康检查——如果目标无法访问,Prometheus 会标记该实例为 DOWN,从而反映服务可用性。

# prometheus.yml 配置示例
scrape_configs:
  - job_name: 'inference-service'
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_label_app]
        regex: 'inference'
        action: keep
    metrics_path: '/metrics'
    scrape_interval: 15s

判断方法:当目标服务是长期运行的进程(如推理服务、Web 服务),且网络拓扑允许 Prometheus 主动访问时,应使用 Pull 模型。Pull 模型的优势在于:集中控制采集频率、目标状态可观测、天然支持水平扩展。

Pushgateway 的适用场景:Pushgateway 是 Prometheus 生态中的一个辅助组件,允许目标服务主动推送指标到 Pushgateway,再由 Prometheus 从 Pushgateway 拉取。它主要解决两类问题:

  1. 短期任务(Short-lived Jobs):例如批处理任务、定时训练任务,任务运行时间短,Prometheus 来不及拉取指标。
  2. 网络隔离环境:目标服务位于防火墙后,Prometheus 无法直接访问,但目标服务可以主动外连。
# 推送指标到 Pushgateway
echo "inference_job_duration_seconds 42.5" | curl --data-binary @- http://pushgateway:9091/metrics/job/inference_batch

案例拆解:某团队使用 Kubernetes CronJob 运行每日模型评估任务。任务运行约 10 分钟,结束后 Pod 被销毁。由于 Prometheus 的采集周期为 15 秒,任务可能在两次采集之间结束,导致指标丢失。解决方案是:任务在结束时将结果推送到 Pushgateway,Prometheus 从 Pushgateway 拉取指标。这样即使任务已结束,指标仍然保留。

风险说明:Pushgateway 不适合长期运行的推理服务。原因包括:Pushgateway 成为单点故障;指标无法关联实例生命周期;Pushgateway 中的指标不会自动过期,需要手动清理。对于推理服务,应优先使用 Pull 模型,仅在批处理或网络隔离场景下使用 Pushgateway。

总结复盘:Pull 模型与 Pushgateway 的选择本质上是“谁主动、谁被动”的问题。Pull 模型将控制权集中在 Prometheus,适合稳态服务;Pushgateway 将控制权交给目标服务,适合短生命周期任务。在设计采集方案时,应遵循“默认 Pull,例外 Push”的原则。

实战建议:在 Kubernetes 环境中,为推理服务配置 PodMonitor 或 ServiceMonitor 来声明 Pull 采集规则。对于批处理任务,使用 Pushgateway 并设置合理的 pushgateway_metrics_ttl 或定期清理机制,避免指标堆积。