第12篇 Prometheus 高可用与长期存储

前面篇章都是基于单机 Prometheus ,已能满足中小企业的基本需求。但针对一些重要业务系统,或把监控数据作为重要数字资产的企业,单机 Prometheus 将存在重大风险。

本篇目标:讲清单机 Prometheus 的可靠性边界在哪,如何用两套 Prometheus + Alertmanager 集群把采集与告警做成高可用,以及如何用 Thanos / VictoriaMetrics 把数据推进对象存储,支撑一年以上的回查。

一、单机 Prometheus 的边界

1、三个真实故障场景

单机部署的问题不是「会不会出事」,而是出事时恰好失去了一切观测能力:

  • 场景一:磁盘写满。 TSDB 在磁盘达到阈值后停止写入,/api/v1/query 还能查旧数据,但新样本全部丢弃,且没有显式告警,最直接的信号反而是 Grafana 上所有曲线停止更新。而「Grafana 曲线停更」也依赖 Grafana 可用,故障是自指的。
  • 场景二:进程重启。 TSDB 每 2 小时把内存里的 head block 落成一个持久块,之间的数据靠 WAL 兜底。正常重启 WAL 能恢复,但机器断电、OOM Kill 这类非优雅退出会丢掉最近几分钟的样本,Grafana 上表现为一段缺口。
  • 场景三:单点宕机。 采集停、告警哑、面板白屏,三件事同时发生。如果 Prometheus 和被监控的应用在同一批机器上,机器级故障时告警链路和故障一起消失。

2、分清两个问题:可用性 与 保留期

「高可用」和「长期存储」经常被混在一句话里说,但它们是两个独立的问题,解法也不同:

问题 关心什么 失败的样子 解决方向
高可用 数据不中断、告警不哑、面板不断 宕机期间采集与告警全断 双副本采集 + 告警去重 + 查询聚合
长期存储 存得久、查得动、成本可控 保留期一到数据被淘汰;回查大范围直接 OOM 对象存储 + 降采样 + 全局查询

⚠️ 注意:高可用不等于备份。 两台一模一样的 Prometheus,配置错误同时写坏数据、或误删同一批块,结果和单点没有区别。可用性解决的是「服务在」,备份解决的是「数据在」。

3、本地 TSDB 的三个天花板

1)保留期

默认只保留 15 天,老版本通过启动参数调整:

bash 复制代码
# 两个参数同时给时,先到者生效,即哪个条件先触发,就先删旧数据
--storage.tsdb.retention.time=30d
--storage.tsdb.retention.size=45GB
# 注意:size 的最小值是 512MB,单位按样本块计算
# 这是**防磁盘打爆的兜底保护**:当指标基数暴涨(series cardinality 突增),哪怕数据还没到 30 天,只要 TSDB 块总量超过 45GB,就会清理老数据

但这两个命令行参数在新版 Prometheus 已经标记废弃 ,推荐迁移到 prometheus.yml 配置内 storage.tsdb 字段配置。

配置示例:

yaml 复制代码
# prometheus.yml
storage:
  tsdb:
    retention:
      time: 30d
      size: 45GB

通过 prometheus --help 命令查看是否已废弃,如下所示,表示已废弃。

bash 复制代码
$ prometheus --help
usage: prometheus.exe [<flags>]
The Prometheus monitoring server

Flags:
  -h, --[no-]help                Show context-sensitive help (also try
                                 --help-long and --help-man).
      --storage.tsdb.path="data/"
                                 Base path for metrics storage. Use with server
                                 mode only.
      --storage.tsdb.retention.time=STORAGE.TSDB.RETENTION.TIME
                                 [DEPRECATED] How long to retain samples
                                 in storage. If neither this flag nor
                                 "storage.tsdb.retention.size" is set,
                                 the retention time defaults to 15d.
                                 Units Supported: y, w, d, h, m, s, ms.
                                 This flag has been deprecated, use the
                                 storage.tsdb.retention.time field in the config
                                 file instead. Use with server mode only.
      --storage.tsdb.retention.size=STORAGE.TSDB.RETENTION.SIZE
                                 [DEPRECATED] Maximum number of bytes that can
                                 be stored for blocks. A unit is required,
                                 supported units: B, KB, MB, GB, TB, PB, EB. Ex:
                                 "512MB". Based on powers-of-2, so 1KB is 1024B.
                                 This flag has been deprecated, use the
                                 storage.tsdb.retention.size field in the config
                                 file instead. Use with server mode only.

