用 Prometheus 监控一个 Kubernetes 集群,刚开始的体验通常很好:装上就能抓指标,PromQL 写起来也顺手。用上一段时间,就会陆续遇到这样几件事:
- 想对比三个月前的接口延迟,发现查不到。Prometheus 默认只保留 15 天的数据。
- 给一个指标加了
user_id这个 label,想按用户看请求量。第二天 Prometheus 的内存涨了好几倍,最后被 OOM 杀掉。 - Prometheus 所在的节点宕机了一次,那段时间的监控数据全没了,告警也跟着停了。
去搜解决办法,会冒出一长串名字:Thanos、Mimir、VictoriaMetrics、InfluxDB、TimescaleDB、ClickHouse、GreptimeDB……它们有的说自己是 Prometheus 的长期存储,有的说自己是时序数据库,有的其实是通用的分析型数据库。
于是有了几个问题:
- 时序数据有什么特别,为什么需要专门的存储?
- Prometheus 自己的存储是怎么设计的?为什么默认只留 15 天、只做单机?
- 为什么多加一个 label,内存就扛不住了?
- 那一长串长期存储方案,和 Prometheus 是什么关系?
- 跳出 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,三种存法的差别就很直观了:
这两条路暴露出时序存储真正要解决的两件事:一个样本要存得足够小,写入路径要对追加友好、删除要便宜。 先看第一件:一个 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 覆盖一段确定的时间,过了保留期直接删掉,不需要逐条删除。
这个设计把三件事同时理顺了:写入是顺序的(追加 WAL、追加内存 chunk),读取是按 series 连续的(block 里排好了序),删除是整块的(删目录)。2.0 正式发布时,官方给出的对比数据是:和 1.8 相比,CPU 占用降到原来的 20%–40%,磁盘空间降到 33%–50%,查询负载不高时,磁盘 IO 通常在 1% 以下14。
回到「只留 15 天」
现在可以回答开头的第一个现象了。15 天只是 --storage.tsdb.retention.time 的默认值,可以改成 90 天甚至一年。官方文档给出的磁盘估算公式是8:
其中 是磁盘空间, 是保留的秒数, 是每秒写入的样本数, 是每个样本的字节数。
按前面的例子,保留一年大约需要 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 值对应的列表里都要多一项。
按 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 的存储引擎,而是下面四样东西:
- 数据模型:指标名加一组 label;
- 暴露格式:
/metrics的文本格式。曾经独立出去的 OpenMetrics 项目,2024 年又并回了 Prometheus33; - 写入协议:remote write;
- 查询语言: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 负责查询和告警的语义。
回到开头的三个现象
现在可以把开头的三个现象放在一起回答了:
- 只留 15 天:15 天只是默认值。真正的限制是本地存储单机、不复制,所以长期数据应该交给对象存储上的后端。
- 加一个 label 就 OOM:存储成本和 series 数成正比,而 series 数是 label 取值个数的乘积。在 Prometheus 里要控制基数;真正需要高基数分析,就交给列式存储。
- 节点宕机数据就断:Prometheus 本来就是按「单机自治」设计的,高可用要靠多副本加查询端去重,或者 remote write 到一个多副本的后端。
不过,长期存储只是时序存储的一个分支。跳出 Prometheus,整个时序存储市场今天是什么样子?
六、时序存储的市场格局
现象:排名里的老面孔和新动向
DB-Engines 的时序数据库排名,是观察这个市场最常用的参考。下表是我能拿到的最新数据(2026 年 3 月)37:
| 排名 | 产品 | 分数 | 和一年前比 |
|---|---|---|---|
| 1 | InfluxDB | 20.74 | −0.76 |
| 2 | Prometheus | 8.71 | +2.33 |
| 3 | kdb | 7.41 | +0.32 |
| 4 | TimescaleDB | 5.42 | +1.93 |
| 5 | DolphinDB | 4.58 | +2.28 |
| 6 | Graphite | 4.51 | −0.06 |
| 7 | QuestDB | 3.66 | +0.57 |
| 8 | Apache Druid | 3.50 | +0.71 |
| 9 | TDengine | 2.27 | +0.51 |
| 10 | VictoriaMetrics | 1.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 和大规模指标优化,是第五节里那两条路线的主力。
怎么选
把前面的问题链串起来,选型时可以依次问自己几个问题:
- 数据是不是 Prometheus 生态里的指标? 如果是,先确认是不是真的需要长期存储和高可用。如果需要,在「复用 block」(Thanos、Mimir)和「只对接协议」(VictoriaMetrics 等)之间选。前者和原生 Prometheus 的行为最一致;后者通常更省资源,但要接受查询语义上的细微差异。
- 是不是需要按高基数的维度分析? 比如按用户、按请求 ID。如果是,这部分数据更适合放进列式数据库(ClickHouse、InfluxDB 3、GreptimeDB),而不是硬塞进 Prometheus 的 label。
- 团队是不是已经在用 PostgreSQL 或 ClickHouse? 如果是,TimescaleDB 或 ClickHouse 往往比再引入一个专用时序库更省事。
- 是不是工业物联网或金融场景? 这时专用产品(TDengine、IoTDB、kdb+、DolphinDB、QuestDB)在数据模型和性能上的优势最明显。
小结
回头看这一路,每一步都是被一个具体的问题推着走的:
- 时序数据的特点:写入量大、几乎全是追加、只看最近的数据、删除是整段过期。关系数据库的随机写和逐行删除扛不住;固定大小的文件丢精度,借用 KV 又太重。
- 样本的压缩:相邻的样本非常像。Gorilla 用「差值的差值」压时间戳,用异或压数值,把 16 字节的样本压到 1.37 字节。
- 写入路径的设计:压缩要求同一条 series 的数据挨在一起,样本却按时间交错到达。Prometheus 2.0 先写 WAL 和内存,每 2 小时切出一个不可变的 block,过期时整块删除。
- 高基数问题:存储成本和 series 数成正比,而 series 数是 label 取值个数的乘积。把索引搬到磁盘只能缓解;列式存储干脆不要 series 索引,把成本挪到了查询时。
- Prometheus 与长期存储:Prometheus 本来就只想做单机自治的监控。长期存储有两种接法:沿用它的 block 格式,或者只实现 remote write 和 PromQL。真正通用的是这套标准,而不是它的存储引擎。
- 市场格局:列存加对象存储成了新一代架构的默认选择,通用数据库和可观测性平台正在吃掉时序场景,专用时序数据库守住工业物联网、金融和大规模指标这几个垂直场景。
如果你正被开头那三个现象困扰,最小的改动是:先用 prometheus_tsdb_head_series 这个指标看一看当前的 series 数,找出基数最大的那几个 label;再决定哪些数据该留在 Prometheus,哪些该交给长期存储或列式数据库。
参考资料
Footnotes
-
Fabian Reinartz, Writing a Time Series Database from Scratch, 2017. https://fabxc.org/tsdb/ ↩ ↩2
-
InfluxDB 0.9 文档:Storage Engine ↩ ↩2
-
Tiger Data:Time-series compression algorithms, explained ↩
-
OpenTSDB 文档:HBase Schema ↩
-
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
-
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. ↩
-
LWN:Prometheus 2.0 ↩
-
Ganesh Vernekar:Prometheus TSDB: The Head Block ↩ ↩2
-
Prometheus Blog:Announcing Prometheus 2.0 ↩
-
ClickHouse Blog:ClickHouse vs Prometheus: high cardinality, part 1 ↩ ↩2
-
Robust Perception:How much RAM does Prometheus 2.x need for cardinality and ingestion? ↩
-
Ganesh Vernekar:Prometheus TSDB: Persistent Block and its Index ↩
-
InfluxData:Path to 1 Billion Time Series: InfluxDB High Cardinality Indexing ↩
-
Paul Dix:Announcing InfluxDB IOx ↩ ↩2
-
ClickHouse Blog:High cardinality, part 2: cardinality in ClickHouse ↩ ↩2
-
Prometheus 文档:Metric and label naming ↩
-
Grafana Mimir 文档:Architecture ↩
-
Grafana Blog:Grafana Mimir 3.0 ↩
-
Prometheus 文档:Remote-Write 2.0 specification ↩
-
Grafana Blog:How Prometheus remote write v2 can help cut network egress costs ↩
-
Prometheus Blog:Introducing Prometheus Agent Mode ↩
-
VictoriaMetrics 文档:VictoriaMetrics、MetricsQL ↩
-
Google Cloud:Managed Service for Prometheus ↩
-
ClickHouse Blog:Introducing PromQL ↩
-
Prometheus Blog:Introducing the Prometheus Conformance Program ↩
-
PromLabs:PromQL Compliance Tests ↩
-
Prometheus Blog:Announcing Prometheus 3.0 ↩
-
DB-Engines:Ranking of Time Series DBMS ↩
-
DB-Engines:Time Series DBMS, including secondary database models ↩
-
Tiger Data:Timescale becomes TigerData ↩
-
AWS 文档:Amazon Timestream for LiveAnalytics availability change ↩
-
InfluxData:InfluxDB 3 Core and Enterprise GA;The future of Flux ↩
-
Palo Alto Networks:Completes Chronosphere acquisition ↩
-
Andrew Lamb:Flight, DataFusion, Arrow, and Parquet: Using the FDAP Architecture ↩
-
Greptime:GreptimeDB v1.0 GA ↩
-
QuestDB:QuestDB 10 ↩
-
ClickHouse Blog:ClickHouse acquires HyperDX ↩
-
VictoriaMetrics:2026 mid-year roundup ↩
-
TDengine 文档:Architecture、Data Model ↩
-
Apache IoTDB:TsFile Format ↩