编程 Prometheus 3.x 深度实战:从 Pull 模型、TSDB 存储引擎到 PromQL 与生产级可观测性完全指南(2026)

2026-07-09 02:50:59 +0800 CST views 22

Prometheus 3.x 深度实战:从 Pull 模型、TSDB 存储引擎到 PromQL 与生产级可观测性完全指南(2026)

可观测性(Observability)不是"加几个监控图表"那么简单。它是一套让你在凌晨三点被电话叫醒时,能在 5 分钟内定位到"哪个服务的哪个接口、因为什么、影响了多少用户"的工程能力。而在云原生时代,Prometheus 几乎是这套能力的默认底座——它是 CNCF 第二个毕业项目(仅次于 Kubernetes),也是 Kubernetes 官方推荐的监控方案。

但绝大多数团队对 Prometheus 的使用,停留在"装个 kube-prometheus-stack,看几个 Grafana 面板"的层面。一旦遇到 cardinality 爆炸、查询变慢、长期存储成本失控,就开始抓瞎。本文不堆砌概念,而是从设计哲学 → 数据模型 → 存储引擎 → 代码实战 → 性能优化五层,把 Prometheus 真正讲透,让你不仅能用,还能在它"生病"时给它"看病"。


一、背景介绍:监控的两次范式转移

要理解 Prometheus 为什么是这样设计的,得先看它解决了什么问题。

1.1 从"黑盒告警"到"白盒观测"

早期的监控(Nagios 为代表)是黑盒 + 阈值告警模型:你配一堆 check 脚本(ping、端口、磁盘用量),超过阈值就发邮件。这套模型有两个致命缺陷:

  1. 事后才知道:等磁盘 90% 告警时,往往已经晚了;你看到的是"症状",看不到"病因"。
  2. 静态、脆弱:服务一扩容,监控配置就得改;微服务架构下成百上千个实例,靠手写 check 根本不现实。

云原生带来的变化是:基础设施变成动态、短暂、自动扩缩的。一个 Pod 可能只活 30 秒。这时候你需要的是白盒监控——直接暴露应用内部的运行状态(请求数、延迟分布、队列长度、goroutine 数……),让系统自己"报告"自己。

这正是 Prometheus 的设计起点:让每个被监控目标自己暴露指标,Prometheus 主动去拉取(Pull)

1.2 为什么是 Pull,不是 Push?

这是 Prometheus 最受争议也最被误解的设计。很多人直觉认为"Push 更实时、更省心",但 Prometheus 选择了 Pull,背后是几个工程权衡:

  • 可调试性:Pull 模型下,你直接用 curl http://target:8080/metrics 就能看到目标暴露了什么。如果是 Push,出问题时你很难知道某个指标"为什么没来"——它可能在客户端就丢了,你毫无头绪。
  • 目标健康由中心判断:Proxy 挂了、网络分区了,Prometheus 通过"这次 scrape 失败"自然知道目标不健康(up == 0),不需要目标额外上报心跳。
  • 松耦合:目标不需要知道 Prometheus 在哪、有几个、token 是什么。加一个新 Prometheus 实例只是多配一个 scrape 源。

当然,Pull 也有代价:短生命周期任务(批处理、CI job)在 Prometheus 来 scrape 之前就退出了,拉不到。Prometheus 的解法是 Pushgateway——这类任务把指标 push 到一个中转网关,Prometheus 再去 scrape 网关。这是"Pull 为主、Push 为辅"的混合模型,后面会讲。

1.3 Prometheus 的边界:它擅长什么,不擅长什么

一个资深工程师的标志,是清楚工具的边界。Prometheus 明确不是

  • 不是日志系统(那是 Loki / Elasticsearch 的事);
  • 不是分布式追踪系统(那是 Tempo / Jaeger 的事);
  • 不是事件溯源 / 审计系统(它的数据模型是时序样本,不是不可变事件流);
  • 不是强一致存储(scrape 本身就有间隔,数据天然是"近似"的)。

它擅长的是:数值型时序指标的采集、存储与高效查询,尤其是"最近一段时间内的聚合分析"("过去 5 分钟 QPS 多少""P99 延迟趋势如何")。这个边界划清楚了,你就不会拿它做错误的事。


