用 Prometheus 监控一个 Kubernetes 集群,刚开始的体验通常很好:装上就能抓指标,PromQL 写起来也顺手。用上一段时间,就会陆续遇到这样几件事:

  • 想对比三个月前的接口延迟,发现查不到。Prometheus 默认只保留 15 天的数据。
  • 给一个指标加了 user_id 这个 label,想按用户看请求量。第二天 Prometheus 的内存涨了好几倍,最后被 OOM 杀掉。
  • Prometheus 所在的节点宕机了一次,那段时间的监控数据全没了,告警也跟着停了。

去搜解决办法,会冒出一长串名字:Thanos、Mimir、VictoriaMetrics、InfluxDB、TimescaleDB、ClickHouse、GreptimeDB……它们有的说自己是 Prometheus 的长期存储,有的说自己是时序数据库,有的其实是通用的分析型数据库。

于是有了几个问题:

  1. 时序数据有什么特别,为什么需要专门的存储?
  2. Prometheus 自己的存储是怎么设计的?为什么默认只留 15 天、只做单机?
  3. 为什么多加一个 label,内存就扛不住了?
  4. 那一长串长期存储方案,和 Prometheus 是什么关系?
  5. 跳出 Prometheus 来看,今天的时序存储市场是什么格局?

本文就顺着这几个问题,从「一个样本怎么存」讲起,一直讲到整个市场。


一、时序数据的特点

现象

先算一笔账。假设一个中等规模的集群有 1000 个抓取目标,每个目标暴露 1000 条时间序列,每 15 秒抓取一次:

  • 每秒写入 1000 × 1000 ÷ 15 ≈ 6.7 万个样本;
  • 每天写入约 57.6 亿个样本;
  • 一个样本是 8 字节的时间戳加 8 字节的 float64,共 16 字节。不压缩的话,一天就是 92GB。

写入量这么大,查询却很集中。看板和告警绝大多数时候只看最近一小时、最近一天的数据,几个月前的数据偶尔才查一次。

数据长什么样

Prometheus 抓到的一个样本长这样:

http_requests_total{job="api", path="/pay", code="500"}  @1728900000  1027

它由四部分组成:指标名(metric)、一组标签(label)、时间戳和值。指标名加上一组确定的 label 取值,叫作一条时间序列(series)。上面这一行属于 http_requests_total{job="api", path="/pay", code="500"} 这条 series,它每 15 秒多一个点。

这类数据的读写有几个鲜明的特点:

  • 写入几乎全是追加。 新样本的时间戳总是比旧的大,很少有人回头修改一个已经写入的样本。
  • 删除是整段过期。 不会删某一个点,而是「15 天以前的全部不要了」。
  • 读取有两种方向。 一种是「纵向」读:看一条 series 在过去一小时的走势。另一种是「横向」读:在同一个时刻,把几百条 series 加起来,比如所有实例的 QPS 之和。Prometheus TSDB 的作者 Fabian Reinartz 在设计文章里就是用这两个方向来分析问题的1。

第一次尝试:放进关系数据库

最直觉的办法是建一张表,(series_id, timestamp, value) 做主键,一个样本一行。这在数据量小的时候没问题,量一上来就会遇到三个麻烦:

  • 随机写。 每秒 6.7 万个样本分属 100 万条 series,按主键插进 B-tree,几乎每次都落在不同的页上。InfluxDB 早期就试过基于 B+tree 的存储(BoltDB),数据库超过几 GB 之后,写入的 IOPS 开始出现尖峰2。
  • 行开销比数据还大。 PostgreSQL 每行约有 48 字节的额外开销3,而一个样本本身只有 16 字节。
  • 删除很贵。 按保留期删数据,就是每天执行一次 DELETE ... WHERE timestamp < ?,要逐行删掉 57 亿行,再等存储引擎回收空间。

第二次尝试:固定大小的文件和分布式 KV

早期的监控系统走了两条路。

第一条是固定大小的文件。RRDtool 在创建数据库时就定好了大小,写满一圈后覆盖最旧的数据;降采样也在写入时完成,比如只保留每 5 分钟的平均值4。Graphite 的 Whisper 思路类似,每个点固定占 12 字节5。好处是文件大小永远不变,代价是原始精度在写入那一刻就丢了。而且一个指标一个文件,指标一多,磁盘就被大量的小文件随机写拖垮。

第二条是借用分布式 KV。OpenTSDB 把数据存在 HBase 里,rowkey 由「指标 ID + 按小时取整的时间 + 标签 ID」拼成,一行装一个小时的数据,再靠后台合并把一行里的多列拼成一列,以此减少 KV 本身的开销6。这样能横向扩展,但每个点都要背着 HBase 的开销。

把同一批样本分别放进关系表、Whisper 和 OpenTSDB,三种存法的差别就很直观了:

