跳到主要内容

长期存储与高可用

前七篇解决的是「一台 Prometheus 把监控跑起来」的问题。这篇回答收尾的三个问题:数据存多久、Prometheus 挂了怎么办、规模变大了怎么拆。顺带把全系列最容易踩爆的雷——高基数——单独拎出来讲透。

环境说明

文中示例基于 Prometheus 2.5x / 3.x,远程存储以 VictoriaMetrics 集群版为例。配置片段为单机部署形态,Kubernetes 环境下请换算成 Operator 的 CRD 写法(见 prometheus-operator 一篇)。

本地存储的边界​

Prometheus 的 TSDB 把数据写本地磁盘:先写 WAL(write-ahead log)保证崩溃不丢,内存里攒满 2 小时窗口就落成一个 block 目录,后台再做压缩(compaction)把多个 block 合并、把过期数据清掉。这个设计面向的是「最近数据的快速读写」,不是归档。

保留策略两个参数:

prometheus \
--storage.tsdb.path=/data/prometheus \
--storage.tsdb.retention.time=15d \
--storage.tsdb.retention.size=40GB
  • --storage.tsdb.retention.time:默认 15 天,超龄数据按 block 粒度整块删除。
  • --storage.tsdb.retention.size:数据总量超了就删最老的 block,Prometheus 2.5x 和 3.x 都支持,行为一致。

两个一起配时谁先触发谁生效。size 上限建议留 20% 余量,别把磁盘顶满——WAL 和压缩过程都要额外空间。

单机 Prometheus 的定位要说清楚:它是监控数据的缓存层,负责最近几周的快速查询和告警评估,不是长期存储。磁盘一挂数据全没,保留期之外的历史一概查不到。需要跨年趋势、容量规划、合规留存,就得往下节的远程存储走。

磁盘用量估算

粗算公式:每秒抓取样本数 × 每样本约 1-2 字节 × 保留秒数。10 万 samples/s、保留 30 天,大约 260-520 GB(压缩后实际更小)。最准的办法是跑几天后看磁盘占用,再线性外推到目标保留期。

远程存储 remote_write​

机制一句话:Prometheus 抓取样本写入本地 TSDB 的同时,从 WAL 实时把样本转发一份到远程端点。远程端点收到后自己存、自己提供查询。本地 TSDB 照旧工作,远程只是「多抄一份」——所以本地重启后 remote_write 会从 WAL 断点续传,默认最多补 2 小时的数据。

配置就这么几行:

remote_write:
- url: http://vminsert:8480/insert/0/prometheus/api/v1/write
queue_config:
capacity: 10000
max_shards: 30
batch_send_deadline: 5s

选型对比​

三家主流方案的现状(2026 年视角):

方案架构思路运维成本适用规模
VictoriaMetrics单二进制起步,集群版拆 vminsert / vmselect / vmstorage 三个角色最低,组件少、配置少中小团队到千万级序列都压得住
Mimir微服务全家桶(distributor、ingester、store-gateway、compactor 等),底座是对象存储最高,组件多、要懂对象存储大规模多租户监控平台
Thanossidecar 贴在现有 Prometheus 旁,数据上传对象存储,querier 出全局视图中等,组件不少但边界清晰已有多个 Prometheus 想统一查询

各给一句「什么时候选它」:

  • VictoriaMetrics:想今晚就把长期存储跑起来、不想研究一堆新概念,选它。单机版一个二进制就能扛住大多数团队的量,压缩率还比 Prometheus 本地好。
  • Mimir:公司要做内部监控平台、按团队或业务线分租户、已有 S3 / MinIO 对象存储,选它。和 Grafana 生态是原生搭配。
  • Thanos:已经有好几个 Prometheus 在跑、不想动现有部署,只想加全局查询层和对象存储归档,选它。sidecar 模式对存量最友好。
三家都能做的事

remote_write 接收、PromQL 查询、Grafana 数据源、多副本去重,三家全都支持。差异在运维复杂度和扩展上限,不在功能有无。选型先想「谁来维护」,再想「要扛多少量」。

高可用​