二、核心概念:多维数据模型是 Prometheus 的灵魂

很多"监控工具"的指标是一维的:cpu_usage = 73。Prometheus 的革命性在于多维标签(label)

2.1 时间序列的恒等式

在 Prometheus 里,一条**时间序列(time series)**由两部分唯一确定:

<metric_name>{<label1>=<v1>, <label2>=<v2>, ...}

例如:

http_requests_total{method="GET", code="200", job="api", instance="10.0.1.7:8080"}

关键是:标签值的每一种组合,都是一条独立的时间序列

  • http_requests_total{method="GET", code="200"} 是一条序列;
  • http_requests_total{method="GET", code="500"} 是另一条序列;
  • 它们共享 metric name,但在存储和查询上是完全独立的流。

再加上时间戳和值,一个**样本(sample)**就是 (timestamp, value)。所以"一条时间序列"本质是一个按时间排序的样本列表。

这套模型妙在哪?它把"维度切片"的能力交给了查询语言(PromQL)。你采集时只管打标签,分析时再决定怎么切:

# 总请求率
sum(rate(http_requests_total[5m]))

# 只看 5xx 错误率
sum(rate(http_requests_total{code=~"5.."}[5m]))

# 按 method 分组看 QPS
sum by (method) (rate(http_requests_total[5m]))

同一个指标,无限种切法。 这正是多维模型比传统一维监控强的地方。

2.2 四种指标类型:用错类型,数据就废了

Prometheus 客户端库定义了四种核心指标类型,它们的语义差异决定了你能做什么运算:

Counter(计数器):只增不减

Counter 是单调递增的累计值(进程重启会被重置为 0,但 Prometheus 客户端默认会处理这种重置)。你永远不应该直接看 Counter 的绝对值,而要看它的变化率

# 每秒请求数(自动处理重启重置)
rate(http_requests_total[5m])

# 增长总量
increase(http_requests_total[1h])

典型用途:请求总数、错误总数、已处理字节数。永远不要用 Counter 表示"当前温度""当前在线人数"这类会上下浮动的量——那是 Gauge 的活。

Gauge(仪表盘):可增可减

Gauge 表示某一时刻的瞬时值,可以任意上下波动:

temperature_celsius{room="server"} 23.5
go_goroutines 184
http_in_flight_requests 12

用途:温度、内存占用、在途请求数、队列长度、连接数。max/min/avg 对 Gauge 才有意义,rate 对 Gauge 通常没意义(除非你想看"变化速度")。

Histogram(直方图):看分布,不只是平均值

这是性能分析的杀手锏。Histogram 把观测值分到一系列**桶(bucket)**里,并暴露:

  • _bucket{le="0.1"}:观测值 ≤ 0.1 的次数;
  • _bucket{le="+Inf"}:总次数;
  • _sum:观测值总和;
  • _count:观测值总个数。
# P99 延迟(用 histogram_quantile)
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))

# 平均延迟
rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m])

只看平均延迟是危险的:平均 50ms 可能掩盖了"1% 的请求花了 3 秒"。Histogram + histogram_quantile 让你看到长尾。这是 SRE 四大黄金信号(延迟、流量、错误、饱和度)里"延迟"的正确打开方式。

Summary(摘要):客户端分位数

Summary 和 Histogram 类似,但分位数(φ-quantile)是在客户端算好的(φ=0.99 等)。优点是查询便宜,缺点是:

  • 不能跨实例聚合 Summary 的分位数(avg(phi) 在数学上是错的);
  • 桶的边界在采集端固定,事后改不了。

经验法则:需要跨多个实例/服务聚合分位数 → 用 Histogram;只看单个实例、且不想在查询端算 → 用 Summary。绝大多数生产场景我推荐 Histogram,因为它更灵活(Prometheus 3.x 的 Native Histogram 更把这个优势放大了,见后文)。

2.3 暴露格式(Exposition Format)

Prometheus 拉取目标时,目标在 /metrics 端点返回纯文本:

# HELP http_requests_total Total number of HTTP requests.
# TYPE http_requests_total counter
http_requests_total{method="GET",code="200"} 1027
http_requests_total{method="GET",code="500"} 3
# HELP http_request_duration_seconds Histogram of request durations.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{method="GET",le="0.005"} 210
http_request_duration_seconds_bucket{method="GET",le="0.01"} 480
http_request_duration_seconds_bucket{method="GET",le="0.025"} 901
http_request_duration_seconds_bucket{method="GET",le="0.05"} 990
http_request_duration_seconds_bucket{method="GET",le="0.1"} 1001
http_request_duration_seconds_bucket{method="GET",le="+Inf"} 1001
http_request_duration_seconds_sum{method="GET"} 23.4
http_request_duration_seconds_count{method="GET"} 1001

注意 # TYPE 行——它告诉 Prometheus 这个 metric 是什么类型(counter/gauge/histogram/summary/untyped)。永远给你的指标声明 TYPE,否则 Prometheus 当成 untyped 处理,很多运算会出问题。

2.4 标签的代价:cardinality 是万恶之源

这是全文最重要的一个认知:标签的基数(cardinality)决定了 Prometheus 的生死

每条"指标名 + 标签组合"都是一条独立时间序列,在内存和磁盘里都有开销。假设:

  • http_requests_totalmethod(4 种) × code(6 种) = 24 条序列。很健康。
  • 如果你"好心"加了个 user_id 标签(100 万用户),序列数直接爆炸到 2400 万条。Prometheus 的 head 内存会被打爆,WAL 暴涨,查询直接 OOM。

铁律:标签里绝不放无界的值(user_idrequest_idemailURL 完整路径timestamp订单号)。这些是无界集合,会制造 cardinality 炸弹。需要按这些维度分析?那是日志/追踪系统的事,别塞进指标。

一个判断技巧:如果一个标签的可能取值会"随时间增长且没有上限",它就不该是指标标签。


三、架构分析:Prometheus 不是单体,是带分工的引擎

很多人以为 Prometheus 就是"一个存数据的进程"。实际上它的内部是几个职责清晰的模块协作。

3.1 组件全景

                ┌─────────────────────────────────────────┐
                │            Prometheus Server             │
                │  ┌──────────┐   ┌──────────┐ ┌────────┐  │
   scrape ───▶  │  │ Retrieval│──▶│  TSDB    │ │ PromQL │  │
  (Pull)        │  │ (抓取)   │   │ (存储)   │ │ Engine │  │
                │  └──────────┘   └────┬─────┘ └───┬────┘  │
                │                     │           │       │
                └─────────────────────┼───────────┼───────┘
                                      │           │
                          WAL/blocks  │      Query/HTTP │
                                      ▼           ▼
                             磁盘 (./data)    HTTP API / Web UI
                                      
  配套组件(独立进程):
  - Exporter:把第三方系统指标转成 Prometheus 格式(node_exporter、mysqld_exporter…)
  - Pushgateway:短任务 push 中转
  - Alertmanager:接收告警、去重、分组、路由、抑制、发送
  - Service Discovery:kubernetes_sd / file_sd / consul_sd / ec2_sd …

数据流一句话:Retrieval 按 scrape_interval 去 Pull 各 target 的 /metrics → 样本写入 TSDB(先 WAL 再内存 head)→ PromQL Engine 响应查询 → 同时 Rules 管理器周期性评估 recording/alerting rules → 告警推给 Alertmanager

3.2 抓取流程(Scrape Loop)细节