同一批样本的三种存法 12:00:15 抓到 http_requests_total 的 3 个样本。关系表把它们作为 3 行插进 3 个不同的 B-tree 页;Whisper 给每个指标一个固定大小的文件,样本写进 15 秒精度归档的一个槽,粗精度归档只存平均值;OpenTSDB 用指标、整点时间和标签拼出 rowkey,一行装一小时,样本按秒偏移存成一列。 12:00:15 抓到的 3 个样本 · http_requests_total 关系表 · 一行一个样本 Whisper · 一个指标一个文件 OpenTSDB · 一行装一小时 path="/login",code="200" 88 path="/login",code="500" 4 path="/pay",code="500" 1027 下面三栏把这 3 个样本分别存进三种结构,跟着 1027 看它落在哪里。 page 17 B-tree 叶子页 (3, 1728900015, 1027) page 52 B-tree 叶子页 (1, 1728900015, 88) page 90 B-tree 叶子页 (2, 1728900015, 4) 3 个样本落在 3 个不同的页, 每行还要另带约 48 字节。 api/pay/500.wsp 大小固定 archive 0 15s · 1 天 archive 1 5m 平均 · 30 天 archive 2 1h 平均 · 1 年 本次写入 槽 2881 ← 1027 api/login/*.wsp 另 2 个文件 文件大小在创建时就定了, 粗精度的归档只存平均值。 rowkey HBase 行 metric 000001 小时 670CEBA0 12:00 path 000002:000007 code 000003:000009 列 按秒偏移 +0 1020 +15 1027 一行最多 240 列,后台再合并。
图 1 · 同一批样本:关系表把它们插进不同的页,Whisper 预先分好固定的槽,OpenTSDB 按小时把一条 series 装进一行

这两条路暴露出时序存储真正要解决的两件事:一个样本要存得足够小,写入路径要对追加友好、删除要便宜。 先看第一件:一个 16 字节的样本,最少能压到多小?


二、样本的压缩

分析:相邻样本非常像

把一条 series 的样本按时间排好,会发现相邻的两个样本几乎一样:

  • 时间戳每 15 秒一个,间隔几乎是常数,偶尔因为抓取抖动差一两秒;
  • 值要么不变(比如一个温度、一个配置项),要么变化很小。

Facebook 在 2015 年的 Gorilla 论文里,就是利用这两个规律,把内存里的时序数据压缩了十倍以上7。

时间戳:差值的差值

假设一条 series 的时间戳是:

1728900000, 1728900015, 1728900030, 1728900045, 1728900061

相邻两个的差值是 15、15、15、16,再对差值做一次差,得到 0、0、1。Gorilla 的做法是:第一个时间戳原样存,第二个存差值,从第三个开始只存「差值的差值」(delta-of-delta),并且按大小分档编码:

  • 等于 0,只写 1 个 bit:0;
  • 落在 [-63, 64] 之间,写 10 再加 7 个 bit;
  • 更大的值用更长的前缀和位数。

上面这串时间戳,后三个分别只占 1、1、9 个 bit。论文统计,Facebook 的生产数据里约 96% 的时间戳只需要 1 个 bit。

数值:和前一个值做异或

浮点数没法直接做差,Gorilla 改为和前一个值做按位异或(XOR)。比如一个温度传感器连续上报 22.5、22.5、22.75:

22.5   = 0x4036800000000000
22.5   = 0x4036800000000000    XOR = 0                  → 写 1 个 bit
22.75  = 0x4036C00000000000    XOR = 0x0000400000000000 → 前导 0 有 17 个,有效位只有 1 个

值不变时异或结果为 0,只写 1 个 bit。值变了,异或结果的前后都是大段的 0,只需要记下「前面有几个 0、中间有效位有多长、有效位是什么」。22.5 变成 22.75 这一下,只需要 14 个 bit:2 个控制位、5 个 bit 的前导零个数、6 个 bit 的有效位长度,再加 1 个有效位。

效果

Gorilla 的论文给出的结果是平均 1.37 字节一个样本,比原始的 16 字节小了约 12 倍7。Prometheus 沿用了同样的思路,官方文档给出的数字是平均每个样本 1–2 字节8。

回到开头那笔账:一天 92GB 的原始数据,按每个样本 1.5 字节算,压缩后约 8.6GB。保留 15 天大约 130GB,一块普通的 SSD 就放得下。

浮点压缩此后还在持续改进:Chimp(2022)、Elf(2023)和 ALP(2024)都在 Gorilla 的基础上进一步提高了压缩率9。Prometheus 3.15(2026 年 9 月)也把新的 XOR2 浮点编码转成了稳定特性10。

不过,这种编码有一个前提:**同一条 series 的样本要挨在一起,才能和「前一个值」做差。**可样本是按时间到达的,每 15 秒,100 万条 series 各送来一个点,彼此交错在一起。怎么才能既按 series 连续地存,又不至于随机写盘?


三、写入路径的设计

第一次尝试:一条 series 一个文件

最直接的做法是每条 series 一个文件,来一个点就追加到对应的文件末尾。Prometheus 1.x 就是这样设计的:每条 series 一个文件,数据切成 1KB 的 chunk,索引存在 LevelDB 里11。

问题出在 Kubernetes 上。在 Kubernetes 里,pod 不断被销毁和重建,每个新 pod 都带着新的 pod label,也就带来一批新的 series,旧的 series 随之停止写入。这种现象叫 series churn。LWN 报道 Prometheus 2.0 时提到,它的测试场景是每 10 分钟替换一半的 pod12。series 一多,「一条 series 一个文件」就意味着:

  • 磁盘上有上百万个文件,每 15 秒要随机追加一遍;
  • 大量短命的 series 留下大量只有几个点的小文件;
  • 过期删除要逐个文件去处理。

InfluxDB 也在这里走过弯路。它先用 LevelDB(一种 LSM 树),写入很快,但删除要先写墓碑标记再等后台合并,代价很高。改成按时间分片、每个分片一个 LevelDB 之后,过期可以直接删文件了,可小文件又多到把文件句柄耗光。换成 BoltDB,句柄问题解决了,又遇到前面说的写入 IOPS 尖峰。最后才自己设计了 TSM 引擎:先写 WAL 和内存缓存,再批量写成不可变的文件,并且按时间分片2。

解决:Prometheus 2.0 的 block 设计

2017 年,Fabian Reinartz 为 Prometheus 2.0 重写了存储引擎1。核心思路是先按时间切,再在每段时间里按 series 组织:

  • 最近的数据放在内存里,叫 Head block。每条 series 在 Head 里有自己的 chunk,新样本追加到 chunk 末尾,用上一节的方法压缩。一个 chunk 最多装 120 个样本(按 15 秒的抓取间隔,约 30 分钟),写满的 chunk 会刷到磁盘并用 mmap 映射回来13。
  • 每个样本先写 WAL。 内存里的数据断电就没了,所以每个样本先追加到预写日志(WAL)里。进程崩溃后重放 WAL,就能恢复 Head。追加写是顺序 IO,很便宜。
  • 每满 2 小时,切成一个不可变的 block。 Head 覆盖的时间达到 3 小时,就把最老的 2 小时写成磁盘上的一个 block 目录,然后截断 WAL13。block 里的样本按 series 排好,查询时可以顺序读取。
  • 小 block 在后台合并成大 block,最大到保留期的 10% 或 31 天(取较小者)8。
  • 过期就是删除整个目录。 一个 block 覆盖一段确定的时间,过了保留期直接删掉,不需要逐条删除。
Prometheus TSDB 的写入路径和磁盘布局 最近的样本在内存的 Head block 里,同时追加到 data/wal/;Head 覆盖满 3 小时后,把最老的 10:00 到 12:00 写成一个不可变的 block 目录,里面有 meta.json、chunks、index 和 tombstones;更老的 block 过期时整个目录删除。 内存 磁盘 · data/ Head block 当前时间 12:43 series 1,000,000 条 覆盖 10:00 – 12:43 最老 2h 10:00 – 12:00 待切出 postings job="api" → 7, 9, 12 … Head 覆盖满 3 小时, 就把最老的 2 小时写成 block, 然后截断 WAL。 每条 series 的 chunk 最多 120 个样本, 写满后刷到 chunks_head/ 并 mmap 回来。 wal/ 每个样本先追加到这里,崩溃后重放 00000041 128MB,已写满 00000042 写入中 checkpoint.00000040 截断前留下的快照 01JA3K7X…/ 不可变的 block meta.json minTime 10:00 maxTime 12:00 chunks/000001 按 series 排好的压缩样本 index 符号表 + postings 倒排 tombstones 删除标记,不改 chunk 01JA2Q9M…/ 08:00 – 10:00 过期时整个目录删除
图 2 · 新样本先进内存和 WAL,每满 2 小时切出一个不可变的 block,过期时整块删除

这个设计把三件事同时理顺了:写入是顺序的(追加 WAL、追加内存 chunk),读取是按 series 连续的(block 里排好了序),删除是整块的(删目录)。2.0 正式发布时,官方给出的对比数据是:和 1.8 相比,CPU 占用降到原来的 20%–40%,磁盘空间降到 33%–50%,查询负载不高时,磁盘 IO 通常在 1% 以下14。

回到「只留 15 天」

现在可以回答开头的第一个现象了。15 天只是 --storage.tsdb.retention.time 的默认值,可以改成 90 天甚至一年。官方文档给出的磁盘估算公式是8:

D=T×R×BD = T \times R \times B

其中 DD 是磁盘空间,TT 是保留的秒数,RR 是每秒写入的样本数,BB 是每个样本的字节数。

按前面的例子,保留一年大约需要 3TB,单盘也放得下。真正的问题不在容量,而在官方对本地存储的定位。文档原话是:本地存储 “not clustered or replicated”,面对磁盘或节点故障时 “not arbitrarily scalable or durable”8。数据只有一份,存得越久,丢的时候损失越大。这一点留到第五节再展开。

block 设计解决了写入和删除的问题,但 Head 里还藏着一样东西:每条 series 在内存里都有一个对象,索引里也有它的一项。series 的数量一多,会发生什么?


四、高基数问题

现象

回到开头的第二个现象。假设 http_requests_total 有这些 label:

  • instance:100 个实例;
  • path:50 个接口;
  • code:5 种状态码。

最多是 100 × 50 × 5 = 25000 条 series。为了按用户看请求量,加上一个 user_id。如果有 1 万个活跃用户,series 数就变成 25000 × 10000 = 2.5 亿。样本数没变,还是每个请求累加一次;变的是 series 的数量。label 取值组合的个数叫作基数(cardinality),这就是常说的「高基数问题」。

原因

为什么 series 数比样本数更要命?看 Head 里一条 series 要占用什么:

  • 一个内存对象。 Prometheus 用一个 memSeries 结构表示一条 series,光结构本身约 200 字节,还没算 label 字符串和正在写的 chunk15。Brian Brazil 估算,Prometheus 2.x 每条 series 约需 732 字节,这个数字已经为 Go 的垃圾回收预留了一倍余量16。
  • 倒排索引里的若干项。 查询 {path="/pay", code="500"} 时,Prometheus 不会扫描所有 series,而是查倒排索引(postings):每个 label=value 对应一个排好序的 series ID 列表,把两个列表求交集,就得到要读的 series17。每多一条 series,它的每个 label 值对应的列表里都要多一项。
倒排索引与基数 http_requests_total 有 4 条 series,倒排索引把每个 label 值映射到 series ID 列表;查询 path="/pay" 且 code="500" 时,对 {3,4} 和 {2,4} 求交集得到 series 4。如果再加一个有 1 万个取值的 user_id,series 数变成 4 万条,每条都要一个内存对象和若干索引项。 series 倒排索引(postings) 再加一个 user_id http_requests_total 1 path="/login",code="200" 2 path="/login",code="500" 3 path="/pay",code="200" 4 path="/pay",code="500" 每条 series 在 Head 里 都有一个 memSeries 对象。 Brian Brazil 的估算: 每条约 732 字节(含 GC 余量) index label=value → ID path="/login" 1, 2 path="/pay" 3, 4 code="200" 1, 3 code="500" 2, 4 查询 /pay 且 500 {3, 4} ∩ {2, 4} = {4} 只读这一条 user_id="u0001" user_id="u0002" … user_id="u9999" 4 × 10,000 = 40,000 条 虚线:还没出现的 series。 每来一个新用户,就多出 4 条 series 和一批索引项。
图 3 · 倒排索引让查询只读命中的 series,但每条 series 都要占内存,label 取值一多就成倍增长

按 732 字节估算,2.5 亿条 series 光这一项就需要约 183GB 内存。这还没算上 series churn:短命的 series 不再写入以后,也要等 Head 下一次切 block(2 到 3 小时)才会从内存里清掉15。

所以高基数的根源是:存储的成本和 series 的数量成正比,而 series 的数量是各个 label 取值个数的乘积。

第一次尝试:把索引搬到磁盘

InfluxDB 也遇到过同样的问题。它早期的索引全部放在内存里,一般只能撑 100 万到 400 万条 series,而且启动时要把整个索引加载到堆上。2017 年的 1.3 版推出了 TSI,把索引做成磁盘上的 LSM 结构,用 mmap 访问,目标是支持 10 亿条 series18。

这缓解了内存压力,但没有改变本质:索引的大小仍然和 series 数成正比,有时候索引比数据本身还大。InfluxData 的创始人 Paul Dix 后来总结,InfluxDB 实际上是「倒排索引」和「时间序列数据」两个数据库拼在一起,这种割裂没法通过渐进修改来解决19。

换个思路:不要 series 索引

2020 年,InfluxData 宣布用 Rust 重写存储引擎(项目代号 IOx,后来成为 InfluxDB 3)。新设计里,tag 就是表里普通的一列,数据按列存储、按排序键排好,查询靠每个数据块的最小值和最大值来跳过不相关的部分19。新出现一个设备,只是在列里多写几个值,不会产生新的索引项,所以它宣称对基数没有上限。

ClickHouse 走的是同一个方向。它在一篇对比 Prometheus 的文章里说:在 ClickHouse 里,一个新的 label 组合只是多一行,不会产生新的结构对象;稀疏索引大约每 8000 行才有一项。所以成本取决于查询命中了多少行,而不是一共有多少条 series20。

但基数并没有凭空消失。同一篇文章也承认,按一个高基数的列做大范围的 GROUP BY 仍然很贵:示例里按 request_path 分组聚合一天的数据,耗时 9.4 秒,峰值内存 22.8GB20。列存把高基数的成本从「写入时一直占着内存」挪到了「查询时临时计算」。对于偶尔查一次的分析需求,这很划算;对于每 15 秒刷新一次的看板,就未必了。

在 Prometheus 里能做什么

如果后端还是 Prometheus,能做的主要是控制基数:

  • 不把无界的值放进 label。 官方的命名规范明确说,不要用 label 存用户 ID、邮箱这类取值没有上限的维度21。「按用户看请求量」更适合交给日志或链路追踪,或者一个列式数据库。
  • 用 native histogram 代替传统直方图。 传统直方图的每个 bucket 都是一条单独的 series,native histogram 把整个分布放进一条 series。它在 3.8 版(2025 年 11 月)成为稳定特性10。

到这里,前两个现象都有了答案。还剩第三个:Prometheus 宕机,数据就没了。单机的边界怎么突破?


五、Prometheus 与长期存储

原因:Prometheus 本来就没打算做数据库

要理解 Prometheus 为什么只做单机,得先看它是什么。官方文档的定位是一个监控和告警系统:用拉模式定期抓取各个目标的 /metrics,每台 Prometheus 服务器独立运行,只依赖本地磁盘,不依赖网络存储。这样,在其他基础设施都出了问题的时候,监控还能正常工作22。

本地的 TSDB 是为这个目标服务的一个部件,所以它有意保持简单:单机、不复制,默认只存最近的数据。需要长期保存、高可用和全局视图时,官方文档指向的是「远程存储集成」(remote storage integrations)8。

第一种办法:把 block 原样搬到对象存储

既然 Prometheus 每 2 小时会切出一个不可变的 block,最省事的办法就是把这些 block 传到 S3 这类对象存储上。

Thanos 就是这么做的。它在每台 Prometheus 旁边部署一个 sidecar,把新生成的 block 原样上传到对象存储;再由 Store Gateway 从对象存储里读 block 提供查询,Compactor 负责合并和降采样,Querier 把一个 PromQL 查询分发到各处,再把多个副本的结果去重。Thanos 的设计文档说,Prometheus 2.0 的存储格式是它所有组件的基础23。

Cortex 和从它分叉出来的 Grafana Mimir(2022 年,AGPL 许可)改用推送的方式:Prometheus 通过 remote write 把样本推给 Mimir,Mimir 的 ingester 在内存里跑同样的 TSDB,写三个副本,每 2 小时把 block 传到对象存储24。2025 年 11 月发布的 Mimir 3.0 又在写入和查询之间加了一层 Kafka,把读写两条路径彻底拆开25。

这条路线的好处是,存储格式直接沿用 Prometheus 的 block,行为和单机 Prometheus 一致。代价是 block 格式里的倒排索引也一起搬了过去,高基数问题依然存在,只是可以通过分片分摊到更多机器上。

第二种办法:只对接协议

另一条路线不复用 Prometheus 的存储格式,只实现它的两个接口:写入用 remote write,查询用 PromQL。

remote write 是 Prometheus 把样本推送到远端的 HTTP 协议,消息体是用 snappy 压缩的 protobuf。1.0 版在 2023 年成为稳定规范;2.0 版加入了字符串表(把重复的 label 字符串换成索引)、指标元数据等改进,目前还处于实验阶段26。Grafana 报告说,内部切换到 2.0 之后,网络出口流量下降了一半以上27。

2021 年 Prometheus 还推出了 Agent 模式:关掉查询、告警和本地存储,只保留服务发现、抓取和 remote write28。这一步很能说明问题:Prometheus 自己也承认,「抓指标」和「存指标」是可以分开的两件事。

只要实现了这两个接口,后端用什么存储都可以:

  • VictoriaMetrics 用自研的存储格式,数据按月分区,查询语言 MetricsQL 基本兼容 PromQL29;
  • Google Cloud Managed Service for Prometheus 的后端是 Google 内部的监控存储 Monarch30;
  • ClickHouse 在 2026 年 9 月推出了 TimeSeries 表引擎和服务端 PromQL,目前处于 private preview31;
  • GreptimeDB 用 Rust 重新实现了 PromQL,数据以 Parquet 格式放在对象存储上32。
Prometheus 和长期存储的两种连接方式 Prometheus 本地只存最近的数据。Thanos 由 sidecar 读取本地 data/ 目录,Mimir 通过 remote write 接收样本,两者都把 2 小时的 block 存进 S3。VictoriaMetrics、ClickHouse、GreptimeDB 只实现 remote write 和 PromQL,存储格式各不相同。 Prometheus 服务器 复用 Prometheus 的 block 格式 自研存储,只对接协议 prometheus 单机 抓取 每 15s 拉取 存储 data/ · 15d 复制 无 推送 remote write 本地只存最近的数据, 不复制,挂了就断。 长期存储交给右边。 Grafana PromQL 右边每个后端都能用它查 Grafana Mimir Cortex 分叉 写入 remote write 存储 2h block,三副本 Thanos sidecar 读取 同机的 data/ 目录 上传 2h block,原样 对象存储 s3://metrics/01JA3K7X…/{chunks,index,meta.json} VictoriaMetrics 查询 MetricsQL 存储 按月分区 ClickHouse 查询 PromQL 存储 TimeSeries 表 GreptimeDB 查询 PromQL + SQL 存储 Parquet + S3 还有 M3、Google Monarch 和各家云厂商的托管服务 … 它们只认 remote write 和 PromQL,不关心 Prometheus 怎么存。
图 4 · 长期存储有两种接法:沿用 Prometheus 的 block,或者只实现 remote write 和 PromQL

分析:Prometheus 更像一套标准

把这两条路线放在一起看,会发现一件有意思的事:在各家实现之间真正通用的,并不是 Prometheus 的存储引擎,而是下面四样东西:

  1. 数据模型:指标名加一组 label;
  2. 暴露格式:/metrics 的文本格式。曾经独立出去的 OpenMetrics 项目,2024 年又并回了 Prometheus33;
  3. 写入协议:remote write;
  4. 查询语言:PromQL 和它的 HTTP API。

Prometheus 和 CNCF 在 2021 年推出的兼容性认证(Prometheus Conformance Program),认证的也正是这些接口,而不是存储34。PromLabs 的 PromQL 兼容性测试(最近一轮公开结果停在 2022 年)里,Cortex、Thanos、Grafana Cloud 和 AWS 的托管服务都是 100% 通过;VictoriaMetrics 只有 74%,因为 MetricsQL 有意修改了 rate、increase 这类函数的外推方式35。

所以我的看法是:今天的 Prometheus,作为一套标准的分量,已经超过了它作为一个数据库的分量。 这是我对现象的归纳,不是官方的说法。连 ClickHouse 这样的通用数据库都在补 PromQL,就是一个佐证。Prometheus 3.x 原生接收 OpenTelemetry 的 OTLP 数据、默认支持 UTF-8 的指标名36,也在把这套标准往 OpenTelemetry 那边靠:OpenTelemetry 负责埋点和传输,Prometheus 负责查询和告警的语义。

回到开头的三个现象

现在可以把开头的三个现象放在一起回答了:

  1. 只留 15 天:15 天只是默认值。真正的限制是本地存储单机、不复制,所以长期数据应该交给对象存储上的后端。
  2. 加一个 label 就 OOM:存储成本和 series 数成正比,而 series 数是 label 取值个数的乘积。在 Prometheus 里要控制基数;真正需要高基数分析,就交给列式存储。
  3. 节点宕机数据就断:Prometheus 本来就是按「单机自治」设计的,高可用要靠多副本加查询端去重,或者 remote write 到一个多副本的后端。

不过,长期存储只是时序存储的一个分支。跳出 Prometheus,整个时序存储市场今天是什么样子?


六、时序存储的市场格局

现象:排名里的老面孔和新动向

DB-Engines 的时序数据库排名,是观察这个市场最常用的参考。下表是我能拿到的最新数据(2026 年 3 月)37:

排名产品分数和一年前比
1InfluxDB20.74−0.76
2Prometheus8.71+2.33
3kdb7.41+0.32
4TimescaleDB5.42+1.93
5DolphinDB4.58+2.28
6Graphite4.51−0.06
7QuestDB3.66+0.57
8Apache Druid3.50+0.71
9TDengine2.27+0.51
10VictoriaMetrics1.84+0.46

InfluxDB 仍然排第一,但分数在缓慢下降;Prometheus、TimescaleDB 和 DolphinDB 涨得最快。如果把「把时序当作次要模型」的数据库也算进来,ClickHouse 能排到第 5,排在大多数专用时序数据库前面38。

排名之外,过去两年还有几件事值得注意:

  • Timescale 在 2025 年 6 月改名 Tiger Data,理由是它云上的大部分负载根本不是时序数据,公司转而定位为「最快的 PostgreSQL」39。
  • AWS 自研的 Timestream for LiveAnalytics 从 2025 年 6 月起不再接受新客户,AWS 转而推荐托管版的 InfluxDB40。
  • InfluxDB 3 在 2025 年 4 月正式发布,用 Rust 完全重写,并且放弃了 2.x 主推的 Flux 查询语言,改回 SQL 和 InfluxQL41。
  • 可观测性平台 Chronosphere(创始人来自 Uber 的 M3 团队)在 2026 年 1 月被 Palo Alto Networks 以 33.5 亿美元收购42。

分析:「时序数据库」这个品类正在被拆散

这些事情背后,我看到三股力量。下面是我对现象的归纳。

第一股力量是新一代架构:列式存储加对象存储。 第四节讲过,用列存代替 series 索引,可以绕开高基数问题;第五节讲过,用对象存储代替本地盘,可以绕开单机的限制。新一代引擎把这两件事合在了一起,并且尽量复用开源组件,而不是从头写:InfluxDB 3 用 Arrow 作内存格式、DataFusion 作查询引擎、Parquet 作文件格式(InfluxData 称之为 FDAP 架构)43;GreptimeDB 在 2026 年 4 月发布 1.0,同样把 Parquet 放在对象存储上44;QuestDB 10.0 把 Parquet 作为表的存储格式,并兼容 Iceberg45。存储格式一旦变成通用的列式格式,查询引擎也就自然变成了通用的 SQL 引擎。

第二股力量是通用数据库吃掉了时序场景。 当时序数据可以用「按时间排序的列存」来存时,通用的分析型数据库天然就能做。Tiger Data 改名、AWS 停掉自研的时序库、ClickHouse 补上 PromQL,说的都是同一件事。

第三股力量是可观测性的融合。 指标、日志、链路追踪原本各存一套,现在越来越多的产品想把它们放进同一个库:GreptimeDB 1.0 的定位就是「可观测性数据库」;ClickHouse 在 2025 年收购 HyperDX,推出了 ClickStack46;VictoriaMetrics 也陆续推出了 VictoriaLogs 和 VictoriaTraces47。驱动力主要是成本:三套存储变成一套,运维和存储都更便宜。

专用时序数据库守住的场景

在这种局面下,专用时序数据库靠的是对某个垂直场景的深度优化:

  • 工业物联网。 设备多、测点固定、写入极其规律。TDengine 采用「一个设备一张表」的模型,每张表的数据连续存放,新数据只需追加,表与表之间不竞争锁;再用「超级表」统一同类设备的结构48。Apache IoTDB 出自清华,有自己的 TsFile 列式文件格式49。两者都在往「AI 原生」上重新定位,比如 TDengine 的 TDgpt 可以在 SQL 里直接做预测和异常检测。
  • 金融和量化。 kdb+ 长期是这个领域的标准,2026 年推出了新一代产品 KDB-X,并开放免费的社区版50;国内的 DolphinDB 在 DB-Engines 上涨得很快;QuestDB 也主打金融行情数据。
  • Prometheus 规模的指标存储。 VictoriaMetrics 和 Mimir 专门为 PromQL 和大规模指标优化,是第五节里那两条路线的主力。

怎么选

把前面的问题链串起来,选型时可以依次问自己几个问题:

  1. 数据是不是 Prometheus 生态里的指标? 如果是,先确认是不是真的需要长期存储和高可用。如果需要,在「复用 block」(Thanos、Mimir)和「只对接协议」(VictoriaMetrics 等)之间选。前者和原生 Prometheus 的行为最一致;后者通常更省资源,但要接受查询语义上的细微差异。
  2. 是不是需要按高基数的维度分析? 比如按用户、按请求 ID。如果是,这部分数据更适合放进列式数据库(ClickHouse、InfluxDB 3、GreptimeDB),而不是硬塞进 Prometheus 的 label。
  3. 团队是不是已经在用 PostgreSQL 或 ClickHouse? 如果是,TimescaleDB 或 ClickHouse 往往比再引入一个专用时序库更省事。
  4. 是不是工业物联网或金融场景? 这时专用产品(TDengine、IoTDB、kdb+、DolphinDB、QuestDB)在数据模型和性能上的优势最明显。

小结

回头看这一路,每一步都是被一个具体的问题推着走的:

  1. 时序数据的特点:写入量大、几乎全是追加、只看最近的数据、删除是整段过期。关系数据库的随机写和逐行删除扛不住;固定大小的文件丢精度,借用 KV 又太重。
  2. 样本的压缩:相邻的样本非常像。Gorilla 用「差值的差值」压时间戳,用异或压数值,把 16 字节的样本压到 1.37 字节。
  3. 写入路径的设计:压缩要求同一条 series 的数据挨在一起,样本却按时间交错到达。Prometheus 2.0 先写 WAL 和内存,每 2 小时切出一个不可变的 block,过期时整块删除。
  4. 高基数问题:存储成本和 series 数成正比,而 series 数是 label 取值个数的乘积。把索引搬到磁盘只能缓解;列式存储干脆不要 series 索引,把成本挪到了查询时。
  5. Prometheus 与长期存储:Prometheus 本来就只想做单机自治的监控。长期存储有两种接法:沿用它的 block 格式,或者只实现 remote write 和 PromQL。真正通用的是这套标准,而不是它的存储引擎。
  6. 市场格局:列存加对象存储成了新一代架构的默认选择,通用数据库和可观测性平台正在吃掉时序场景,专用时序数据库守住工业物联网、金融和大规模指标这几个垂直场景。

如果你正被开头那三个现象困扰,最小的改动是:先用 prometheus_tsdb_head_series 这个指标看一看当前的 series 数,找出基数最大的那几个 label;再决定哪些数据该留在 Prometheus,哪些该交给长期存储或列式数据库。


参考资料

Footnotes

  1. Fabian Reinartz, Writing a Time Series Database from Scratch, 2017. https://fabxc.org/tsdb/ ↩ ↩2

  2. InfluxDB 0.9 文档:Storage Engine ↩ ↩2

  3. Tiger Data:Time-series compression algorithms, explained ↩

  4. rrd-beginners(1) ↩

  5. Graphite 文档:Whisper ↩

  6. OpenTSDB 文档:HBase Schema ↩

  7. Pelkonen T, et al. Gorilla: A Fast, Scalable, In-Memory Time Series Database. PVLDB, 2015;8(12):1816–1827. https://www.vldb.org/pvldb/vol8/p1816-teller.pdf ↩ ↩2

  8. Prometheus 文档:Storage ↩ ↩2 ↩3 ↩4 ↩5

  9. Liakos P, et al. Chimp: Efficient Lossless Floating Point Compression for Time Series Databases. PVLDB, 2022;Li R, et al. Elf: Erasing-based Lossless Floating-Point Compression. PVLDB, 2023;Afroozeh A, et al. ALP: Adaptive Lossless floating-Point Compression. SIGMOD, 2024. ↩

  10. Prometheus CHANGELOG ↩ ↩2

  11. Prometheus 1.8 文档:Storage ↩

  12. LWN:Prometheus 2.0 ↩

  13. Ganesh Vernekar:Prometheus TSDB: The Head Block ↩ ↩2

  14. Prometheus Blog:Announcing Prometheus 2.0 ↩

  15. ClickHouse Blog:ClickHouse vs Prometheus: high cardinality, part 1 ↩ ↩2

  16. Robust Perception:How much RAM does Prometheus 2.x need for cardinality and ingestion? ↩

  17. Ganesh Vernekar:Prometheus TSDB: Persistent Block and its Index ↩

  18. InfluxData:Path to 1 Billion Time Series: InfluxDB High Cardinality Indexing ↩

  19. Paul Dix:Announcing InfluxDB IOx ↩ ↩2

  20. ClickHouse Blog:High cardinality, part 2: cardinality in ClickHouse ↩ ↩2

  21. Prometheus 文档:Metric and label naming ↩

  22. Prometheus 文档:Overview ↩

  23. Thanos 文档:Design ↩

  24. Grafana Mimir 文档:Architecture ↩

  25. Grafana Blog:Grafana Mimir 3.0 ↩

  26. Prometheus 文档:Remote-Write 2.0 specification ↩

  27. Grafana Blog:How Prometheus remote write v2 can help cut network egress costs ↩

  28. Prometheus Blog:Introducing Prometheus Agent Mode ↩

  29. VictoriaMetrics 文档:VictoriaMetrics、MetricsQL ↩

  30. Google Cloud:Managed Service for Prometheus ↩

  31. ClickHouse Blog:Introducing PromQL ↩

  32. GreptimeDB 文档:PromQL ↩

  33. CNCF Blog:OpenMetrics is archived, merged into Prometheus ↩

  34. Prometheus Blog:Introducing the Prometheus Conformance Program ↩

  35. PromLabs:PromQL Compliance Tests ↩

  36. Prometheus Blog:Announcing Prometheus 3.0 ↩

  37. DB-Engines:Ranking of Time Series DBMS ↩

  38. DB-Engines:Time Series DBMS, including secondary database models ↩

  39. Tiger Data:Timescale becomes TigerData ↩

  40. AWS 文档:Amazon Timestream for LiveAnalytics availability change ↩

  41. InfluxData:InfluxDB 3 Core and Enterprise GA;The future of Flux ↩

  42. Palo Alto Networks:Completes Chronosphere acquisition ↩

  43. Andrew Lamb:Flight, DataFusion, Arrow, and Parquet: Using the FDAP Architecture ↩

  44. Greptime:GreptimeDB v1.0 GA ↩

  45. QuestDB:QuestDB 10 ↩

  46. ClickHouse Blog:ClickHouse acquires HyperDX ↩

  47. VictoriaMetrics:2026 mid-year roundup ↩

  48. TDengine 文档:Architecture、Data Model ↩

  49. Apache IoTDB:TsFile Format ↩

  50. KX:KDB-X general availability ↩