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、端口、磁盘用量),超过阈值就发邮件。这套模型有两个致命缺陷:
- 事后才知道:等磁盘 90% 告警时,往往已经晚了;你看到的是"症状",看不到"病因"。
- 静态、脆弱:服务一扩容,监控配置就得改;微服务架构下成百上千个实例,靠手写 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_total有method(4 种) ×code(6 种) = 24 条序列。很健康。- 如果你"好心"加了个
user_id标签(100 万用户),序列数直接爆炸到 2400 万条。Prometheus 的 head 内存会被打爆,WAL 暴涨,查询直接 OOM。
铁律:标签里绝不放无界的值(user_id、request_id、email、URL 完整路径、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 实际做了什么:
- 服务发现:根据
scrape_configs的kubernetes_sd_configs等,拿到当前所有 target 列表(动态! - Relabel:对 target 的临时标签(
__address__、__meta_*)做改写/过滤,决定最终 scrape 谁、路径是什么、打什么标签。 - HTTP GET /metrics:带
Accept: text/plain请求,默认超时由scrape_timeout控制(要小于scrape_interval)。 - 解析文本:按 exposition format 解析,应用
metric_relabel_configs(这是 scrape 之后、入库之前的最后一次过滤/改写——在这里丢弃不需要的标签/序列,能极大省存储)。 - 追加到 TSDB head。
实战提示:
metric_relabel_configs比relabel_configs更省资源——前者在样本解析后、写盘前丢弃,直接不占存储;后者只是决定"要不要去 scrape"。要砍不需要的指标,用metric_relabel_configs的action: 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_configs里drop掉无界标签; - 把
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 是王道
ratevsirate:rate用整个区间算平均速率,平滑、适合看板;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 是无状态但数据本地的,挂了就丢近期数据。生产标准架构是 双副本 + 远程存储:
| 方案 | 思路 | 适合 |
|---|---|---|
| Thanos | Sidecar 读 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 系列,几个对生产有真实价值的变化:
- UTF-8 指标名与标签名:正式支持更丰富的字符(如
<>.在标签值里、metric 名允许更多符号),与 OpenTelemetry 的命名更兼容,迁移时少踩坑。 - Native Histogram(原生直方图)GA:不再需要手拍 bucket 边界,客户端自适应的稀疏直方图,精度更高、存储更省。新项目建议直接上。
- 乱序摄入默认开启:弱网/边缘采集场景样本不再动不动被丢。
- 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 面板的人",变成"系统出问题时第一个定位到根因的人"——而这,正是可观测性的全部意义。
附录:本文实战清单
- Go 服务:
CounterVec+HistogramVec+Gauge中间件埋点(§4.1)- Python 服务:
Summary.time()装饰器埋点(§4.2)kubernetes_sd+relabel_configs+metric_relabel_configs动态发现与存储瘦身(§4.3)- Recording Rules 预计算、Alerting Rules + Alertmanager 抑制(§4.4–4.5)
- Cardinality 诊断与治理、TSDB 参数、Exemplar 打通追踪(§5)
- Thanos / Mimir / VictoriaMetrics 选型判断(§5.5)