一次 scrape 实际做了什么:

  1. 服务发现:根据 scrape_configskubernetes_sd_configs 等,拿到当前所有 target 列表(动态!
  2. Relabel:对 target 的临时标签(__address____meta_*)做改写/过滤,决定最终 scrape 谁、路径是什么、打什么标签。
  3. HTTP GET /metrics:带 Accept: text/plain 请求,默认超时由 scrape_timeout 控制(要小于 scrape_interval)。
  4. 解析文本:按 exposition format 解析,应用 metric_relabel_configs(这是 scrape 之后、入库之前的最后一次过滤/改写——在这里丢弃不需要的标签/序列,能极大省存储)。
  5. 追加到 TSDB head

实战提示:metric_relabel_configsrelabel_configs 更省资源——前者在样本解析后、写盘前丢弃,直接不占存储;后者只是决定"要不要去 scrape"。要砍不需要的指标,用 metric_relabel_configsaction: drop

3.3 TSDB 存储引擎:Prometheus 真正的护城河

这是 Prometheus 区别于"定时存 MySQL"类监控的核心。TSDB 的设计目标:高写入吞吐、低查询延迟、高压缩比、崩溃可恢复

3.3.1 内存中的 Head

最新写入的样本先进入内存 Head

  • 每个 series 在 head 里维护一个活跃 chunk(默认最多存 120 个样本,约 2 小时@15s)。
  • 每个 series 有 headAppendPool 和锁,并发追加是安全的,但高 cardinality 会让锁竞争和内存暴涨。
  • 写入先写 WAL(Write-Ahead Log),再写内存。这样即使崩溃,重启后重放 WAL 就能恢复 head,不丢数据。

3.3.2 持久化:Block 与压缩编码

每隔 min-block-duration(默认 2h),head 里"足够老"的 chunk 被截断、落盘成一个 Block

data/
├── 01HX.../           # 一个 2 小时的 block
│   ├── chunks/        # 样本数据(压缩)
│   ├── index          # 倒排索引:label value → series id → 样本偏移
│   ├── meta.json
│   └── tombstones     # 删除标记(删除序列不是物理擦除,而是记 tombstone)
├── wal/
│   ├── 0000001        # WAL 段
│   └── checkpoint.0001/
└── ...

样本压缩编码是 TSDB 高压缩比的来源:

  • 时间戳:用 double-delta(差值的差值)编码。因为 scrape 间隔固定,时间戳增量几乎恒定,double-delta 大多为 0,编码极小。
  • 值(float64):用 XOR 编码(Facebook Gorilla 算法)。相邻样本如果值相近,XOR 后高位和低位大量为 0,只需存"有意义的比特段"。实测压缩比可达十到几十倍。

3.3.3 倒排索引(Postings)

Prometheus 查询常是"找所有 job="api" 的序列"。如果线性扫描所有 series 会爆炸。TSDB 维护倒排索引

label value "api"  →  [series_id_3, series_id_7, series_id_9, ...]
label value "500"  →  [series_id_7, series_id_22, ...]

查询 job="api" AND code="500" = 取两个 posting list 做交集,O(结果集) 而非 O(全量)。这是 PromQL 能扛住大数据集查询的关键。

3.3.4 Compaction(合并)

后台会周期性把多个小 block 合并成更大的 block(如 2h→4h→...→最大 max-block-duration,默认 36h 上限),并重新压缩、合并倒排索引。注意:Prometheus 自身不做降采样(down-sampling)——保留期内的数据都是原始精度。降采样是 Thanos / Mimir / VictoriaMetrics 的活,后面讲。

3.3.5 乱序样本(Out-of-Order)与 3.x 的改进

现实中网络/时钟问题会导致样本时间戳"乱序"(晚到的样本比已存的还旧)。Prometheus 2.x 直接拒绝乱序样本(丢数据)。Prometheus 3.x 默认开启乱序摄入(OOO),允许在一个有限时间窗(默认 1 小时)内接受乱序样本,大大减少了"样本被丢弃"的诡异问题。这对边缘场景、弱网采集是实打实的改进。

3.4 查询引擎(PromQL Engine)

PromQL 查询经过:解析(AST)→ 规划(决定查哪些 block、哪些 series)→ 执行(向量运算、range 选择、聚合)→ 返回。其中 range vector(如 metric[5m])会触发对多个 block 的样本读取,开销远大于 instant vector。这也是为什么"在 recording rule 里预计算"能救查询性能。

3.5 联邦与远程读写

  • Federation(联邦):让一个 Prometheus 从另一个 Prometheus 的 /federate 拉取已聚合的结果(不是原始样本),用于分层(全局 Prometheus 汇总各区域 Prometheus 的汇总值)。
  • Remote Write / Read:把样本推到(或从不兼容的)远程存储(Thanos、Mimir、Cortex、VictoriaMetrics、InfluxDB 等),解决长期存储和全局视图问题。这是生产环境把 Prometheus "scale out" 的标准姿势。

四、代码实战:从零搭一个生产级指标暴露

光说不练假把式。下面三个实战,覆盖 Go 服务埋点、Python 埋点、以及最容易被写错的 relabel_configs

4.1 Go 微服务:Counter + Histogram + Gauge 全套埋点

这是我认为每个 Go 后端都该有的"标准姿势"——用中间件统一埋点,业务代码零侵入:

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

// 1) 定义指标。用包级变量,MustRegister 保证注册不重复。
var (
	httpRequestsTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "http_requests_total",
			Help: "Total number of HTTP requests handled, by method and code.",
		},
		[]string{"method", "code"}, // 有界标签!method × code,基数可控
	)

	httpRequestDuration = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "http_request_duration_seconds",
			Help:    "Histogram of latencies for HTTP requests.",
			Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
		},
		[]string{"method"},
	)

	inFlight = prometheus.NewGauge(
		prometheus.GaugeOpts{
			Name: "http_in_flight_requests",
			Help: "Current number of in-flight HTTP requests.",
		},
	)

	// 业务指标示例:订单处理耗时(用 Histogram 看分布)
	orderProcessSeconds = prometheus.NewHistogram(
		prometheus.HistogramOpts{
			Name:    "order_process_seconds",
			Help:    "Order processing latency.",
			Buckets: prometheus.DefBuckets,
		},
	)
)