单机 Prometheus 是单点:它挂了,告警停了,仪表盘白了。标准姿势是双副本 + 外部去重:

        ┌──→ prometheus-a ──┐
targets ┤ ├─→ remote_write ─→ 长期存储(自带副本)
└──→ prometheus-b ──┘
  • 两个 Prometheus 用同一份配置抓同一批目标,各自独立写数据。它们之间不通信、无状态、互不感知。
  • 查询层(VictoriaMetrics / Mimir / Thanos Querier)负责去重:两份数据靠 replica 标签区分,查询时按它去重合并成一份。去重是查询侧的事,Prometheus 自己不管。
  • 告警两边都会触发,靠 Alertmanager 集群去重:Alertmanager 实例之间用 gossip 协议同步通知状态,同一条告警只发一次。

HA 的要点就这么多:Prometheus 层做冗余,Alertmanager 层做集群,去重交给查询层。别试图给 Prometheus 加「主备切换」——双活就是它的 HA 形态。

联邦与分片​

联邦(federation)​

层级抓取:下级 Prometheus 抓各自的机器,上级 Prometheus 抓下级的 /federate 端点,把汇总指标抽上来:

- job_name: federate
honor_labels: true
metrics_path: /federate
params:
match[]:
# 只抓预聚合过的指标(对应 recording rules 的命名约定)
- '{__name__=~"job:.*:rate5m"}'
static_configs:
- targets:
- prometheus-dc1:9090
- prometheus-dc2:9090

适用场景:机房或区域分层,上级只要少量聚合后的指标做全局大盘。

局限很硬:联邦抓什么就复制什么,match[] 写得宽一点,等于把下级全部时序复制一份到上级,上级直接基数爆炸。所以联邦只配拉 recording rules 预聚合过的指标,别拿它当数据同步用。

纵向分片​

更常用的拆法是按 job 拆:网络设备一个 Prometheus、node 指标一个、业务应用一个。每个 Prometheus 管一类目标,配置独立、容量独立规划,挂了一个不影响别的。查询侧再拿统一查询层(VM / Thanos / Mimir)把数据拼回全局视图。

先分片,再考虑联邦

中小规模直接纵向分片加统一查询层就够了。联邦留给真正有多机房层级结构、且上级只需要汇总数据的场景。

基数治理(重点)​

高基数是 Prometheus 的头号杀手,没有之一。每一条不同的 label 组合就是一条独立时序,时序数量直接决定内存占用和查询压力。

典型事故长这样:有人给 http_requests_total 加了 user_id 标签,或者把原始 URL 塞进了 path 标签。user_id 有几十万个取值,一条指标瞬间裂变成几十万条时序,head block 内存膨胀,然后 OOM,然后告警全停。原始 URL 更狠——带参数的 URL 每个都不一样,取值无界,等于亲手造了一台时序印钞机。

统计当前基数​

先看哪些指标贡献的时序最多:

topk(10, count by (__name__)({__name__=~".+"}))

按 job 看分布:

topk(10, count by (job)({__name__=~".+"}))

不想记语法的话,Web UI 的 Status → TSDB Status 页面有现成的 Head Stats 和 Top 10 列表,点几下就能看。

怎么看结果

单个指标超过 10 万条时序就该警惕了。正常指标的时序数等于标签组合数,比如 node_cpu_seconds_total 大约是机器数 × CPU 核数 × mode 数,量级心里要有数;超出直觉一个数量级的,基本都是标签取值出了问题。

drop 高基数指标​

找到元凶后在采集端就丢掉,别让它们进 TSDB。用 metric_relabel_configs(relabeling 的完整语法见[指标采集与 Exporter](/docs/observability/prometheus/指标采集与 Exporter)一篇)。

整个指标不要了:

- job_name: app
static_configs:
- targets: [app:8080]
metric_relabel_configs:
- source_labels: [__name__]
regex: 'http_requests_total'
action: drop

只剪掉高基数标签、保留指标本身:

- job_name: app
static_configs:
- targets: [app:8080]
metric_relabel_configs:
# 把 user_id 标签从所有指标上剪掉
- regex: 'user_id'
action: labeldrop

