土法炼钢兴趣小组的算法知识备份

【系统架构设计百科】指标与监控架构:维度数据模型与基数爆炸

文章导航

分类入口
architecture
标签入口
#metrics#Prometheus#VictoriaMetrics#Thanos#cardinality#USE#RED

目录

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)记录请求链路不同,指标关注的是聚合后的数值趋势。

一条指标数据点由三部分组成:

这三者组合在一起,形成了 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 指标实际上会生成三组时间序列:

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: namespace

3.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”中提出的压缩算法。核心思路是利用时间序列的两个统计特性:

  1. 时间戳通常等间隔。 使用 Double-delta 编码:存储时间戳差值的差值。如果抓取间隔稳定为 15 秒,连续时间戳的差值为 15,差值的差值为 0,可以用 1 个 bit 表示。

  2. 相邻数据点的值通常接近。 使用 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"
}

策略四:指标审计流程。 建立制度化的指标评审机制。任何新增的指标或标签在上线前必须经过评审,评审要点包括:

  1. 每个标签的值空间是否有界?上界是多少?
  2. 这个标签是否会随业务增长而无限膨胀?
  3. 这个指标的总序列数预估是多少?
  4. 是否有更低基数的替代方案能满足同样的监控需求?

5.4 工程案例:某金融科技公司的基数治理

某金融科技公司运营着约 2000 个微服务实例,Prometheus 集群管理的活跃时间序列约 800 万条。2023 年 Q3,序列数在两周内从 800 万飙升到 3500 万,Prometheus 内存从 32GB 涨到 85GB,触发了 OOM。

排查过程。 通过 TSDB Status API 发现,payment_transaction_duration_seconds_bucket 这一个指标贡献了 2400 万条序列。进一步分析发现:

根因是 merchant_id 标签——一个高基数标识符被直接暴露在 Histogram 指标中。

治理方案:

  1. 移除 merchant_id 标签,将商户维度的分析下沉到日志系统。序列数从 2400 万降到 20,400 条(17 x 8 x 25 x 5 x 6)。
  2. 减少桶数量从 15 个到 8 个(去掉了几个不必要的中间桶),序列数进一步降到 12,000 条。
  3. 建立指标评审制度:所有新指标需要在 PR 中计算预估基数,超过 10,000 条序列的需要架构评审。
  4. 部署基数监控告警:当 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"}

关键原则:

  1. 标签名使用蛇形命名法,全小写
  2. 不使用缩写,除非是广泛认可的缩写(如 httptcp
  3. 标签值应该是有意义的字符串,不是编码值
  4. 避免将同一信息同时放在指标名称和标签中

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"

联邦的限制:

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-downsampling

7.3 VictoriaMetrics 架构

VictoriaMetrics 是用 Go 编写的高性能时序数据库,兼容 Prometheus 的远程写入(Remote Write)协议和 PromQL(通过 MetricsQL 扩展)。它的集群版本采用了存储与计算分离的架构:

组件 职责
vminsert 接收远程写入数据,分片后写入 vmstorage
vmstorage 存储时序数据,支持多副本
vmselect 接收查询请求,从多个 vmstorage 读取数据并合并

VictoriaMetrics 的关键优势:

  1. 更高的压缩率。 相比 Prometheus TSDB,VictoriaMetrics 的压缩率通常高 2-7 倍,意味着同样的数据占用更少的磁盘空间。
  2. 更低的内存消耗。 通过更激进的内存管理策略,处理相同基数的序列需要更少的内存。
  3. 原生集群支持。 无需 Thanos 这样的外部组件即可实现水平扩展。
  4. 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: 30

7.4 三种方案的对比

特性 Prometheus 联邦 Thanos VictoriaMetrics 集群
长期存储 不支持(受本地磁盘限制) 对象存储(S3、GCS、MinIO) 本地磁盘或对象存储
全局查询 仅限预聚合指标 完整 PromQL 完整 MetricsQL(PromQL 超集)
高可用 需手动部署双副本 原生支持去重 原生支持多副本
降采样 不支持 支持(5m、1h) 不需要(查询时自动优化)
运维复杂度 高(6+ 组件) 中(3 组件)
压缩率 基线 与 Prometheus 相当 高于 Prometheus 2-7 倍
社区生态 Prometheus 原生 CNCF 毕业项目 活跃开源,商业公司支持
适用规模 千级实例 万级实例 万级实例
数据一致性 最终一致 最终一致 最终一致
学习曲线

选型建议:


八、USE / RED / Golden Signals 方法论

知道了怎么采集和存储指标之后,下一个问题是:应该采集哪些指标?三种经典方法论给出了不同的答案。

8.1 USE 方法

USE(Utilization, Saturation, Errors)方法由 Brendan Gregg 提出,专门用于分析基础设施资源(Infrastructure Resources)的性能问题。

对每种资源类型应用 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)。

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):

Golden Signals 本质上是 USE 和 RED 的融合——它既包含了面向用户的信号(延迟、流量、错误),也包含了面向资源的信号(饱和度)。

8.4 三种方法论的选择指南

方法论 适用对象 关注视角 典型场景
USE 基础设施资源 资源内部 排查 CPU、内存、磁盘、网络瓶颈
RED 请求驱动的服务 用户体验 监控 API 网关、Web 服务、微服务
Golden Signals 通用 用户体验 + 资源 SRE 值班大盘、SLO 定义

实际工程中的建议:

  1. 对于微服务的应用层监控,使用 RED 方法。每个服务暴露 Rate、Errors、Duration 三组指标,这是 Grafana Dashboard 的核心内容。
  2. 对于 Kubernetes 节点和中间件(数据库、消息队列、缓存)的监控,使用 USE 方法。
  3. 对于 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 模式非常适合以下场景:


十一、指标管道与预处理

在大规模环境中,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 + 自定义消费者),可以实现:

  1. 指标过滤: 丢弃不需要的高基数指标,减少存储成本
  2. 标签重写: 统一不同团队使用的标签命名
  3. 降采样: 将 15 秒精度的数据降采样到 1 分钟或 5 分钟精度
  4. 多目标扇出: 同一份数据写入多个下游系统(长期存储、实时告警、数据湖)

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=1GB

vmagent 的关键优势是 -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 采样率成正比

典型的排障流程:

  1. 告警触发(指标):错误率超过 5%
  2. 定位服务(指标):通过 Dashboard 发现是订单服务的 /api/orders 接口
  3. 查看追踪(追踪):找到一条慢请求的 Trace,发现 90% 的时间花在调用支付服务上
  4. 查看日志(日志):在支付服务的日志中找到具体的异常堆栈——银行网关超时

这三步跨越了三个系统。为了让这个流程顺畅,需要建立 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,抓取延迟开始出现堆积。团队做了两件事:

  1. 按 Kubernetes Namespace 分片,部署 3 个 Prometheus 实例,各自抓取各自的 Namespace。
  2. 引入 VictoriaMetrics 作为长期存储,3 个 Prometheus 实例通过 remote_write 将数据写入。
  3. Grafana 的数据源从 Prometheus 切换到 VictoriaMetrics,获得跨分片查询能力。

16.3 阶段三:VictoriaMetrics 集群 + vmagent(2000-20000 服务实例)

当实例数突破 5000 后,Prometheus 分片的运维成本变得很高——每增加一个 Namespace 就可能需要调整分片策略。团队决定将 Prometheus 替换为 vmagent,只负责抓取和转发。

最终架构:

16.4 关键教训

  1. 不要过早引入复杂架构。 单实例 Prometheus 能用的时候不要上 Thanos。
  2. remote_write 的缓冲配置至关重要。 早期使用 Prometheus 原生 remote_write 时,VictoriaMetrics 短暂不可用导致 Prometheus 内存暴涨,最终通过 vmagent 的磁盘缓冲解决。
  3. 基数控制必须从第一天开始。 等到序列数失控再治理,代价是重构所有服务的指标代码。
  4. 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: keep

17.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 的产出速率。此时的应对策略:

  1. 增加远程写入目标的写入能力(扩容 vminsert)
  2. 通过 write_relabel_configs 减少写入的指标量
  3. 使用 vmagent 替代 Prometheus 的原生 remote_write,利用磁盘缓冲避免丢失

十八、总结

指标监控架构的核心矛盾是维度灵活性与基数可控性之间的张力。维度数据模型赋予了我们强大的查询能力,但每多一个标签值就意味着更多的时间序列,更多的内存消耗和存储成本。

解决这个矛盾的关键不是技术上的——Prometheus、VictoriaMetrics、Thanos 都是成熟的工程方案——而是治理上的:建立指标评审制度,从第一天就控制基数增长;使用 USE、RED、Golden Signals 方法论指导该采集什么指标,而不是盲目地”监控一切”。

对于架构选型,简单是最大的美德。单实例 Prometheus 能覆盖的场景比大多数人想象的要广。当它确实不够用时,VictoriaMetrics 集群以更低的运维复杂度提供了水平扩展能力,Thanos 则在与 Prometheus 生态的兼容性上更胜一筹。选择哪个取决于你的团队对 Kubernetes 的运维能力和对 Prometheus 生态的依赖程度。

最后,指标只是可观测性三大支柱之一。它擅长回答”发生了什么”和”趋势如何”,但不擅长回答”为什么发生”——这是日志和追踪的职责。下一篇 分布式追踪 将讨论链路追踪的架构设计。


参考资料

  1. Prometheus 官方文档, “Data Model”, https://prometheus.io/docs/concepts/data_model/
  2. Prometheus 官方文档, “Metric and Label Naming”, https://prometheus.io/docs/practices/naming/
  3. Prometheus 官方文档, “Federation”, https://prometheus.io/docs/prometheus/latest/federation/
  4. Prometheus 官方文档, “Remote Write Tuning”, https://prometheus.io/docs/practices/remote_write/
  5. Thanos 官方文档, “Architecture Overview”, https://thanos.io/tip/thanos/design.md/
  6. VictoriaMetrics 官方文档, “Cluster Version”, https://docs.victoriametrics.com/cluster-victoriametrics/
  7. Brendan Gregg, “The USE Method”, https://www.brendangregg.com/usemethod.html
  8. 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/
  9. Google, “Site Reliability Engineering”, Chapter 6: Monitoring Distributed Systems, https://sre.google/sre-book/monitoring-distributed-systems/
  10. Pelkonen, T. et al., “Gorilla: A Fast, Scalable, In-Memory Time Series Database”, VLDB 2015
  11. Google, “Site Reliability Engineering Workbook”, Chapter 5: Alerting on SLOs, https://sre.google/workbook/alerting-on-slos/
  12. Prometheus 官方文档, “Storage”, https://prometheus.io/docs/prometheus/latest/storage/
  13. OpenTelemetry 官方文档, “OpenTelemetry Collector”, https://opentelemetry.io/docs/collector/
  14. Robust Perception Blog, “Cardinality is Key”, https://www.robustperception.io/cardinality-is-key/

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计百科】架构质量属性:不只是"高可用高性能"

需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。

2026-04-13 · architecture

【系统架构设计百科】告警策略:如何避免"狼来了"

大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。

2026-04-13 · architecture

【系统架构设计百科】复杂性管理:架构的核心战场

系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略


By .