func init() {
	// 2) 注册。重复注册会 panic,用 MustRegister 在启动时暴露问题。
	prometheus.MustRegister(httpRequestsTotal, httpRequestDuration, inFlight, orderProcessSeconds)
}

// 3) 中间件:统一埋点,业务 handler 不用改
func instrument(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		inFlight.Inc()
		defer inFlight.Dec()

		start := time.Now()
		// 用 ResponseWriter 包装拿 status code
		sw := &statusWriter{ResponseWriter: w, status: 200}
		next(sw, r)

		elapsed := time.Since(start).Seconds()
		httpRequestDuration.WithLabelValues(r.Method).Observe(elapsed)
		httpRequestsTotal.WithLabelValues(r.Method, itoa(sw.status)).Inc()
	}
}

type statusWriter struct {
	http.ResponseWriter
	status int
}

func (sw *statusWriter) WriteHeader(code int) {
	sw.status = code
	sw.ResponseWriter.WriteHeader(code)
}

func itoa(i int) string {
	// 简化:生产用 strconv.Itoa
	if i == 200 { return "200" }
	if i == 500 { return "500" }
	return "other"
}

func main() {
	mux := http.NewServeMux()
	// /metrics 端点由 promhttp 提供
	mux.Handle("/metrics", promhttp.Handler())

	mux.HandleFunc("/healthz", instrument(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok"))
	}))

	mux.HandleFunc("/order", instrument(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		// ... 处理订单业务逻辑 ...
		time.Sleep(20 * time.Millisecond)
		orderProcessSeconds.Observe(time.Since(start).Seconds())
		w.WriteHeader(200)
	}))

	log.Println("listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

跑起来后 curl localhost:8080/metrics | grep http_request_duration_seconds_bucket 就能看到分桶数据。然后:

# P99 订单处理延迟
histogram_quantile(0.99, rate(order_process_seconds_bucket[5m]))

# 当前在途请求数(Gauge 直接看)
http_in_flight_requests

# 错误率(用 rate 看 Counter 的变化)
sum(rate(http_requests_total{code="500"}[5m]))
 / sum(rate(http_requests_total[5m]))

4.2 Python 服务:装饰器式埋点

Python 生态用 prometheus-client,用装饰器统计函数耗时几乎是零成本:

from prometheus_client import start_http_server, Summary, Counter, Gauge
import random
import time

REQUEST_TIME = Summary(
    "request_processing_seconds",
    "Time spent processing a single request",
)
TOTAL = Counter("app_requests_total", "Total handled requests")
BACKLOG = Gauge("app_backlog", "Current queued jobs")


@REQUEST_TIME.time()  # 自动记录耗时到 histogram(Summary 类型)
def handle_job(job_id: int):
    BACKLOG.inc()
    try:
        time.sleep(random.uniform(0.01, 0.2))
        TOTAL.inc()
    finally:
        BACKLOG.dec()


if __name__ == "__main__":
    start_http_server(8000)  # 暴露 /metrics 在 8000
    while True:
        handle_job(random.randint(0, 9999))

注意:Summary.time() 是客户端算分位数,不能跨进程 avg。如果要跨多副本聚合延迟,改用 Histogram + histogram_quantile

4.3 prometheus.yml:kubernetes_sd + relabel 实战

这是生产里最容易写错、也最体现功力的部分。下面这段配置实现:只 scrape 带注解 prometheus.io/scrape: "true" 的 Pod,并按注解动态决定路径和端口,同时把 namespace/pod 写进标签:

global:
  scrape_interval: 15s
  evaluation_interval: 15s
  external_labels:
    cluster: prod-cn      # 多集群联邦时区分来源

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'kubernetes-pods'
    kubernetes_sd_configs:
      - role: pod         # 用 K8s API 动态发现 Pod
    relabel_configs:
      # ① 只保留声明了 scrape 注解的 Pod
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: "true"
      # ② 用注解里的 path 覆盖抓取路径(默认 /metrics)
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
        action: replace
        target_label: __metrics_path__
        regex: (.+)
      # ③ 用注解里的 port 拼出真实地址
      - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
        action: replace
        regex: ([^:]+)(?::\d+)?;(\d+)
        replacement: $1:$2
        target_label: __address__
      # ④ 把 K8s 元数据变成永久标签,方便查询切分
      - source_labels: [__meta_kubernetes_namespace]
        action: replace
        target_label: namespace
      - source_labels: [__meta_kubernetes_pod_name]
        action: replace
        target_label: pod
    # ⑤ scrape 之后、入库之前:丢弃不需要的标签,省存储!
    metric_relabel_configs:
      - regex: 'go_.*|process_.*'   # 不要 Go runtime 指标
        action: drop

relabel_configs 发生在"决定抓谁、怎么抓",metric_relabel_configs 发生在"样本落盘前"。两者配合,既有动态发现,又有存储瘦身。

4.4 Recording Rules:把慢查询变成快查询

如果你的看板每天被 100 人刷 sum(rate(http_requests_total[5m])) by (job, method),每次都是实时重算——纯属浪费。用 recording rule 预计算成一条新序列:

groups:
  - name: request_rates
    interval: 30s          # 每 30s 算一次
    rules:
      - record: job_method:http_requests:rate5m
        expr: sum by (job, method) (rate(http_requests_total[5m]))
      - record: job:http_in_flight:sum
        expr: sum by (job) (http_in_flight_requests)

之后看板直接查 job_method:http_requests:rate5m(已经算好的序列),毫秒级返回。命名约定 level:metric:operation 是社区规范,强烈建议遵守,免得半年后没人看得懂这些预计算指标是啥。

4.5 Alerting Rules + Alertmanager:告警不"狼来了"

groups:
  - name: instance_health
    rules:
      - alert: InstanceDown
        expr: up == 0
        for: 5m            # 持续 5 分钟才告警,过滤抖动
        labels:
          severity: page
        annotations:
          summary: "实例 {{ $labels.instance }} 失联"
          description: "job={{ $labels.job }} 的 {{ $labels.instance }} 已超过 5 分钟无响应。"
      - alert: HighErrorRate
        expr: |
          sum(rate(http_requests_total{code=~"5.."}[5m])) by (job)
          / sum(rate(http_requests_total[5m])) by (job) > 0.05
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.job }} 错误率超过 5%"