labeldrop 是止血首选:指标还在,只是把无界标签剪掉,时序数立刻塌回正常量级。改完重载配置,新抓的数据就干净了;老数据还在磁盘上,等保留期自然过期。

Recording rules 预计算​

另一个降压力的手段:把仪表盘上反复算的重查询预计算成新指标,查询时直接取结果。完整例子如下。

规则文件 rules/app.rules.yml:

groups:
- name: app-aggregates
interval: 30s
rules:
# 按 job 维度预聚合 QPS
- record: job:http_requests:rate5m
expr: sum by (job) (rate(http_requests_total[5m]))

# 按 job 和状态码预聚合
- record: job_code:http_requests:rate5m
expr: sum by (job, code) (rate(http_requests_total[5m]))

# P99 延迟预聚合
- record: job:http_request_duration:p99
expr: histogram_quantile(0.99, sum by (job, le) (rate(http_request_duration_seconds_bucket[5m])))

prometheus.yml 里挂上:

rule_files:
- rules/*.rules.yml

使用:Grafana 面板里原来的重查询直接换成预聚合指标:

job:http_requests:rate5m

原来每次都要扫几千条原始时序算一遍的查询,现在只读一条预聚合序列,面板加载从秒级降到毫秒级。命名约定照抄官方的「级别:指标名:操作」格式,一看名字就知道内容。

预计算不是免费的

每条 recording rule 自己也会生成时序,sum by 保留的标签越多,生成的序列越多。只聚合到查询真正需要的维度,别把原始标签全带过来——那等于复制了一份高基数。

常见坑​

remote_write 队列积压​

现象:远程存储里数据延迟越来越大,或者干脆断档。发送队列是内存缓冲,满了会按设计丢数据。先看这几个指标:

prometheus_remote_storage_samples_pending
prometheus_remote_storage_samples_dropped_total
prometheus_remote_storage_shards

积压了就调 queue_config:加大 capacity(单 shard 队列深度)、加大 max_shards(并发发送分片数)、调小 batch_send_deadline 让发送更积极。调完还压不住,就是网络或远端写入能力到顶了,该扩容扩容。

保留期改短不立刻生效​

把 retention.time 从 30 天改成 15 天,磁盘不会马上瘦下来。TSDB 按 block 整块删除,要等包含老数据的 block 整体过期才删;压缩还可能把不同时期的数据合并进同一个大 block,这个大 block 要全部内容过期才释放。所以改完短则几小时、长则一两天才看到磁盘回落,别以为配置没生效就去重启。

双副本数据不一致​

现象:同一条查询,两个副本(去重前)的值对不上。排查顺序:

  1. 两边抓取是不是都在跑:对比两边的 up 和 scrape_samples_scraped,最常见的是一边目标 down 了没人发现。
  2. 抓取时间点本来就错开:两个副本各自有抓取周期,时间点不可能完全一致,rate 类查询的小幅差异是正常的,看趋势别看瞬时值。
  3. 重启或故障窗口:哪个副本重启过,那段时间它的数据就是空的——这正是双副本存在的意义,去重层会自动用另一份补上。

小结​

本篇收尾讲了单机之外的那些事:本地 TSDB 只是缓存层,长期数据靠 remote_write 交给 VictoriaMetrics / Mimir / Thanos;高可用靠双副本加查询层去重加 Alertmanager 集群;规模大了先纵向分片,多机房层级才考虑联邦;基数治理是日常功课——定期查时序分布,发现无界标签立刻 labeldrop,重查询用 recording rules 预计算。

回顾全系列八篇脉络:从核心概念与架构起步,讲清 TSDB、抓取模型和四种指标类型;接着[指标采集与 Exporter](/docs/observability/prometheus/指标采集与 Exporter)解决「数据从哪来」,relabeling 解决「采集时怎么改写」;PromQL 两篇把查询语言练熟;告警规则和 Alertmanager 把「出了事通知谁」跑通;prometheus-operator 把整套栈搬进 Kubernetes;本篇则回答最后一个问题——数据存多久、挂了怎么办、变大了怎么拆。八篇走完,从单机玩具到生产可用的路就铺完了,剩下的就是在自己的基础设施上把它们真正跑起来。