2)查询成本

range query 的成本随时间跨度线性增长,回查 30 天的 rate() 在序列数过万后会直接打爆内存。本地 TSDB 没有「降采样」概念,查一年的曲线和查一小时的曲线,扫描的是同一种精度的块。

3)磁盘膨胀

压缩后每个样本约 1.5~2 字节,账很好算:

复制代码
磁盘/天 ≈ 序列数 × (86400 / 抓取间隔秒) × 2 字节

按本系列验证环境的规模算一笔账:

来源 序列数(约) 抓取间隔 样本/秒 磁盘/天
5 台主机(node_exporter) 4,000 15s 267 46 MB
3 个 Spring Boot 应用 4,500 15s 300 52 MB
直方图开启后(×20 基数,第4篇踩坑) 90,000 15s 6,000 1 GB

没开直方图时 15 天也就 1.5G,本地盘完全够;直方图一开,账立刻变成另一个量级。这说明很多「需要长期存储」的场景,先做减法比先上架构更有效。

二、高可用:两套 Prometheus 是官方答案

1、为什么不是集群

Prometheus 的设计哲学是单机自治:每个实例独立抓取、独立存储、独立评估规则,Server 本身没有内置集群协议。官方对高可用的答案是朴素的两句话:跑两个功能完全相同的 Prometheus,抓同一批 target。

两个实例不做任何主从协调,没有 leader 选举,谁也不备份谁。代价是每个 target 被抓两次、每份数据存两份;换来的是任何一个实例宕机,采集、规则评估、告警链路都不中断。

两个实例共享同一份 targets 文件,配置天然一致:

yaml 复制代码
# 两套 Prometheus 共用同一个 file_sd 目录(挂载同一个文件)
file_sd_configs:
  - files:
      - /etc/prometheus/targets/*.yml
    refresh_interval: 30s

2、副本标签:不加会出乱序

两个实例抓同一个 target,产生的序列 label 完全相同。如果直接把两份数据写进同一个下游(remote_write 或对象存储),时间戳相同、值可能有微差(抓取时刻不同),下游会把后到的判定为乱序样本直接拒绝。

解法是给两套实例打上不同的 external_labels:

yaml 复制代码
# prometheus-a.yml ------ 与 prometheus-b.yml 唯一的区别就是 replica 值
global:
  scrape_interval: 15s
  external_labels:
    replica: A        # B 实例写 replica: B

⚠️ 注意:

  • replica 标签必须在 external_labels 里打,不能在 relabel_configs 里打。 external_labels 会附加到该实例产出的所有样本与告警上;而 relabel 打的标签只影响抓取目标,WAL、告警、remote_write 的元数据不会带上它,去重时会漏。
  • external_labels 的作用范围被严格限定在与外部系统通信时 。其官方定义明确指出,这些标签仅添加在发往联合(federation)、远程存储(remote storage)、Alertmanager 的数据上,因此,在 prometheus ui 上查不到这些标签,验证也只能通过远程存储或 alertmanager 接收到的告警。

3、Alertmanager 集群:告警去重

两套 Prometheus 同时评估同一批规则,同一条告警必然触发两次。如果 Alertmanager 也是单点,告警链路依然脆弱;正确做法是部署 Alertmanager 集群(gossip 协议),并让两套 Prometheus 把告警发给所有节点:

yaml 复制代码
# 两套 Prometheus 的 alerting 配置完全相同
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      - alertmanager-1:9093
      - alertmanager-2:9093
      - alertmanager-3:9093

​

bash 复制代码
# Alertmanager 节点间互相发现
docker run -d --name=alertmanager-1 \
  --network my-bridge \
  -v /opt/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml \
  prom/alertmanager:v0.27.0 \
  --config.file=/etc/alertmanager/alertmanager.yml \
  --cluster.listen-address=0.0.0.0:9093 \
  --cluster.peer=alertmanager-2:9093 \
  --cluster.peer=alertmanager-3:9093

集群内的去重不需要任何额外配置:Alertmanager 按 group_by识别同一条告警,两个 Prometheus 发来的重复通知只会被投递一次。注意重复告警在收到第一个通知后 5 分钟内再次到达,默认不会再发,正好覆盖了两套 Prometheus 抓取错峰带来的时间差。

验证集群状态:

bash 复制代码
$ amtool cluster show --alertmanager.url=http://localhost:9093

$ docker exec -it alertmanager amtool cluster show --alertmanager.url=http://localhost:9093
Cluster Status:  ready
Node Name:       01M39TMA1Y0XFV9NTSACRZZPK6

4、Grafana 怎么接两个数据源

双副本之后,Grafana 面板面对的是两个 Prometheus。三个方案:

方案 做法 优点 缺点
手动切数据源 两种方式:1、配两个 datasource,面板里切换2、只配置一个 datasource,不可用时修改 datasource 配置,切换到另一个 prometheus 上 零成本 排查时要人肉确认看的哪个副本,容易看漏
Grafana 聚合 混合数据源查询 无需中间件 每个面板都要改查询,聚合语义易出错
统一查询层 Thanos Query 作为唯一 datasource 面板零改动,自动去重 引入一套新组件

三、长期存储:先把口子留好

1、remote_write:成本最低的保留手段

如果需求只是「数据多留一年,偶尔查一下」,不必急着上 Thanos。Prometheus 原生的 remote_write 可以把样本实时转发给任意外部存储,本地 TSDB 只保留近期数据:

yaml 复制代码
remote_write:
  - url: http://victoria-metrics:8428/api/v1/write
    queue_config:
      max_shards: 30
      capacity: 10000
      max_samples_per_send: 2000
      batch_send_deadline: 5s
    metadata_config:
      send: true
      send_interval: 1m

三个 queue 参数的含义:

  • capacity 是内存队列长度
  • max_samples_per_send 控制单批大小
  • batch_send_deadline 保证低流量时样本也不会无限攒着

远端不可达时队列会在内存里堆积,max_shards 限制了最多并行发多少。如果远端宕机超过几小时,旧的未发送样本会被丢弃并在日志里报错。

⚠️ 注意:remote_write 不是备份。 它只转发新样本,Prometheus 本地已有的历史不会回补;而且目标存储挂掉期间的数据同样有丢失窗口。只能把它当「延长保留期」的手段,不能当灾备方案。

2、先做减法:降基数与分层抓取

前文的算账表已经说明,存储压力的大头往往是基数而不是天数。因此,在引入任何存储组件之前,可先通过以下两种方式减轻存储压力:

1)分层抓取间隔

前面文章里统一用了 15s,但那是为告警灵敏度服务的。如果部分场景只做趋势分析,可以拆出独立的 job 用 60s 抓取,样本量直接降 75%:

yaml 复制代码
# 同一个 exporter,两个 job 两档间隔
- job_name: "node-detail"        # 告警用,15s
  scrape_interval: 15s
  file_sd_configs: [{files: ["/etc/prometheus/targets/node.yml"]}]
- job_name: "node-archive"       # 长期趋势用,60s
  scrape_interval: 60s
  file_sd_configs: [{files: ["/etc/prometheus/targets/node.yml"]}]

2)丢弃确定不看的序列

用 metric_relabel_configs 在采集末端裁剪,它发生在 relabel_configs 之后、样本入库之前:

yaml 复制代码
metric_relabel_configs:
  # node_exporter 的 Go runtime 指标对业务无用,直接丢
  - source_labels: [__name__]
    regex: "go_(gc|memstats|threads|info)_.*"
    action: drop
  # 只保留 2xx/5xx 状态码维度的桶,1xx/3xx 不看
  - source_labels: [status]
    regex: "1..|3.."
    action: drop

⚠️ 注意:drop 是不可逆的,宁可保守。 删掉一条序列,历史数据一起消失。规则上线前先在验证环境跑一周,确认没有面板和告警引用这些序列再全量铺开。

四、方案对比:Thanos、VictoriaMetrics、Mimir

本章只作扩展介绍,没有经过实际验证。

当「remote_write + 本地减法」不再够用,如需要全局查询、需要跨多套 Prometheus 聚合、需要自动降采样,就需在以下三套主流方案里选择:

对比维度 Thanos VictoriaMetrics Mimir
出身 Prometheus 官方生态(Improbable 发起,CNCF) 独立项目(Percona 系团队) Grafana Labs(Cortex 演进)
接入方式 Sidecar 挂在 Prometheus 旁边,不改抓取 通常用 vmagent 替换抓取,或 Prometheus remote_write Prometheus remote_write
存储 对象存储(S3/MinIO/OSS) 自有存储(本地盘或共享盘) 对象存储
查询语言 PromQL(原样) MetricsQL(PromQL 超集,兼容为主) PromQL
降采样 Compactor 自动 5m/1h 两级 内置(查询时自动降精度) 内置(blocks 分层)
组件数量 5 个(sidecar/query/store/compactor/ruler) 单机版 1 个,集群版 3 类 3 类组件
运维成本 中(对象存储 + 多组件) 低 中高(K8s 友好,裸机部署较繁)
适合谁 已有多套 Prometheus,想统一查询与归档 中小规模,想要省心省资源 大规模、多租户、Kubernetes 环境

用三个问题定方案:

  • 能不能改 Prometheus 的抓取链路? 不能 → Thanos sidecar,它对 Prometheus 零侵入,只要求 2 小时块机制;能 → VictoriaMetrics + vmagent 是更省资源的路线。
  • 有没有对象存储可用? 有(云 OSS 或自建 MinIO) → Thanos / Mimir 顺理成章;只有本地盘 → VictoriaMetrics 单机版是务实之选。
  • 查询方是不是只认 PromQL? 第11篇的所有报告 PromQL 都基于原版语义,语法方言的细微差别会让报告模板失效。严格兼容 → Thanos / Mimir。

「remote_write + 本地减法」已能满足绝大部分场景的应用,因此,本文不再扩展介绍以上三种方式。后续若有实际应用场景,再专门写博文分析。

五、备份与兜底

remote_write 方式能够解决长期存储问题,但还可以再上一道保险:本地快照。

Prometheus 自带快照 API,可以对本地 TSDB 做一致性快照:

复制代码
# 需要启动参数 --web.enable-admin-api
curl -XPOST http://prometheus-a:9090/api/v1/admin/tsdb/snapshot

# 返回快照目录,rsync 到备份机即可
# {"status":"success","data":{"name":"20260924T191200Z-xxxx"}}

七、小结

本篇核心要点:

  • 单机 Prometheus 的边界是结构性的:TSDB 默认 15 天、range query 成本随跨度线性增长、进程即单点,加参数只能缓解,不能根治;
  • 高可用的官方答案就是双副本:两个实例用 external_labels 区分(放 relabel 里会漏)、共享同一份 file_sd targets、规则各自评估,靠 Alertmanager 集群的 gossip 与 group_by 完成告警去重;
  • 长期存储先做减法再做加法:分层抓取间隔与 metric_relabel 裁剪能救回大半块磁盘,remote_write 是成本最低的保留期延长手段,但都不是备份;
相关推荐
飞鸟真人10 小时前
应用性能监测(APM)之 (五)Prometheus与OpenMetrics协议
prometheus·apm·open telemetry·openmetrics
Cheney Pan11 小时前
第13篇 监控安全与权限治理
安全·prometheus
蓝胖的四次元口袋2 天前
Prometheus+Grafana知识梳理(2)
grafana·prometheus
Cheney Pan3 天前
第8篇 关键指标分析方法:RED/USE 与黄金信号
prometheus
Cheney Pan4 天前
第7篇 业务数据监控:SQL Exporter 与自定义业务指标
prometheus·exporter
Cheney Pan4 天前
第4篇 Spring Boot 应用监控:Micrometer 与 JVM 指标
jvm·spring boot·后端·prometheus
飞鸟真人4 天前
应用性能监测(APM)之 (四)Prometheus与Metrics集群存储
prometheus
Cheney Pan4 天前
第09篇 告警体系设计:Alertmanager 路由与告警治理
prometheus
蓝胖的四次元口袋5 天前
Prometheus+Grafana知识梳理(1)
grafana·prometheus