Alertmanager 负责去重、分组、路由、抑制。下面这段实现"critical 抑制 warning、page 级别走 PagerDuty、其余走邮件":

route:
  receiver: default
  group_by: ['alertname', 'cluster', 'job']
  group_wait: 30s        # 同组第一条等 30s,攒一批一起发
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - match:
        severity: page
      receiver: pagerduty

receivers:
  - name: default
    email_configs:
      - to: 'oncall@example.com'
  - name: pagerduty
    pagerduty_configs:
      - service_key: '<your-integration-key>'

inhibit_rules:
  # 同一 alertname+cluster 下,有 critical 时抑制 warning,避免重复轰炸
  - source_match:
      severity: 'critical'
    target_match:
      severity: 'warning'
    equal: ['alertname', 'cluster']

五、性能优化:当 Prometheus "生病"时怎么治

这是区分"会用"和"精通"的分水岭。下面四个问题是生产环境的高频"急诊"。

5.1 Cardinality 爆炸:头号杀手

症状:Prometheus 内存持续增长不释放、重启后内存暴涨、查询变慢、WAL 体积巨大。

诊断:在 Prometheus 自带的 /api/v1/status/ cardinality 端点(3.x 原生支持)或 Grafana 的 "Prometheus 2.0 Stats" 面板看高基数 label。一条救命查询——找出"单指标序列数最多"的 top:

# 近似看每个 metric 的序列数(在 admin API /tsdb 里更准)
count by (__name__) ({__name__=~".+"})  # 谨慎!大集群这也会很重

更稳的做法是用 cardinality explorer(Grafana Labs 的 cardinality-api 或 Thanos bucket 工具)离线分析。

治疗

  • metric_relabel_configsdrop 掉无界标签;
  • user_id/path 这类维度从指标移到日志(用 exemplar 关联,见 5.4);
  • 对必须保留的高基数指标,考虑用 Native Histogram(见 5.3)或迁移到 VictoriaMetrics(对高基数的内存效率更好)。

5.2 TSDB 内存与磁盘调优

关键启动参数(不是玄学,是看数据):

prometheus \
  --storage.tsdb.retention.time=15d \
  --storage.tsdb.max-block-duration=2h \
  --storage.tsdb.wal-compression \
  --query.max-samples=50000000 \      # 单次查询最多加载的样本数,防 OOM
  --storage.tsdb.head-chunks-limit=4000000
  • retention:本地只留热数据(如 15 天),长期数据走 remote_write 到对象存储(Thanos/Mimir)。本地磁盘别想存一年。
  • head-chunks-limit:限制 head 里 chunk 总数,超出会触发更激进的落盘,防内存 OOM(代价是查询略慢)。
  • wal-compression:WAL 压缩,省磁盘 I/O,默认开。

5.3 查询性能:rate vs irate,Recording 是王道

  • rate vs iraterate 用整个区间算平均速率,平滑、适合看板;irate 只看区间最后两个样本的瞬时速率,对毛刺敏感但波动大、且区间不能太长。看板用 rate,排查瞬时抖动可用 irate
  • 避免全量正则{__name__=~".+"} 会扫所有序列,是性能噩梦。尽量用具体 metric name 开头。
  • 大区间 + 聚合rate(x[1h])rate(x[5m]) 读取的样本多 12 倍。看长期趋势时应配合 recording rule 预计算 1h 速率。
  • 降采样给长期视图:超过 retention 的历史用 Thanos/Mimir 的降采样(5m/1h 精度聚合),查询"过去 90 天 QPS"秒回。

5.4 Exemplar:把指标和追踪连起来

一个高级但极实用的能力:Exemplar(样例)。当你在 Histogram 的 bucket 里附上一个 trace ID,Grafana 就能从"这个延迟桶"一键跳到"对应的那次请求的分布式追踪"。这把指标、日志、追踪三件套真正打通:

// Go 端用 exemplar 把 trace_id 绑到 histogram bucket
obs := httpRequestDuration.WithLabelValues(r.Method)
if span := trace.SpanFromContext(r.Context()); span != nil {
    if ex, ok := obs.(prometheus.ExemplarObserver); ok {
        ex.ObserveWithExemplar(elapsed, prometheus.Labels{
            "trace_id": span.SpanContext().TraceID().String(),
        })
    } else {
        obs.Observe(elapsed)
    }
}

这就是"看到 P99 飙了 → 点进 exemplar → 拿到 trace_id → 跳 Jaeger 看那次慢请求的全链路"。可观测性的闭环就在这里。

5.5 高可用与长期存储:Thanos / Mimir / VictoriaMetrics 怎么选

单机 Prometheus 是无状态但数据本地的,挂了就丢近期数据。生产标准架构是 双副本 + 远程存储

方案思路适合
ThanosSidecar 读 Prometheus 本地 block → 上传对象存储;Query 组件做全局查询;兼容原生 Prometheus已有一堆 Prometheus,想做全局视图+长期存储,改动最小
Mimir (Grafana)完全兼容 Prometheus 远程写,自己管分片/索引/存储从零建大规模多租户监控平台
VictoriaMetrics单机/集群都强,高基数内存效率极佳,查询语法兼容 PromQL高基数场景、想省成本、运维简单

