2023
年,某大型在线教育平台在春季学期开学日遭遇了一次监控系统崩溃。故障的直接原因不是业务流量过大,而是
Prometheus
本身的内存被撑爆了。事后复盘发现,一位工程师在用户行为追踪的指标中添加了
user_id 作为标签(Label),导致指标时间序列数从
200 万条暴涨到 1.2 亿条。Prometheus 的 TSDB
在内存中维护了所有活跃序列的索引,1.2 亿条序列需要约 120GB
内存——远超机器的 64GB 物理内存。OOM Killer 杀掉了 Prometheus
进程,所有告警规则停止评估,值班工程师在业务系统已经开始报错
20 分钟后才通过用户投诉得知故障。
这就是基数爆炸(Cardinality Explosion)——指标监控领域最常见也最致命的架构问题。
这篇文章要回答三个核心问题:第一,Prometheus 的 pull 模型在大规模场景下遇到哪些瓶颈,联邦(Federation)、远程写入(Remote Write)和 Thanos / VictoriaMetrics 各自如何解决?第二,指标基数爆炸的根因是什么,怎么治?第三,USE、RED、Golden Signals 三种方法论分别适用于什么场景,如何指导你选择该采集哪些指标?
推荐先阅读 日志架构 了解可观测性的日志维度,以及 分布式追踪 了解链路追踪的设计。三者构成可观测性(Observability)的三大支柱。
一、指标的本质:维度数据模型
1.1 什么是指标
指标(Metric)是对系统某个可量化属性在时间维度上的连续记录。与日志(Log)记录离散事件、追踪(Trace)记录请求链路不同,指标关注的是聚合后的数值趋势。
一条指标数据点由三部分组成:
- 指标名称(Metric Name):描述被度量的对象,如
http_requests_total - 标签集(Label Set):键值对的集合,用于区分不同维度,如
{method="GET", path="/api/users", status="200"} - 时间戳与值(Timestamp + Value):某个时刻的数值,如
1681344000 -> 42857
这三者组合在一起,形成了 Prometheus 采用的维度数据模型(Dimensional Data Model)。在这个模型中,每一个唯一的(指标名称 + 标签集)组合构成一条时间序列(Time Series)。
1.2 维度数据模型 vs 层次数据模型
早期的监控系统(如 Graphite、StatsD)采用层次命名模型(Hierarchical Naming Model),指标路径形如:
servers.web01.requests.GET.200.count
servers.web01.requests.POST.500.count
这种模型的问题在于查询灵活性差。如果你想查询所有服务器上状态码为 500 的请求总数,需要遍历所有服务器节点路径。增加新维度(如数据中心)意味着重组整棵路径树。
维度数据模型将维度从路径中解耦为标签:
http_requests_total{server="web01", method="GET", status="200"}
http_requests_total{server="web01", method="POST", status="500"}
你可以在任意维度上灵活聚合:
sum(rate(http_requests_total{status="500"}[5m])) by (server)
这种灵活性是 Prometheus 广泛流行的根本原因之一,但也正是基数爆炸的根源——每增加一个标签值,时间序列数就可能翻倍。
1.3 时间序列基数的计算
基数(Cardinality)是指标名称与所有标签值的笛卡尔积。假设一个指标有以下标签:
| 标签 | 可能的值数量 |
|---|---|
| method | 4(GET、POST、PUT、DELETE) |
| path | 50 |
| status | 5(200、201、400、404、500) |
| instance | 100 |
总基数 = 4 x 50 x 5 x 100 = 100,000 条时间序列。这只是一个指标的基数,一个典型的微服务通常暴露 200-500 个指标,整个集群的总序列数很容易突破千万级别。
二、指标类型:Counter、Gauge、Histogram、Summary
Prometheus 定义了四种基础指标类型,理解它们的语义差异是正确使用指标的前提。
2.1 Counter(计数器)
Counter 是单调递增的累计值,只能增加或在进程重启时归零。典型用途是记录请求总数、错误总数、处理的字节总数。
// Go 语言中使用 Prometheus 客户端库定义 Counter
var httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests processed.",
},
[]string{"method", "path", "status"},
)
func handleRequest(w http.ResponseWriter, r *http.Request) {
// 业务逻辑处理
status := processRequest(w, r)
httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(status)).Inc()
}查询 Counter 时,几乎永远不应该直接使用原始值,而是通过
rate() 或 increase()
计算速率:
# 过去 5 分钟的每秒请求速率
rate(http_requests_total[5m])
# 过去 1 小时的请求增量
increase(http_requests_total[1h])
一个常见的错误是对 Counter 使用 sum()
而不先取
rate(),这会得到一个无意义的不断增长的累计值。
2.2 Gauge(仪表盘)
Gauge 是可以任意上下波动的瞬时值。典型用途是当前温度、内存使用量、队列深度、活跃连接数。
var activeConnections = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "tcp_active_connections",
Help: "Number of currently active TCP connections.",
},
)
func onConnect() {
activeConnections.Inc()
}
func onDisconnect() {
activeConnections.Dec()
}Gauge 可以直接聚合,也可以计算变化率:
# 当前所有实例的活跃连接总数
sum(tcp_active_connections)
# 内存使用的变化趋势(每秒增减)
deriv(process_resident_memory_bytes[5m])
2.3 Histogram(直方图)
Histogram 将观测值分配到预定义的桶(Bucket)中,用于计算分位数(Quantile)和分布情况。每个 Histogram 指标实际上会生成三组时间序列:
_bucket{le="..."}:每个桶的累计计数_sum:所有观测值的总和_count:观测值的总数量
var requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds.",
Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
},
[]string{"method", "path"},
)
func handleRequest(w http.ResponseWriter, r *http.Request) {
start := time.Now()
processRequest(w, r)
duration := time.Since(start).Seconds()
requestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
}使用 histogram_quantile()
函数计算分位数:
# 计算 P99 延迟
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
# 计算 P50 延迟,按服务分组
histogram_quantile(0.50,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
)
Histogram
的关键设计决策是桶边界的选择。桶太少会丢失精度,桶太多会增加时间序列基数。11
个桶的 Histogram 加上 _sum 和
_count,一共生成 13 条时间序列。如果再有 4
个标签维度组合出 1000 种可能,一个 Histogram 指标就会产生
13,000 条时间序列。
2.4 Summary(摘要)
Summary 与 Histogram 类似,但它在客户端直接计算分位数,而不是在服务端通过桶来近似。
var requestDurationSummary = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "http_request_duration_summary_seconds",
Help: "HTTP request duration in seconds (summary).",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
MaxAge: 10 * time.Minute,
},
[]string{"method"},
)Histogram 与 Summary 的核心区别:
| 特性 | Histogram | Summary |
|---|---|---|
| 分位数计算位置 | 服务端(PromQL) | 客户端 |
| 可跨实例聚合 | 可以 | 不可以 |
| 精度 | 取决于桶边界 | 取决于配置的误差范围 |
| 时间序列数 | 桶数量 + 2 | 分位数数量 + 2 |
| 推荐场景 | 需要聚合的场景 | 单实例精确分位数 |
实际工程建议:优先使用 Histogram。 原因是 Summary 的分位数无法跨实例聚合——你不能把 10 个实例各自的 P99 取平均来得到全局 P99,这在数学上是错误的。Histogram 通过在服务端聚合桶计数再计算分位数,可以给出全局有效的结果。
三、Prometheus 架构:pull 模型的优势与瓶颈
3.1 pull 模型的工作原理
Prometheus 的核心架构基于 pull(拉取)模型:Prometheus
Server 按照配置的抓取间隔(Scrape Interval),主动通过 HTTP
请求从目标实例的 /metrics
端点拉取指标数据。
# prometheus.yml - 基础抓取配置
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "api-server"
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: namespace3.2 pull 模型的优势
pull 模型看似简单,但在工程上有几个重要优势:
服务发现与健康检测一体化。 如果
Prometheus 无法拉取某个实例的指标,up
指标自动变为 0,无需额外的健康检查机制。这比 push
模型中需要区分”目标已停止”和”目标网络不可达”要简单得多。
无需客户端缓冲。 在 push 模型中,如果接收端暂时不可用,客户端需要缓冲数据以防丢失。pull 模型中,客户端只需维护当前状态,不关心谁来拉取、什么时候拉取。
集中式配置管理。 抓取目标和频率由 Prometheus 统一管理,变更一处即可,不需要修改每个被监控实例的配置。
简化防火墙规则。 Prometheus 发起连接,被监控实例只需暴露一个 HTTP 端口,不需要知道 Prometheus 的地址。
3.3 pull 模型的瓶颈
然而,pull 模型在大规模场景下面临三个核心瓶颈:
瓶颈一:单实例抓取能力有限。 每次抓取是一个同步 HTTP 请求。假设平均抓取时间为 100ms,Prometheus 使用约 20 个并发抓取 goroutine,理论上每秒能完成 200 次抓取。如果抓取间隔是 15 秒,最多支持 3000 个抓取目标。当目标实例超过 5000 个,抓取就开始出现延迟堆积。
瓶颈二:内存与 TSDB 写入压力。 每条活跃的时间序列在内存中需要约 1-2KB 的索引和缓冲空间。1000 万条序列需要 10-20GB 内存。TSDB 的 Head Block 每 2 小时压缩一次(Compaction),压缩期间会产生显著的 CPU 和 I/O 峰值。
瓶颈三:无法跨数据中心拉取。 pull 模型要求 Prometheus 能直接访问被监控实例的 HTTP 端口。跨数据中心、跨网络边界的拉取在安全策略和网络延迟上都不现实。
3.4 push vs pull 模型对比
| 特性 | pull(Prometheus) | push(Datadog Agent、InfluxDB) |
|---|---|---|
| 服务发现 | Prometheus 端统一管理 | 客户端需要知道接收端地址 |
| 健康检测 | 拉取失败即表示不健康 | 需要额外心跳机制 |
| 短生命周期任务 | 需要 Pushgateway 辅助 | 天然支持 |
| 防火墙友好性 | 被监控端只需暴露端口 | 客户端需要能连接到接收端 |
| 网络边界穿透 | 困难 | 相对容易 |
| 背压控制 | Prometheus 控制速率 | 客户端可能淹没接收端 |
| 数据新鲜度 | 受抓取间隔限制 | 可实时推送 |
对于短生命周期任务(如批处理作业、CronJob),Prometheus 提供了 Pushgateway(推送网关)作为桥接。但 Pushgateway 本身是一个单点,且不具备自动清理过期指标的能力,生产环境中需要谨慎使用。
四、时序数据库内部机制:TSDB 的存储与压缩
理解 Prometheus TSDB 的内部结构对于容量规划和性能调优至关重要。
4.1 Block 结构
Prometheus TSDB 将数据组织为时间分块(Block),每个 Block 包含一个固定时间范围(默认 2 小时)的数据:
data/
├── 01BKGV7JBM69T2G1BGBGM6KB12/ # Block 1 (时间范围: T0 - T0+2h)
│ ├── meta.json # 元数据:时间范围、序列数、样本数
│ ├── index # 倒排索引:标签 -> 序列 ID
│ ├── chunks/ # 压缩后的时序数据
│ │ └── 000001
│ └── tombstones # 删除标记
├── 01BKGTZQ1SYQJTR4PB43C8PD98/ # Block 2 (时间范围: T0+2h - T0+4h)
│ ├── ...
├── wal/ # 预写日志(Write-Ahead Log)
│ ├── 00000001
│ └── 00000002
└── chunks_head/ # 内存映射的 Head 块
Head Block 是当前正在写入的内存块,所有新到达的样本首先写入 Head Block 和 WAL(Write-Ahead Log,预写日志)。WAL 确保进程崩溃后数据可以恢复。每 2 小时,Head Block 中的数据被压缩写入磁盘,形成一个持久化的 Block。
4.2 压缩与合并
TSDB 的压缩(Compaction)过程分为两个阶段:
垂直压缩(Head Compaction)。 每 2 小时,将 Head Block 中的内存数据刷写到磁盘。这个过程包括:构建倒排索引、对时间序列数据进行 Gorilla 压缩(Double-delta 编码 + XOR 编码)、写入 chunks 文件。
水平压缩(Block Merge)。 多个相邻的小 Block 被合并成更大的 Block。默认策略是将 3 个相邻 Block 合并为 1 个,所以最终会形成 6 小时、18 小时、54 小时等不同粒度的 Block。合并过程中会删除已标记为 tombstone 的数据、合并重叠的序列。
压缩前:
[Block 2h] [Block 2h] [Block 2h] [Block 2h] [Block 2h] [Block 2h]
第一轮压缩:
[ Block 6h ] [ Block 6h ]
第二轮压缩:
[ Block 18h ]
4.3 Gorilla 压缩算法
Prometheus TSDB 采用 Facebook 在 2015 年论文”Gorilla: A Fast, Scalable, In-Memory Time Series Database”中提出的压缩算法。核心思路是利用时间序列的两个统计特性:
时间戳通常等间隔。 使用 Double-delta 编码:存储时间戳差值的差值。如果抓取间隔稳定为 15 秒,连续时间戳的差值为 15,差值的差值为 0,可以用 1 个 bit 表示。
相邻数据点的值通常接近。 使用 XOR 编码:存储当前值与前一个值的 XOR 结果。对于变化缓慢的指标(如 CPU 温度),XOR 结果中大部分 bit 为 0,可以高效压缩。
实测中,Gorilla 压缩可以将每个数据点的存储从 16 字节(8 字节时间戳 + 8 字节 float64)压缩到平均 1.37 字节,压缩比约 12:1。
4.4 保留策略与容量规划
Prometheus 的默认数据保留时间为 15 天。磁盘空间的计算公式:
磁盘空间 = 活跃序列数 x 每秒样本数 x 每样本字节数 x 保留时间
示例:
- 活跃序列:500 万条
- 抓取间隔:15 秒 -> 每秒样本数 = 5,000,000 / 15 = 333,333
- 每样本压缩后大小:约 1.5 字节
- 保留时间:15 天 = 1,296,000 秒
磁盘空间 = 333,333 x 1.5 x 1,296,000 = 648 GB
内存需求的粗略估算:
内存 ≈ 活跃序列数 x 每序列 1-2 KB + Head Block 样本缓冲
示例:
- 500 万活跃序列 x 2 KB = 10 GB
- Head Block 2 小时缓冲 ≈ 333,333 x 1.5 x 7200 = 3.6 GB
- 总计约 14 GB(加上查询缓存、索引等开销,建议预留 20-25 GB)
五、基数爆炸:成因、检测与治理
5.1 基数爆炸的典型场景
基数爆炸的本质是标签值空间不受控增长。以下是最常见的几种场景:
场景一:将高基数标识符作为标签。 用户 ID、订单 ID、请求 ID、Session ID 这些标识符的值空间是无界的。把它们放入标签,序列数会随业务增长线性膨胀。
// 错误做法:user_id 作为标签
httpRequests.WithLabelValues(r.Method, r.URL.Path, userID).Inc()
// 正确做法:user_id 放入日志或追踪系统,指标只保留低基数维度
httpRequests.WithLabelValues(r.Method, r.URL.Path, statusCode).Inc()场景二:URL 路径未归一化。 RESTful API
的路径参数如 /api/users/12345 和
/api/users/67890 是不同的路径。如果直接将原始
URL 作为标签值,路径参数的取值空间等于用户数。
// 错误做法:原始路径
path := r.URL.Path // "/api/users/12345"
// 正确做法:路径模板化
path := matchRouteTemplate(r) // "/api/users/:id"场景三:Histogram 桶数量与标签维度叠加。
假设一个 Histogram 有 20 个桶,加上 _sum 和
_count,一共 22 条序列。如果再有
method(4 个值)、path(100
个值)、status(5 个值)三个标签,总序列数 = 22
x 4 x 100 x 5 = 44,000 条。仅这一个指标就贡献了近 5
万条序列。
场景四:动态生成的标签值。 错误信息、异常堆栈、正则匹配结果等不可枚举的值被用作标签。
5.2 检测基数问题
Prometheus 提供了内置指标用于监控自身的基数状态:
# 当前活跃时间序列总数
prometheus_tsdb_head_series
# 过去一小时新创建的序列数(高值意味着基数在增长)
rate(prometheus_tsdb_head_series_created_total[1h])
# 每个指标名称的序列数 Top 10
topk(10, count by (__name__) ({__name__=~".+"}))
# 每个任务的序列数
count by (job) ({__name__=~".+"})
还可以使用 Prometheus 的 TSDB Status API 获取基数信息:
curl -s http://localhost:9090/api/v1/status/tsdb | python3 -m json.tool返回结果中的 seriesCountByMetricName
字段列出了每个指标名称的序列数,是定位基数问题最直接的手段。
5.3 治理策略
策略一:Relabel 过滤。 在抓取阶段通过
metric_relabel_configs
丢弃不需要的标签或指标:
scrape_configs:
- job_name: "api-server"
metric_relabel_configs:
# 丢弃包含高基数标签的指标
- source_labels: [__name__]
regex: "http_requests_with_user_id_total"
action: drop
# 移除不需要的标签
- regex: "user_id"
action: labeldrop
# 将 URL 路径参数替换为模板
- source_labels: [path]
regex: "/api/users/[0-9]+"
target_label: path
replacement: "/api/users/:id"策略二:Recording Rules 预聚合。 使用记录规则(Recording Rules)在 Prometheus 内部预先计算聚合后的时间序列,减少查询时的实时计算量,同时可以丢弃原始高基数序列:
# recording_rules.yml
groups:
- name: api_aggregations
interval: 30s
rules:
# 预聚合:按状态码和方法汇总,丢弃 path 维度
- record: api:http_requests:rate5m
expr: sum(rate(http_requests_total[5m])) by (method, status)
# 预聚合:P99 延迟,按服务汇总
- record: api:http_request_duration:p99_5m
expr: histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))
# 预聚合:错误率
- record: api:http_errors:ratio_5m
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)策略三:客户端侧控制。 在指标定义时就限制标签值的取值范围:
// 定义一个有界的标签值集合
var validStatuses = map[int]string{
200: "2xx",
201: "2xx",
301: "3xx",
302: "3xx",
400: "4xx",
401: "4xx",
403: "4xx",
404: "4xx",
500: "5xx",
502: "5xx",
503: "5xx",
}
func statusBucket(code int) string {
if bucket, ok := validStatuses[code]; ok {
return bucket
}
if code >= 200 && code < 300 {
return "2xx"
}
if code >= 300 && code < 400 {
return "3xx"
}
if code >= 400 && code < 500 {
return "4xx"
}
return "5xx"
}策略四:指标审计流程。 建立制度化的指标评审机制。任何新增的指标或标签在上线前必须经过评审,评审要点包括:
- 每个标签的值空间是否有界?上界是多少?
- 这个标签是否会随业务增长而无限膨胀?
- 这个指标的总序列数预估是多少?
- 是否有更低基数的替代方案能满足同样的监控需求?
5.4 工程案例:某金融科技公司的基数治理
某金融科技公司运营着约 2000 个微服务实例,Prometheus 集群管理的活跃时间序列约 800 万条。2023 年 Q3,序列数在两周内从 800 万飙升到 3500 万,Prometheus 内存从 32GB 涨到 85GB,触发了 OOM。
排查过程。 通过 TSDB Status API
发现,payment_transaction_duration_seconds_bucket
这一个指标贡献了 2400 万条序列。进一步分析发现:
- 该 Histogram 有 15 个桶 +
_sum+_count= 17 条基础序列 - 标签维度:
merchant_id(1200 个值)xpayment_method(8 个值)xcurrency(25 个值)xregion(5 个值)xstatus(6 个值) - 总计:17 x 1200 x 8 x 25 x 5 x 6 = 24,480,000 条序列
根因是 merchant_id
标签——一个高基数标识符被直接暴露在 Histogram 指标中。
治理方案:
- 移除
merchant_id标签,将商户维度的分析下沉到日志系统。序列数从 2400 万降到 20,400 条(17 x 8 x 25 x 5 x 6)。 - 减少桶数量从 15 个到 8 个(去掉了几个不必要的中间桶),序列数进一步降到 12,000 条。
- 建立指标评审制度:所有新指标需要在 PR 中计算预估基数,超过 10,000 条序列的需要架构评审。
- 部署基数监控告警:当
prometheus_tsdb_head_series超过 1000 万时触发 P2 告警。
治理后,活跃序列数稳定在 600 万左右,Prometheus 内存稳定在 18GB。
六、自定义指标的命名规范
良好的命名规范是指标可维护性的基础。Prometheus 社区总结了一套被广泛采纳的最佳实践。
6.1 指标名称规范
指标名称应遵循以下格式:
<namespace>_<subsystem>_<name>_<unit>_<suffix>
具体规则:
| 规则 | 正确示例 | 错误示例 |
|---|---|---|
| 使用蛇形命名法(snake_case) | http_request_duration_seconds |
httpRequestDurationSeconds |
| 包含单位后缀 | node_memory_usage_bytes |
node_memory_usage |
Counter 以 _total 结尾 |
http_requests_total |
http_requests |
| 时间单位用秒(seconds) | request_duration_seconds |
request_duration_ms |
| 字节单位用字节(bytes) | response_size_bytes |
response_size_kb |
| 使用基础单位 | process_cpu_seconds_total |
process_cpu_minutes_total |
避免使用 type 作为标签名 |
{cache_operation="hit"} |
{type="cache_hit"} |
6.2 标签命名规范
标签命名同样重要:
# 好的标签命名
http_requests_total{method="GET", status_code="200", service="user-api", environment="production"}
# 差的标签命名
http_requests_total{Method="GET", sc="200", svc="user-api", env="prod"}
关键原则:
- 标签名使用蛇形命名法,全小写
- 不使用缩写,除非是广泛认可的缩写(如
http、tcp) - 标签值应该是有意义的字符串,不是编码值
- 避免将同一信息同时放在指标名称和标签中
6.3 单位约定
Prometheus 社区统一使用基础单位:
| 度量类型 | 基础单位 | 后缀 |
|---|---|---|
| 时间 | 秒 | _seconds |
| 温度 | 摄氏度 | _celsius |
| 字节数 | 字节 | _bytes |
| 比率 | 无维度(0-1 之间) | _ratio |
| 电压 | 伏特 | _volts |
| 计数 | 个 | _total(Counter)或无后缀(Gauge) |
使用基础单位的好处是 Grafana 可以自动做单位换算——一个以秒为单位的指标在图表上可以自动显示为毫秒或分钟,但如果原始单位是毫秒,自动换算就会出错。
七、大规模架构:联邦、远程写入与全局视图
当单个 Prometheus 实例无法满足需求时,需要扩展架构。主流的三种方案是 Prometheus 联邦、Thanos 和 VictoriaMetrics。
7.1 Prometheus 联邦
联邦(Federation)是 Prometheus 原生支持的层次化扩展方案。其思路是部署多个分区 Prometheus 实例,各自负责抓取一部分目标,再部署一个全局 Prometheus 从各分区实例拉取聚合后的指标。
# 全局 Prometheus 的联邦配置
scrape_configs:
- job_name: "federation"
honor_labels: true
metrics_path: "/federate"
params:
match[]:
- '{__name__=~"api:.*"}' # 只拉取 Recording Rules 预聚合的指标
- '{__name__=~"up"}' # 拉取健康状态
- '{__name__=~"node_.*"}' # 拉取节点指标
static_configs:
- targets:
- "prometheus-shard-1:9090"
- "prometheus-shard-2:9090"
- "prometheus-shard-3:9090"联邦的限制:
- 全局 Prometheus 仍然是单点,成为新的瓶颈
- 只适合拉取预聚合后的低基数指标,无法实现真正的全局查询
- 跨分区的 PromQL 查询无法执行
7.2 Thanos 架构
Thanos 是 CNCF(Cloud Native Computing Foundation)的毕业项目,提供了 Prometheus 的长期存储(Long-term Storage)和全局查询视图(Global Query View)能力。
graph TB
subgraph 数据中心 A
P1[Prometheus 1] --> S1[Thanos Sidecar 1]
P2[Prometheus 2] --> S2[Thanos Sidecar 2]
end
subgraph 数据中心 B
P3[Prometheus 3] --> S3[Thanos Sidecar 3]
P4[Prometheus 4] --> S4[Thanos Sidecar 4]
end
S1 --> OBJ[(对象存储<br/>S3 / GCS / MinIO)]
S2 --> OBJ
S3 --> OBJ
S4 --> OBJ
S1 --> TQ[Thanos Query]
S2 --> TQ
S3 --> TQ
S4 --> TQ
STORE[Thanos Store Gateway] --> TQ
OBJ --> STORE
TQ --> GRAFANA[Grafana]
COMPACT[Thanos Compactor] --> OBJ
RULER[Thanos Ruler] --> TQ
Thanos 的核心组件:
Sidecar。 以 sidecar 容器的形式与 Prometheus 一起部署。它做两件事:(1)将 Prometheus 的 Block 数据上传到对象存储(Object Storage),实现长期存储;(2)暴露 StoreAPI gRPC 接口,允许 Thanos Query 实时查询 Prometheus 的 Head Block 数据。
Store Gateway。 从对象存储中读取历史 Block 数据,同样暴露 StoreAPI 接口。它在本地缓存 Block 的索引头部(Index Header),减少对象存储的查询次数。
Query(Querier)。 实现了完整的 PromQL 引擎,向上游提供 Prometheus 兼容的查询 API。它同时查询所有 Sidecar 和 Store Gateway,合并去重后返回结果。
Compactor。 在对象存储中执行 Block 的压缩和降采样(Downsampling)。降采样会生成 5 分钟和 1 小时粒度的数据,用于加速长时间范围的查询。
Ruler。 分布式的规则评估引擎,替代 Prometheus 本地的告警和记录规则。
去重(Deduplication)。
高可用部署中通常运行两个相同配置的 Prometheus
实例,各自独立抓取相同的目标。Thanos Query 通过
--query.replica-label
参数进行去重,确保查询结果不会重复。
# Thanos Query 启动参数
thanos query \
--http-address="0.0.0.0:9090" \
--store="prometheus-1-sidecar:10901" \
--store="prometheus-2-sidecar:10901" \
--store="thanos-store:10901" \
--query.replica-label="replica" \
--query.auto-downsampling7.3 VictoriaMetrics 架构
VictoriaMetrics 是用 Go 编写的高性能时序数据库,兼容 Prometheus 的远程写入(Remote Write)协议和 PromQL(通过 MetricsQL 扩展)。它的集群版本采用了存储与计算分离的架构:
| 组件 | 职责 |
|---|---|
| vminsert | 接收远程写入数据,分片后写入 vmstorage |
| vmstorage | 存储时序数据,支持多副本 |
| vmselect | 接收查询请求,从多个 vmstorage 读取数据并合并 |
VictoriaMetrics 的关键优势:
- 更高的压缩率。 相比 Prometheus TSDB,VictoriaMetrics 的压缩率通常高 2-7 倍,意味着同样的数据占用更少的磁盘空间。
- 更低的内存消耗。 通过更激进的内存管理策略,处理相同基数的序列需要更少的内存。
- 原生集群支持。 无需 Thanos 这样的外部组件即可实现水平扩展。
- MetricsQL 扩展。 在 PromQL 基础上增加了
keep_last_value()、range_first()等实用函数。
# Prometheus 远程写入到 VictoriaMetrics
remote_write:
- url: "http://vminsert:8480/insert/0/prometheus/api/v1/write"
queue_config:
max_samples_per_send: 10000
batch_send_deadline: 5s
max_shards: 307.4 三种方案的对比
| 特性 | Prometheus 联邦 | Thanos | VictoriaMetrics 集群 |
|---|---|---|---|
| 长期存储 | 不支持(受本地磁盘限制) | 对象存储(S3、GCS、MinIO) | 本地磁盘或对象存储 |
| 全局查询 | 仅限预聚合指标 | 完整 PromQL | 完整 MetricsQL(PromQL 超集) |
| 高可用 | 需手动部署双副本 | 原生支持去重 | 原生支持多副本 |
| 降采样 | 不支持 | 支持(5m、1h) | 不需要(查询时自动优化) |
| 运维复杂度 | 低 | 高(6+ 组件) | 中(3 组件) |
| 压缩率 | 基线 | 与 Prometheus 相当 | 高于 Prometheus 2-7 倍 |
| 社区生态 | Prometheus 原生 | CNCF 毕业项目 | 活跃开源,商业公司支持 |
| 适用规模 | 千级实例 | 万级实例 | 万级实例 |
| 数据一致性 | 最终一致 | 最终一致 | 最终一致 |
| 学习曲线 | 低 | 高 | 中 |
选型建议:
- 如果集群规模在 500 个实例以下,且数据保留不超过 30 天,单个 Prometheus 配合合理的 Recording Rules 通常就够了。
- 如果需要长期存储(数月到数年)和跨数据中心的全局视图,且团队有较强的 Kubernetes 运维能力,选择 Thanos。
- 如果追求更高的性能和更简单的运维,尤其是在资源受限的环境中,选择 VictoriaMetrics。
八、USE / RED / Golden Signals 方法论
知道了怎么采集和存储指标之后,下一个问题是:应该采集哪些指标?三种经典方法论给出了不同的答案。
8.1 USE 方法
USE(Utilization, Saturation, Errors)方法由 Brendan Gregg 提出,专门用于分析基础设施资源(Infrastructure Resources)的性能问题。
- 利用率(Utilization): 资源在单位时间内的繁忙程度,通常表示为百分比。
- 饱和度(Saturation): 资源排队等待的工作量,表示资源超出容量的程度。
- 错误(Errors): 资源处理失败的事件数。
对每种资源类型应用 USE 矩阵:
| 资源 | 利用率(U) | 饱和度(S) | 错误(E) |
|---|---|---|---|
| CPU | node_cpu_seconds_total 计算使用率 |
运行队列长度 node_load1 |
机器校验错误(较少见) |
| 内存 | node_memory_MemAvailable_bytes |
swap 使用量、OOM 事件 | ECC 校正错误 |
| 磁盘 I/O | node_disk_io_time_seconds_total |
node_disk_io_time_weighted_seconds_total |
node_disk_read_errors_total |
| 网络 | 带宽利用率 | node_netstat_Tcp_ListenOverflows |
node_network_receive_errs_total |
具体的 PromQL 示例:
# CPU 利用率(排除 idle 模式)
1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))
# 内存利用率
1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)
# 磁盘 I/O 利用率(每秒花在 I/O 上的时间占比)
rate(node_disk_io_time_seconds_total[5m])
# 网络饱和度:TCP 监听溢出
rate(node_netstat_Tcp_ListenOverflows[5m])
USE 方法的适用场景: 硬件和操作系统层面的资源分析。当你怀疑某个性能问题来源于基础设施层(而不是应用层)时,USE 方法是系统性排查的最佳框架。
8.2 RED 方法
RED(Rate, Errors, Duration)方法由 Tom Wilkie 提出,专门用于监控面向用户的服务(Request-driven Services)。
- 速率(Rate): 每秒处理的请求数。
- 错误(Errors): 每秒失败的请求数。
- 持续时间(Duration): 每个请求的处理时间分布。
RED 方法的核心理念是:对于一个服务的外部消费者来说,他们关心的只有三件事——服务是否在处理请求(Rate)、请求是否成功(Errors)、请求有多快(Duration)。
# Rate:每秒请求速率
sum(rate(http_requests_total[5m])) by (service)
# Errors:错误率
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
# Duration:P50 / P90 / P99 延迟
histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))
histogram_quantile(0.90, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))
8.3 Golden Signals
Google SRE 在”Site Reliability Engineering”一书中提出了四个黄金信号(Four Golden Signals):
- 延迟(Latency): 处理请求所花的时间,需要区分成功请求和失败请求的延迟。
- 流量(Traffic): 系统的需求量度量,如每秒 HTTP 请求数、每秒数据库事务数。
- 错误(Errors): 请求失败的比率,包括显式错误(5xx)和隐式错误(成功但延迟超过 SLO)。
- 饱和度(Saturation): 系统中资源最受约束部分的利用程度。
Golden Signals 本质上是 USE 和 RED 的融合——它既包含了面向用户的信号(延迟、流量、错误),也包含了面向资源的信号(饱和度)。
8.4 三种方法论的选择指南
| 方法论 | 适用对象 | 关注视角 | 典型场景 |
|---|---|---|---|
| USE | 基础设施资源 | 资源内部 | 排查 CPU、内存、磁盘、网络瓶颈 |
| RED | 请求驱动的服务 | 用户体验 | 监控 API 网关、Web 服务、微服务 |
| Golden Signals | 通用 | 用户体验 + 资源 | SRE 值班大盘、SLO 定义 |
实际工程中的建议:
- 对于微服务的应用层监控,使用 RED 方法。每个服务暴露 Rate、Errors、Duration 三组指标,这是 Grafana Dashboard 的核心内容。
- 对于 Kubernetes 节点和中间件(数据库、消息队列、缓存)的监控,使用 USE 方法。
- 对于 SLO(Service Level Objective)定义和值班大盘,使用 Golden Signals 作为框架。
以一个电商系统为例:
用户请求 -> API 网关 -> 订单服务 -> 支付服务 -> 数据库
API 网关: RED(请求速率、错误率、P99 延迟)
订单服务: RED(请求速率、错误率、P99 延迟)
支付服务: RED(请求速率、错误率、P99 延迟)
Kubernetes 节点:USE(CPU 利用率/饱和度/错误、内存利用率/饱和度/错误)
MySQL 数据库: USE(连接池利用率、查询队列深度、慢查询数) + RED(QPS、错误率、查询延迟)
Redis 缓存: USE(内存利用率、连接数饱和度) + RED(命令速率、缓存命中率、命令延迟)
九、告警规则设计
指标最终要转化为可操作的告警。告警规则的质量直接影响值班工程师的效率和幸福感。
9.1 告警规则的基本结构
# alerting_rules.yml
groups:
- name: service_alerts
rules:
# 高错误率告警
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
> 0.05
for: 5m
labels:
severity: critical
team: backend
annotations:
summary: "服务 {{ $labels.service }} 错误率超过 5%"
description: "过去 5 分钟错误率为 {{ $value | humanizePercentage }},请立即排查。"
runbook_url: "https://wiki.internal/runbooks/high-error-rate"
# P99 延迟告警
- alert: HighLatencyP99
expr: |
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
) > 2
for: 10m
labels:
severity: warning
team: backend
annotations:
summary: "服务 {{ $labels.service }} P99 延迟超过 2 秒"
description: "当前 P99 延迟为 {{ $value | humanizeDuration }}。"
# 实例宕机告警
- alert: InstanceDown
expr: up == 0
for: 3m
labels:
severity: critical
annotations:
summary: "实例 {{ $labels.instance }} 不可达"
description: "Prometheus 已连续 3 分钟无法抓取 {{ $labels.job }} 的实例 {{ $labels.instance }}。"9.2 告警设计的最佳实践
使用 for 子句避免瞬时抖动。
所有告警都应该设置 for 持续时间。P1
告警至少持续 2-3 分钟,P3 告警可以持续 15-30 分钟。没有
for
的告警会在指标波动时反复触发和恢复,造成告警风暴(Alert
Storm)。
基于症状告警,而非原因告警。 用户感知到的症状(如”API 延迟高”、“错误率高”)应该触发值班响应。底层原因(如”CPU 利用率高”、“磁盘空间不足”)可以作为辅助信息出现在告警描述中,但不应该作为独立的值班告警,除非它们会直接导致服务中断。
# 好的告警:基于用户感知的症状
- alert: OrderAPIHighLatency
expr: histogram_quantile(0.99, sum(rate(order_api_request_duration_seconds_bucket[5m])) by (le)) > 1
for: 5m
# 差的告警:基于底层原因,但不一定影响用户
- alert: HighCPUUsage
expr: 1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) > 0.9
for: 5m
# CPU 90% 不一定意味着服务有问题,可能是正常的高负载每条告警都附带 Runbook URL。 Runbook 是处理该告警的操作手册,包含排查步骤、缓解措施和升级流程。新入职的值班工程师在凌晨 3 点被叫醒时,Runbook 是他唯一能依赖的东西。
分级(Severity Levels)要严格定义。
| 级别 | 含义 | 响应时间 | 示例 |
|---|---|---|---|
| critical | 正在影响用户 | 立即响应 | 错误率 > 5%、核心服务不可用 |
| warning | 可能即将影响用户 | 工作时间处理 | P99 延迟超标、磁盘使用率 > 80% |
| info | 需要关注但不紧急 | 下个迭代处理 | 证书即将过期、版本落后 |
9.3 多窗口多燃烧率告警
对于 SLO(Service Level Objective,服务级别目标)驱动的告警,Google SRE 推荐使用多窗口多燃烧率(Multi-window Multi-burn-rate)策略:
# 基于 SLO 的告警:99.9% 可用性目标
# 月度错误预算 = 0.1% x 30 天 = 43.2 分钟
# 快速燃烧:1 小时窗口内消耗了 2% 的月度预算
- alert: SLOBurnRateFast
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[1h])) by (service)
/
sum(rate(http_requests_total[1h])) by (service)
) > (14.4 * 0.001)
and
(
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
) > (14.4 * 0.001)
for: 2m
labels:
severity: critical
# 慢速燃烧:6 小时窗口内消耗了 5% 的月度预算
- alert: SLOBurnRateSlow
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[6h])) by (service)
/
sum(rate(http_requests_total[6h])) by (service)
) > (6 * 0.001)
and
(
sum(rate(http_requests_total{status=~"5.."}[30m])) by (service)
/
sum(rate(http_requests_total[30m])) by (service)
) > (6 * 0.001)
for: 15m
labels:
severity: warning这种策略的优势在于:快速燃烧告警能在几分钟内检测到突发故障,慢速燃烧告警能检测到持续但不严重的性能退化。两者结合,既不会漏报也不会产生过多噪音。
十、Prometheus 高可用部署
10.1 双副本模式
最简单的高可用方案是部署两个配置完全相同的 Prometheus 实例,各自独立抓取相同的目标。
# prometheus-a.yml
global:
scrape_interval: 15s
external_labels:
replica: "a" # 用于 Thanos 去重
cluster: "production"
# prometheus-b.yml(与 a 完全相同,只有 replica 标签不同)
global:
scrape_interval: 15s
external_labels:
replica: "b"
cluster: "production"两个实例前面放一个负载均衡器(如 Nginx 或 HAProxy),Grafana 指向负载均衡器的地址。当一个实例宕机时,请求自动切换到另一个实例。
这种方案的缺点是:两个实例的数据不完全一致(因为抓取时间略有偏移),在切换时可能看到数据跳变。Thanos Query 的去重功能可以解决这个问题。
10.2 分片(Sharding)
当单个 Prometheus 实例的抓取能力不足时,可以按目标或指标进行分片:
# prometheus-shard-0.yml - 基于 hashmod 的自动分片
scrape_configs:
- job_name: "api-servers"
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__address__]
modulus: 3
target_label: __tmp_hash
action: hashmod
- source_labels: [__tmp_hash]
regex: "0" # Shard 0 只抓取 hash 值为 0 的目标
action: keep三个分片分别配置
regex: "0"、regex: "1"、regex: "2",确保每个目标只被一个分片抓取。
10.3 Prometheus Agent 模式
Prometheus 2.32 引入了 Agent 模式(Agent Mode),专门用于边缘场景和远程写入架构。Agent 模式下 Prometheus 只负责抓取和转发,不存储数据,不支持本地查询。
prometheus --enable-feature=agent --config.file=prometheus-agent.yml# prometheus-agent.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: "edge-services"
static_configs:
- targets: ["service-a:8080", "service-b:8080"]
remote_write:
- url: "http://central-victoriametrics:8480/insert/0/prometheus/api/v1/write"Agent 模式非常适合以下场景:
- 边缘数据中心需要抓取本地服务指标并上报到中心
- Kubernetes 集群中的 Prometheus 只负责抓取,存储交给 VictoriaMetrics 或 Thanos Receive
- 需要降低 Prometheus 的资源消耗
十一、指标管道与预处理
在大规模环境中,Prometheus 抓取的原始指标通常需要经过预处理才能进入长期存储。
11.1 远程写入管道
graph LR
P1[Prometheus 1] -->|remote_write| KAFKA[Kafka]
P2[Prometheus 2] -->|remote_write| KAFKA
P3[Prometheus 3] -->|remote_write| KAFKA
KAFKA --> STREAM[流处理<br/>过滤/聚合/降采样]
STREAM --> VM[(VictoriaMetrics<br/>长期存储)]
STREAM --> ALERT[Alertmanager]
在 Prometheus 和最终存储之间插入一个流处理层(如 Kafka + 自定义消费者),可以实现:
- 指标过滤: 丢弃不需要的高基数指标,减少存储成本
- 标签重写: 统一不同团队使用的标签命名
- 降采样: 将 15 秒精度的数据降采样到 1 分钟或 5 分钟精度
- 多目标扇出: 同一份数据写入多个下游系统(长期存储、实时告警、数据湖)
11.2 VictoriaMetrics 的 vmagent
VictoriaMetrics 提供了 vmagent 组件,作为 Prometheus 抓取和远程写入的轻量替代品:
# vmagent 配置示例
-promscrape.config=/etc/vmagent/scrape.yml
-remoteWrite.url=http://vminsert:8480/insert/0/prometheus/api/v1/write
-remoteWrite.tmpDataPath=/var/lib/vmagent-remotewrite-data
-remoteWrite.maxDiskUsagePerURL=1GBvmagent 的关键优势是
-remoteWrite.tmpDataPath:当远程写入目标不可用时,vmagent
会将数据缓存到本地磁盘,恢复后自动重传。这解决了 Prometheus
原生远程写入在目标不可用时丢失数据的问题。
十二、Grafana Dashboard 设计原则
指标采集和存储只是手段,最终要通过仪表板(Dashboard)呈现给工程师。好的 Dashboard 设计应遵循以下原则。
12.1 分层 Dashboard
Level 0: 全局概览(Golden Signals Dashboard)
- 所有服务的错误率、延迟、流量一览
- 用于值班工程师快速定位异常服务
Level 1: 服务详情(Service Dashboard)
- 单个服务的 RED 指标详情
- 包含按接口、状态码、实例的分解
Level 2: 资源详情(Resource Dashboard)
- 节点级 USE 指标
- 数据库、缓存、消息队列的详细指标
Level 3: 调试面板(Debug Dashboard)
- 运行时指标:GC、线程池、连接池
- 临时创建,问题解决后归档
12.2 Dashboard 中的 PromQL 技巧
# 使用变量模板实现下钻
# 在 Grafana 中配置变量 $service,查询所有 service 标签值
label_values(http_requests_total, service)
# 错误率计算(处理分母为零的情况)
sum(rate(http_requests_total{status=~"5..", service="$service"}[5m]))
/
(sum(rate(http_requests_total{service="$service"}[5m])) > 0)
# 使用 absent() 检测指标缺失
absent(up{job="critical-service"})
# 使用 predict_linear() 预测磁盘耗尽时间
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 24*3600) < 0
# 含义:基于过去 6 小时的趋势,预测 24 小时后磁盘是否会耗尽
十三、指标与可观测性的三大支柱
指标只是可观测性(Observability)的三大支柱之一。日志(Logs)、指标(Metrics)、追踪(Traces)各自解决不同的问题,但它们之间的关联是建设可观测性平台的关键。
13.1 三大支柱的协同
| 支柱 | 解决的问题 | 数据特征 | 成本模型 |
|---|---|---|---|
| 指标 | “发生了什么?趋势如何?” | 数值型、高度聚合 | 与序列数成正比 |
| 日志 | “具体发生了什么?” | 文本型、离散事件 | 与日志量成正比 |
| 追踪 | “请求经过了哪些服务?” | 结构化、因果关系 | 与请求量 x 采样率成正比 |
典型的排障流程:
- 告警触发(指标):错误率超过 5%
- 定位服务(指标):通过 Dashboard
发现是订单服务的
/api/orders接口 - 查看追踪(追踪):找到一条慢请求的 Trace,发现 90% 的时间花在调用支付服务上
- 查看日志(日志):在支付服务的日志中找到具体的异常堆栈——银行网关超时
这三步跨越了三个系统。为了让这个流程顺畅,需要建立 Exemplar(范例)关联:
// 在 Histogram 观测中附加 Trace ID 作为 Exemplar
requestDuration.WithLabelValues(method, path).
(Exemplar(prometheus.Labels{"trace_id": traceID}, duration))Prometheus 从 2.26 版本开始支持 Exemplar。Grafana 在 Histogram 图表中可以显示 Exemplar 数据点,点击后可以直接跳转到对应的 Trace 详情页(通常是 Jaeger 或 Tempo)。
13.2 OpenTelemetry 的统一
OpenTelemetry(OTel)正在成为可观测性数据采集的事实标准。它提供了统一的 SDK 和 Collector,可以同时采集指标、日志和追踪数据:
# OpenTelemetry Collector 配置
receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
http:
endpoint: "0.0.0.0:4318"
prometheus:
config:
scrape_configs:
- job_name: "otel-collector"
static_configs:
- targets: ["localhost:8888"]
exporters:
prometheusremotewrite:
endpoint: "http://victoriametrics:8480/insert/0/prometheus/api/v1/write"
otlp/traces:
endpoint: "tempo:4317"
tls:
insecure: true
loki:
endpoint: "http://loki:3100/loki/api/v1/push"
service:
pipelines:
metrics:
receivers: [otlp, prometheus]
exporters: [prometheusremotewrite]
traces:
receivers: [otlp]
exporters: [otlp/traces]
logs:
receivers: [otlp]
exporters: [loki]十四、生产环境 Checklist
在将指标监控体系部署到生产环境之前,以下检查清单涵盖了关键的工程决策。
14.1 容量规划
14.2 高可用
14.3 基数控制
14.4 告警质量
14.5 Dashboard 规范
十五、常见反模式
在多年的生产实践中,以下反模式反复出现,值得特别警示。
15.1 “监控一切”心态
一些团队试图监控每一个可能的维度,生成大量从不被查看的指标。这不仅浪费存储资源,还增加了 Prometheus 的抓取和索引负担。
正确做法: 从 USE / RED 方法论出发,只采集对判断系统健康状态和排查问题有用的指标。如果一个指标在过去 90 天内从未出现在任何 Dashboard 或告警规则中,考虑删除它。
15.2 告警疲劳
当告警数量过多且大部分是噪音时,值班工程师会逐渐忽略告警,导致真正的故障被淹没。
正确做法: 追踪告警的信噪比。一个健康的告警系统,每个 critical 告警都应该触发人工介入;如果超过 50% 的 critical 告警是误报,说明告警规则需要优化。
15.3 Histogram 桶过多
为了追求精度而设置 30-50 个桶的 Histogram 会极大膨胀时间序列数。实际上,对于大多数业务场景,8-12 个桶已经足够满足 P50、P90、P99 的计算精度需求。
正确做法: 根据业务的延迟分布特征选择桶边界。例如,一个 P99 在 100ms 左右的 API,桶边界可以设为:
Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0}15.4 将 Prometheus 作为事件存储
Prometheus 不适合存储离散事件。以下用法是错误的:
// 错误:试图用指标记录每个订单事件
orderEvents.WithLabelValues(orderID, eventType, timestamp).Set(1)离散事件应该走日志或事件流系统(Kafka、CloudEvents)。指标只应记录聚合后的数值。
15.5 忽略 _total
后缀
Counter 类型的指标必须以 _total
结尾。这不仅是命名规范的要求,更重要的是 Prometheus
的内部处理会依赖这个后缀。OpenMetrics 格式要求 Counter
必须有 _total
后缀,缺少后缀会导致兼容性问题。
十六、工程案例:某视频平台的指标架构演进
16.1 阶段一:单实例 Prometheus(0-200 服务实例)
公司早期只有不到 200 个服务实例,一台 Prometheus 就能胜任。配置简单,15 秒抓取间隔,15 天数据保留,8 核 32GB 内存的机器绑绑有余。活跃序列约 50 万条。
16.2 阶段二:联邦 + 长期存储(200-2000 服务实例)
随着微服务化推进,实例数增长到 2000。单实例 Prometheus 的内存消耗达到 28GB,抓取延迟开始出现堆积。团队做了两件事:
- 按 Kubernetes Namespace 分片,部署 3 个 Prometheus 实例,各自抓取各自的 Namespace。
- 引入 VictoriaMetrics 作为长期存储,3 个 Prometheus 实例通过 remote_write 将数据写入。
- Grafana 的数据源从 Prometheus 切换到 VictoriaMetrics,获得跨分片查询能力。
16.3 阶段三:VictoriaMetrics 集群 + vmagent(2000-20000 服务实例)
当实例数突破 5000 后,Prometheus 分片的运维成本变得很高——每增加一个 Namespace 就可能需要调整分片策略。团队决定将 Prometheus 替换为 vmagent,只负责抓取和转发。
最终架构:
- 每个 Kubernetes 集群部署一个 vmagent 实例,负责抓取本集群的所有目标
- vmagent 通过 remote_write 将数据写入中心化的 VictoriaMetrics 集群
- VictoriaMetrics 集群:3 个 vminsert、6 个 vmstorage(3 副本 x 2 分片)、3 个 vmselect
- 活跃序列约 4000 万条,数据保留 90 天,总磁盘占用约 2TB(得益于 VictoriaMetrics 的高压缩率)
- Grafana 指向 vmselect,获得统一的全局查询视图
16.4 关键教训
- 不要过早引入复杂架构。 单实例 Prometheus 能用的时候不要上 Thanos。
- remote_write 的缓冲配置至关重要。 早期使用 Prometheus 原生 remote_write 时,VictoriaMetrics 短暂不可用导致 Prometheus 内存暴涨,最终通过 vmagent 的磁盘缓冲解决。
- 基数控制必须从第一天开始。 等到序列数失控再治理,代价是重构所有服务的指标代码。
- Recording Rules 是性能的关键。 将 Dashboard 中频繁使用的复杂查询转为 Recording Rules,Grafana 加载速度从 15 秒降到 2 秒。
十七、远程写入的可靠性与背压
17.1 远程写入协议
Prometheus 的远程写入(Remote Write)使用 HTTP POST 请求,数据以 Protocol Buffers 编码,Snappy 压缩。每个请求包含一批样本(Samples)。
remote_write:
- url: "http://vminsert:8480/insert/0/prometheus/api/v1/write"
queue_config:
capacity: 10000 # 内存队列容量(样本数)
max_shards: 200 # 最大并发发送分片数
min_shards: 1 # 最小并发发送分片数
max_samples_per_send: 5000 # 每次发送的最大样本数
batch_send_deadline: 5s # 批次发送的最大等待时间
min_backoff: 30ms # 重试最小退避时间
max_backoff: 5s # 重试最大退避时间
write_relabel_configs:
# 远程写入前过滤:只写入预聚合指标和关键原始指标
- source_labels: [__name__]
regex: "(api:.*|node_.*|up)"
action: keep17.2 背压与数据丢失
当远程写入目标响应变慢或不可用时,Prometheus 的内存队列会填满。队列满后,新的样本会被丢弃。这是一个有意的设计——Prometheus 优先保证自身的稳定性,而不是为了保证数据完整性而消耗无限内存。
监控远程写入的健康状态:
# 远程写入队列中的待发送样本数
prometheus_remote_storage_pending_samples
# 远程写入失败的样本数
rate(prometheus_remote_storage_failed_samples_total[5m])
# 远程写入被丢弃的样本数(队列满导致)
rate(prometheus_remote_storage_dropped_samples_total[5m])
# 远程写入的延迟
prometheus_remote_storage_queue_highest_sent_timestamp_seconds
- prometheus_remote_storage_queue_lowest_sent_timestamp_seconds
当
prometheus_remote_storage_dropped_samples_total
持续增长时,说明远程写入目标已经无法跟上 Prometheus
的产出速率。此时的应对策略:
- 增加远程写入目标的写入能力(扩容 vminsert)
- 通过
write_relabel_configs减少写入的指标量 - 使用 vmagent 替代 Prometheus 的原生 remote_write,利用磁盘缓冲避免丢失
十八、总结
指标监控架构的核心矛盾是维度灵活性与基数可控性之间的张力。维度数据模型赋予了我们强大的查询能力,但每多一个标签值就意味着更多的时间序列,更多的内存消耗和存储成本。
解决这个矛盾的关键不是技术上的——Prometheus、VictoriaMetrics、Thanos 都是成熟的工程方案——而是治理上的:建立指标评审制度,从第一天就控制基数增长;使用 USE、RED、Golden Signals 方法论指导该采集什么指标,而不是盲目地”监控一切”。
对于架构选型,简单是最大的美德。单实例 Prometheus 能覆盖的场景比大多数人想象的要广。当它确实不够用时,VictoriaMetrics 集群以更低的运维复杂度提供了水平扩展能力,Thanos 则在与 Prometheus 生态的兼容性上更胜一筹。选择哪个取决于你的团队对 Kubernetes 的运维能力和对 Prometheus 生态的依赖程度。
最后,指标只是可观测性三大支柱之一。它擅长回答”发生了什么”和”趋势如何”,但不擅长回答”为什么发生”——这是日志和追踪的职责。下一篇 分布式追踪 将讨论链路追踪的架构设计。
参考资料
- Prometheus 官方文档, “Data Model”, https://prometheus.io/docs/concepts/data_model/
- Prometheus 官方文档, “Metric and Label Naming”, https://prometheus.io/docs/practices/naming/
- Prometheus 官方文档, “Federation”, https://prometheus.io/docs/prometheus/latest/federation/
- Prometheus 官方文档, “Remote Write Tuning”, https://prometheus.io/docs/practices/remote_write/
- Thanos 官方文档, “Architecture Overview”, https://thanos.io/tip/thanos/design.md/
- VictoriaMetrics 官方文档, “Cluster Version”, https://docs.victoriametrics.com/cluster-victoriametrics/
- Brendan Gregg, “The USE Method”, https://www.brendangregg.com/usemethod.html
- Tom Wilkie, “The RED Method: Key Metrics for Microservices Architecture”, https://grafana.com/blog/2018/08/02/the-red-method-how-to-instrument-your-services/
- Google, “Site Reliability Engineering”, Chapter 6: Monitoring Distributed Systems, https://sre.google/sre-book/monitoring-distributed-systems/
- Pelkonen, T. et al., “Gorilla: A Fast, Scalable, In-Memory Time Series Database”, VLDB 2015
- Google, “Site Reliability Engineering Workbook”, Chapter 5: Alerting on SLOs, https://sre.google/workbook/alerting-on-slos/
- Prometheus 官方文档, “Storage”, https://prometheus.io/docs/prometheus/latest/storage/
- OpenTelemetry 官方文档, “OpenTelemetry Collector”, https://opentelemetry.io/docs/collector/
- Robust Perception Blog, “Cardinality is Key”, https://www.robustperception.io/cardinality-is-key/
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【系统架构设计百科】时序数据架构:监控与 IoT 的存储设计
时序数据的写入密集、查询模式固定——如何利用这些特征设计高效存储?本文深入 Gorilla 编码原理、降采样策略,对比 InfluxDB、Prometheus、TimescaleDB 的架构设计。
【系统架构设计百科】架构质量属性:不只是"高可用高性能"
需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。
【系统架构设计百科】告警策略:如何避免"狼来了"
大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。
【系统架构设计百科】复杂性管理:架构的核心战场
系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略