我的观点:中小团队别急着上 Thanos/Mimir 的复杂度,先做好"双副本 + 15~30 天本地 retention + 关键看板 recording rule"。真正到"多集群全局视图 + 一年合规留存"时,再按上表选型——多数情况下 VictoriaMetrics 的性价比和运维友好度被严重低估


六、总结展望:Prometheus 3.x 与可观测性的下一步

6.1 Prometheus 3.0 带来的实质变化

2025 年底进入 3.x 系列,几个对生产有真实价值的变化:

  1. UTF-8 指标名与标签名:正式支持更丰富的字符(如 < > . 在标签值里、metric 名允许更多符号),与 OpenTelemetry 的命名更兼容,迁移时少踩坑。
  2. Native Histogram(原生直方图)GA:不再需要手拍 bucket 边界,客户端自适应的稀疏直方图,精度更高、存储更省。新项目建议直接上。
  3. 乱序摄入默认开启:弱网/边缘采集场景样本不再动不动被丢。
  4. OTLP 接收增强:Prometheus 能直接接收 OpenTelemetry 的 OTLP 指标,少一层 Collector 中转。

6.2 Prometheus 与 OpenTelemetry 的关系:不是替代,是分工

很多人担心"OTel 来了 Prometheus 是不是要被取代"。答案是不会。OTel 定义了采集与导出标准(SDK、Collector、协议 OTLP),而 Prometheus 是存储与查询引擎之一。现实是:OTel Collector 采集 → 通过 OTLP 或 remote_write → 落到 Prometheus / Mimir / VictoriaMetrics。两者是管道和仓库的关系,OTel 负责"标准化地产生和搬运数据",Prometheus 负责"高效地存和查"。2026 年的成熟架构,基本都是 OTel + Prometheus 生态协同。

6.3 什么时候你该考虑"不用 Prometheus"

负责任的工程师要会说"不":

  • 需要秒级以下精度 + 强一致 + 事件不可变(如金融交易审计)→ 这不是时序库的活,用专用的事件存储/消息队列。
  • 需要超长保留(多年)+ 极低成本的海量指标 → 考虑 VictoriaMetrics 或把历史降采样后归档到列式湖仓(如 Iceberg)。
  • 需要多维日志检索/全文搜索 → 那是 Loki / Elasticsearch,别把日志当指标。

6.4 一句话收尾

Prometheus 不是"装一下就有监控"的银弹,它是一套把系统运行状态变成可查询、可聚合、可告警的时间序列的方法论。你真正的资产不是 Prometheus 进程,而是你为业务定义的那些指标、那些 label、那些 recording rule 和 alert。把本文的"多维模型 + 四种类型 + cardinality 纪律 + TSDB 原理 + relabel 实战 + 查询优化"吃透,你就能从"看 Grafana 面板的人",变成"系统出问题时第一个定位到根因的人"——而这,正是可观测性的全部意义。


附录:本文实战清单

  1. Go 服务:CounterVec + HistogramVec + Gauge 中间件埋点(§4.1)
  2. Python 服务:Summary.time() 装饰器埋点(§4.2)
  3. kubernetes_sd + relabel_configs + metric_relabel_configs 动态发现与存储瘦身(§4.3)
  4. Recording Rules 预计算、Alerting Rules + Alertmanager 抑制(§4.4–4.5)
  5. Cardinality 诊断与治理、TSDB 参数、Exemplar 打通追踪(§5)
  6. Thanos / Mimir / VictoriaMetrics 选型判断(§5.5)
复制全文 生成海报 Prometheus 可观测性 云原生 TSDB PromQL 监控

推荐文章

Vue3中如何实现国际化(i18n)?
2024-11-19 06:35:21 +0800 CST
Node.js中接入微信支付
2024-11-19 06:28:31 +0800 CST
Vue3中如何处理路由和导航?
2024-11-18 16:56:14 +0800 CST
用 Rust 构建一个 WebSocket 服务器
2024-11-19 10:08:22 +0800 CST
php内置函数除法取整和取余数
2024-11-19 10:11:51 +0800 CST
Gin 与 Layui 分页 HTML 生成工具
2024-11-19 09:20:21 +0800 CST
jQuery `$.extend()` 用法总结
2024-11-19 02:12:45 +0800 CST
程序员茄子在线接单