时序数据库存储架构深度对比:InfluxDB、Prometheus、VictoriaMetrics、TimescaleDB
引言
时序数据库(TSDB)是可观测性存储的基石。如果说上一篇是宏观的"存储地图",那本篇就是深入地图中的"核心城区"——四大主流 TSDB 的存储引擎。无论你是刚开始接触 Prometheus 还是正在调研 VictoriaMetrics,理解底层存储引擎的设计哲学,能帮你更高效地用对它们。
新手速查 — 四大 TSDB 一句话定位
数据库 定位 一句话理解 InfluxDB 专用时序数据库 最早流行的 TSDB,1.x/2.x 用自研 TSM 引擎,3.0 转型列式存储 Prometheus 云原生监控标准 自带本地 TSDB,单节点设计,被 Kubernetes 生态广泛采用 VictoriaMetrics 高性能替代方案 兼容 Prometheus 但更省内存,支持集群,冷数据压缩比极高 TimescaleDB PostgreSQL 时序扩展 不想学新查询语言就用 Postgres + 时序优化 如果读到"LSM-Tree"、“Compaction”、“倒排索引"这些词觉得陌生,别担心——本篇就从最核心的存储引擎说起,先搞清楚它要解决什么问题。
InfluxDB
存储引擎演进:从 LSM 到 TSM
InfluxDB 1.x/2.x 的存储引擎核心是 TSM(Time-Structured Merge Tree),它从 LSM-Tree 演化而来,专门针对时序数据优化[4][9]。存储引擎由四个核心组件构成[4]:
下图展示了 TSM 存储引擎的四个核心组件及其数据流:
flowchart TD
A[WAL<br/>预写日志] --> B[Cache<br/>内存缓存]
B --> C[TSM Files<br/>时序合并树文件]
C --> D[TSI<br/>倒排索引]
style A fill:#FF9800,color:#fff
style B fill:#2196F3,color:#fff
style C fill:#2196F3,color:#fff
style D fill:#9C27B0,color:#fff图说明:上图展示了 InfluxDB 1.x/2.x TSM 存储引擎的四大组件。写入路径从左至右依次经过 WAL 预写日志 → 内存 Cache → TSM 文件,TSI 倒排索引为高效的多维度查询提供支持。橙色节点代表写入入口,蓝色节点代表数据存储与处理层,紫色节点代表索引组件。
写入路径与 WAL 机制
写入流程[4]:
- 写请求追加到 WAL 文件末尾
- 通过
fsync()强制刷盘(保证持久性) - 更新内存 Cache
- 磁盘写入成功后返回确认
sequenceDiagram
participant Client as 写入请求
participant WAL as WAL
participant Cache as Cache
participant TSM as TSM Files
Client->>WAL: 1. 追加写入
WAL->>WAL: 2. fsync 刷盘
WAL->>Cache: 3. 更新内存
Cache-->>Client: 4. 返回确认
Note over Cache,TSM: 定期 snapshot
Cache->>TSM: 5. flush 为 TSM 文件图说明:上图展示了 InfluxDB 的完整写入路径。写入请求先追加到 WAL 并 fsync 刷盘保证持久性,然后更新内存 Cache 后立即返回确认(1-4 步)。后台定期将 Cache 快照刷新为 TSM 文件(第 5 步),实现从热数据到冷数据的异步流转。
Cache 特性[4]:按 key(measurement + tag set + field key)组织,每个 field 独立按时间排序,存储未压缩数据,重启时从 WAL 重建,查询时合并 Cache 数据与 TSM 文件数据。
TSM 文件格式与压缩
TSM 文件以列式格式存储压缩后的序列数据,只存储序列中值的差异(deltas)[4]。Compaction 过程将数据组织为长序列以优化压缩和扫描查询:
TSM 文件层级合并(类似 LSM 的 leveled compaction):数据按 series key 分组再按时间排序,多个 TSM 文件合并为更高层级,合并后 WAL 被截断、Cache 被清空。
TSI(Time Series Index)倒排索引
随着基数增长,早期内存索引在 100-400 万 series 时达到上限[5][6]。TSI 将索引持久化到磁盘,使用 mmap 让 OS 作为 LRU 缓存管理[6]。
TSI 架构(基于 LSM-Tree)[5]:
flowchart TD
A[Index<br/>单 Shard 完整索引] --> B[Partition<br/>分片分区]
B --> C[LogFile L0<br/>内存索引+WAL<br/>阈值 5MB]
B --> D[IndexFile<br/>L1/L2 层级合并<br/>mmap 不可变]
C -->|compaction| D
D --> E[SeriesFile<br/>跨库 series key<br/>自增 ID]
style A fill:#9C27B0,color:#fff
style B fill:#2196F3,color:#fff
style C fill:#FF9800,color:#fff
style D fill:#4CAF50,color:#fff
style E fill:#9C27B0,color:#fff图说明:上图展示了 InfluxDB TSI 倒排索引的 LSM-Tree 架构。Index 分片为多个 Partition(蓝色),每个包含 LogFile(橙色,内存索引+5MB WAL 阈值)和 IndexFile(绿色,不可变 mmap 层级合并)。SeriesFile(紫色)为所有跨库 series key 维护自增 ID,LogFile 超过 5MB 阈值后压缩为 L1 IndexFile。
写入流程[5]:Series 添加到 SeriesFile 或查找已有返回自增 series ID → Index 维护 Roaring Bitmap 记录已有 series ID → Series 哈希后发送到对应 Partition → Partition 将 series 写入 LogFile → LogFile 写入磁盘 WAL 并添加到内存索引。
Compaction[5]:LogFile 超过 5MB 压缩为 L1 IndexFile;两个连续 L1 文件合并为 L2 IndexFile。
IndexFile 结构[5]:TagBlocks(维护单个 tag key 的 tag value 索引)、MeasurementBlock(维护 measurement 及其 tag key 索引)、Trailer(偏移信息 + HyperLogLog 基数估计草图)。
读取与查询——TSI 提供可组合的迭代器 API[5]:
MeasurementSeriesIDIterator()→ measurement 对应的所有 series IDTagValueSeriesIDIterator()→ tag value 对应的所有 series ID- 合并操作:Merge(去重并集)、Intersect(交集)、Difference(差集)
例如 WHERE region != 'us-west' 跨两个 shard 的查询构建为:
| |
InfluxDB 3.0:FDAP 架构(列式存储革命)
InfluxDB 3.0(即 InfluxDB IOx)彻底抛弃了自定义存储格式,转向 FDAP 技术栈[7][8][9]:
flowchart TD
A[Flight<br/>gRPC+Arrow 传输] --> B[DataFusion<br/>向量化查询引擎]
B --> C[Arrow<br/>内存列式格式]
C --> D[Parquet<br/>磁盘列式存储]
style A fill:#2196F3,color:#fff
style B fill:#9C27B0,color:#fff
style C fill:#4CAF50,color:#fff
style D fill:#4CAF50,color:#fff图说明:上图展示了 InfluxDB 3.0 的 FDAP 技术栈四层架构。Flight 提供基于 gRPC 的 Arrow 数组高效网络传输;DataFusion 是 Rust 编写的向量化 SQL 查询引擎,支持投影/过滤下推与自动并行化;Arrow 是内存列式格式,实现跨组件零拷贝共享;Parquet 提供高效的磁盘列式存储格式。蓝色为传输层,紫色为查询引擎,绿色为数据格式层。
核心设计决策与数据[7]:
- Parquet 代替自定义 TSDB 格式,实测整体压缩比约 10x(相比原始数据),InfluxData 官方未公布相对 TSM 的精确对比数字
- Arrow:内存列式格式,cache 友好,避免序列化开销,跨组件零拷贝共享
- DataFusion:Rust 编写的向量化 SQL 查询引擎,支持投影/过滤下推、自动并行化
- Flight:基于 gRPC 的 Arrow 数组网络传输协议,免序列化
- Ingester:接收写入,内存中以 Arrow 缓存(立即可查),WAL 保证持久性,定期刷为 Parquet 文件到 S3/GCS/Azure Blob
- Querier:构建查询计划,查询 Ingester 获取最新未持久化数据,查询 Catalog 获取 Parquet 文件位置
- Compactor:后台合并小 Parquet 文件为大文件(类似 LSM compaction)
- 存储计算分离(Lakehouse 模型):无状态计算节点,存储在对象存储上无限扩展
Prometheus
本地 TSDB 存储引擎
Prometheus 内置本地磁盘 TSDB,采用自定义高效格式,平均每样本仅 1-2 字节[10]。
磁盘文件布局[10]:
| |
Head Block 与 Persistent Block
- Head Block:当前接收写入的块,保存在内存中,通过 WAL 保障崩溃恢复[10]
- Persistent Block:Head Block 满后(2 小时),持久化为不可变 Block,包含 chunks、index、tombstones[10]
Chunk 编码与 Gorilla 压缩
flowchart TD
A["原始数据<br/>120 samples / 2h"] --> B["Gorilla XOR 编码"]
B --> C["delta-of-delta<br/>时间戳压缩"]
B --> D["XOR<br/>值压缩"]
C --> E["密封 Chunk<br/>150-250 bytes"]
D --> E
E --> F["不可变<br/>并发安全"]
style A fill:#FF9800,color:#fff
style B fill:#9C27B0,color:#fff
style C fill:#2196F3,color:#fff
style D fill:#2196F3,color:#fff
style E fill:#4CAF50,color:#fff
style F fill:#4CAF50,color:#fff图说明:上图展示了 Prometheus Chunk 的结构特征。每个 Chunk 包含 120 个样本或 2 小时数据(先到者),采用 Gorilla XOR 编码(delta-of-delta 时间戳 + XOR 值压缩),大小约 150-250 字节。Chunk 密封后不可修改,确保并发读取安全性。
Prometheus chunk 是密封的、不可变的、Gorilla-XOR 编码的数据块,通常 150-250 字节[2][3]。
mmap 机制
Prometheus TSDB 积极使用 mmap(内存映射文件)[10][3]:持久化 Block 的 index 文件和 chunk 文件 mmap、Head Block 完成的 chunk(chunks_head/)mmap。优势:OS 管理页面缓存(LRU),零拷贝访问,减少进程内存占用[3]。
WAL 机制与远程存储接口
- WAL 存储在
wal/目录,128MB 段,包含尚未压缩的原始数据[10] - 至少保留 3 个 WAL 文件,默认启用 WAL 压缩(2.20.0 起可将 WAL 大小减半)
- 支持乱序写入(
tsdb.out_of_order_time_window)
Prometheus 通过四种方式集成远程存储[10]:Remote Write(写入远程 URL,snappy 压缩 protobuf over HTTP)、Remote Write Receiver(接收其他客户端样本)、Remote Read(从远程 URL 读回样本)、Remote Read 响应。
关键局限:远程读路径仅获取原始序列数据,所有 PromQL 评估仍在本地完成,存在可扩展性限制[10]。默认保留 15 天,单节点无集群[10]。
VictoriaMetrics
集群组件架构
对于时序数据库来说,“集群"解决的核心问题是:单机撑不住了怎么办?VictoriaMetrics 把写入、存储、查询三个环节拆成独立服务,各司其职、独立扩缩容。
VictoriaMetrics 集群采用 Shared-Nothing 架构,由三个独立可扩展的服务组成[11]:
动画说明:上方动画分阶段展示 VM 集群的数据流。第一阶段(15-35% 周期)展示写请求进入 vminsert,经一致性哈希分配到 vmstorage 节点;第二阶段(35-55%)展示查询请求进入 vmselect,向所有 vmstorage 节点并行拉取;第三阶段(55-75%)展示 vmstorage 返回查询结果;最终阶段展示完整的 Shared-Nothing 架构特征。
下面用两个小图分别展示写入路径和查询路径:
flowchart TD
A[vminsert<br/>写入路由] --> B[vmstorage<br/>Node A]
A --> C[vmstorage<br/>Node B]
A --> D[vmstorage<br/>Node C]
A --> E[vmstorage<br/>Node D]
style A fill:#2196F3,color:#fff
style B fill:#4CAF50,color:#fff
style C fill:#4CAF50,color:#fff
style D fill:#4CAF50,color:#fff
style E fill:#4CAF50,color:#fff图说明:写入路径。vminsert(蓝色)按 metric name 加所有 labels 的一致性哈希将数据分发到 vmstorage 节点(绿色)。
flowchart TD
A[vmselect<br/>查询处理] --> B[vmstorage<br/>Node A]
A --> C[vmstorage<br/>Node B]
A --> D[vmstorage<br/>Node C]
A --> E[vmstorage<br/>Node D]
style A fill:#2196F3,color:#fff
style B fill:#4CAF50,color:#fff
style C fill:#4CAF50,color:#fff
style D fill:#4CAF50,color:#fff
style E fill:#4CAF50,color:#fff图说明:查询路径。vmselect(蓝色)发起查询时从所有 vmstorage 节点(绿色)并行拉取数据。
- vminsert:按 metric name + 所有 labels 的一致性哈希分发到 vmstorage 节点[11]
- vmstorage:存储原始数据,节点互不通信、不共享数据
- vmselect:执行查询,从所有 vmstorage 节点获取所需数据
存储引擎:改进的 MergeTree / LSM
vmstorage 的存储引擎基于类 LSM-Tree 结构,数据经历多级转换[12]:
flowchart TD
A[In-memory Part<br/>1-17 MB] --> B[Small Part<br/>磁盘 ≤10 MB]
B --> C[Big Part<br/>磁盘 ≤1 TB]
style A fill:#FF9800,color:#fff
style B fill:#2196F3,color:#fff
style C fill:#4CAF50,color:#fff图说明:上图展示了 VictoriaMetrics 存储引擎中三种 LSM Part 的层级流转。In-memory Part(橙色,1-17 MB)是第一次 flush 的产物,立即可查询;Small Part(蓝色,磁盘 ≤10 MB)从内存 flush 或合并产生;Big Part(绿色,磁盘 ≤1 TB)通过合并产生。颜色从暖到冷表示数据从热到冷的生命周期。
三种 LSM Part 类型[12]:
| Part 类型 | 位置 | 大小范围 | 说明 |
|---|---|---|---|
| In-memory part | 内存 | 1-17 MB | 第一次 flush 产物,可查询 |
| Small part | 磁盘(data/small/) | 最大 10 MB | 从内存 flush 或合并产生 |
| Big part | 磁盘(data/big/) | 最大 ~1 TB | 合并产生 |
Flush 触发条件[12]:大小触发(buffer 达到约 120MB)或时间触发(距上次 flush 超过 2 秒)。最多 60 个 in-memory part,约占系统内存 10%。
Merge 机制与压缩算法
合并不按固定调度运行,基于"因果关系”[12]:
Merge Multiplier(合并乘数)[12]:定义输出 part 总大小/最大输入 part 大小,默认值 7.5,单次最多合并 15 个 part。合并时同时执行去重(Deduplication),通过 -dedup.minScrapeInterval 配置去重窗口。
- 热数据路径:Gorilla 编码(delta-of-delta + XOR)
- 冷数据(导出到 S3 的 Parquet):Gorilla + ZSTD 叠加,压缩比可达 25-35x
- 与 Prometheus 本地存储相比,内存占用显著降低,单机可处理数千万级时间序列
层级降采样(Downsampling)
新手类比:你不需要记得一周前每一秒的 CPU 利用率是多少——看小时级别的平均值就够了。TSDB 的"层级降采样"就是这个思路:数据越老,保留的分辨率越低,节省存储空间的同时不影响趋势分析。
VictoriaMetrics 没有内置的自动降采样(需要搭配 vmctl 等外部工具),但理解这个概念对后续理解 TSDB 冷热分层至关重要:
动画说明:上方动画展示 TSDB 层级降采样的四个阶段。原始数据(橙色,15 秒间隔)经过多次 compaction 逐步合并为更大时间窗口的聚合数据——5 分钟聚合(avg/min/max, 每 20 点合 1 点)→ 1 小时聚合(12 个 5m 合 1 个)→ 1 天聚合(24 个 1h 合 1 个)。存储密度随着分辨率降低而指数级提升——从 15 秒一个数据点到 1 天一个点,存储需求降低 5760 倍。
性能特点
| 指标 | 数据 |
|---|---|
| 单机写入 | 可达百万级数据点/秒[11] |
| 内存效率 | 官方称低 7x(VM blog),第三方基准实测约 1.7x[3] |
| 压缩比 | 热数据 ~12x(Gorilla),冷数据 25-35x[3] |
| 推荐集群阈值 | 写入率 > 100 万点/秒[11] |
流式聚合:VictoriaMetrics 提供了内置的流式聚合(Stream Aggregation)功能,可在写入端对指标进行实时降采样和聚合,减少存储量与查询负载。深入实践请参阅 Stream Metrics 系列。
TimescaleDB
核心定位:PostgreSQL 扩展
TimescaleDB 是 PostgreSQL 扩展,在 Postgres 坚实的事务引擎、复制和生态之上叠加时序优化[3][15]。
Hypertable 与 Chunk 架构
flowchart TD
A[Hypertable<br/>逻辑表] --> B[Chunk 1<br/>T0-T1]
A --> C[Chunk 2<br/>T1-T2]
A --> D[Chunk 3<br/>T2-T3]
A --> E[Chunk N<br/>Tn-1-Tn]
style A fill:#9C27B0,color:#fff
style B fill:#2196F3,color:#fff
style C fill:#2196F3,color:#fff
style D fill:#2196F3,color:#fff
style E fill:#2196F3,color:#fff图说明:上图展示了 TimescaleDB 的 Hypertable 逻辑架构。Hypertable(紫色)是对用户透明的逻辑表,自动按时间范围分区为多个 Chunk(蓝色),每个 Chunk 底层是标准的 PostgreSQL heap 表。INSERT 操作根据时间戳自动路由到对应的 Chunk,SELECT 查询通过 Chunk 裁剪(Chunk Exclusion)跳过不相关的时间分区。
压缩策略:列式存储
压缩算法(列级别)[15]:Dictionary Encoding(字典编码)、Delta Encoding(增量编码)、Gorilla Compression(浮点数 XOR 压缩)、Run-Length Encoding(行程长度编码)。
压缩效果[14][15]:chunk 压缩后存储减少 最高 98%,列式格式支持快速扫描和聚合,可透明查询列式数据无需解压。
Hypercore(TimescaleDB 2.18, 2025)[17]:统一 rowstore/columnstore 引擎,同一 hypertable 同时持有热行 + 压缩冷列,基于 chunk age 自动转换。
连续聚合(Continuous Aggregates)
连续聚合是增量更新的物化视图,避免每次查询重新计算聚合[14][15]:
flowchart TD
A[Materialization Engine<br/>聚合引擎] --> B[Materialization Hypertable<br/>存储聚合结果]
C[Invalidation Engine<br/>失效检测] --> A
D[Query Engine<br/>查询引擎] --> B
style A fill:#2196F3,color:#fff
style B fill:#4CAF50,color:#fff
style C fill:#FF9800,color:#fff
style D fill:#9C27B0,color:#fff图说明:上图展示了 TimescaleDB 连续聚合的四组件架构。Invalidation Engine(橙色)检测原始 Hypertable 的数据变更并通知 Materialization Engine(蓝色)执行增量聚合计算,结果写入 Materialization Hypertable(绿色)。Query Engine(紫色)透明地查询预聚合结果。
关键机制[14]:增量更新(只处理新增/变更数据)、物化阈值(滞后时间点)、两阶段事务、失效引擎跟踪数据变更。支持层级连续聚合(秒级→小时级→日级)和实时聚合(预聚合数据与最新原始数据合并)[14]。
sequenceDiagram
participant Raw as 原始 Hypertable
participant IE as Invalidation Engine
participant ME as Materialization Engine
participant Mat as 物化 Hypertable
Raw->>IE: 数据变更通知
IE->>IE: 计算失效区间
IE->>ME: 触发增量刷新
ME->>Raw: 读取新增/变更数据
ME->>ME: 执行聚合计算
ME->>Mat: 写入物化结果图说明:上图展示了 TimescaleDB 连续聚合的增量刷新流程。原始 Hypertable 的数据变更通知 Invalidation Engine 计算失效区间,触发 Materialization Engine 读取新数据、执行聚合计算并写入物化 Hypertable。整个过程对用户透明且增量式执行。
与原生 TSDB 的差异
| 维度 | TimescaleDB | 原生 TSDB |
|---|---|---|
| 存储引擎 | PostgreSQL heap + 列式压缩 | 专用 TSM/LSM/Gorilla |
| 事务语义 | 完整 ACID(继承 Postgres) | 通常无 ACID |
| 查询 | 完整 Postgres SQL | 专用查询语言 |
| 压缩 | 列式压缩(90-98%) | Gorilla 12x+ |
| 连续聚合 | 原生支持(增量物化视图) | 无(需外部工具) |
TSDB 横向对比
底层数据结构对比
| 项目 | 磁盘文件格式 | 内存结构 | 索引类型 |
|---|---|---|---|
| InfluxDB 1.x/2.x | TSM 文件(列式,delta 编码) | Cache(未压缩) | TSI(LSM-based 倒排,Roaring Bitmap,mmap) |
| InfluxDB 3.0 | Parquet(列式) | Arrow(列式) | 运行时从 Parquet 元数据重建 |
| Prometheus | chunks/(段 512MB)+ index | Head Block(内存) | 倒排索引(block 内 label→series→chunk) |
| VictoriaMetrics | LSM parts(in-memory/small/big) | raw-row shards → in-memory parts | mergeset 索引引擎 |
| TimescaleDB | Postgres heap table + 列式压缩 chunk | Postgres shared_buffers | B-tree(Postgres 原生) |
写入路径与压缩算法对比
| 项目 | WAL 机制 | 时间戳压缩 | 值压缩 | 平均字节/样本 |
|---|---|---|---|---|
| InfluxDB | append + fsync | delta 编码 | delta 编码(列式) | ~2-3 bytes |
| Prometheus | 128MB 段,snappy 压缩 | delta-of-delta (Gorilla) | XOR (Gorilla) | 1-2 bytes |
| VictoriaMetrics | 内存 part flush 到磁盘 | delta-of-delta (Gorilla 改进) | XOR + ZSTD(冷) | ~1-2 bytes(热),25-35x(冷) |
| TimescaleDB | Postgres WAL | delta encoding | Gorilla + RLE + Dictionary | 90-98% 压缩 |
集群/分布式架构对比
| 项目 | 架构模式 | 分片方式 | 复制 | 多租户 |
|---|---|---|---|---|
| InfluxDB Enterprise | 数据节点+Meta 节点 | 按 shard | RP 副本数 | 有限 |
| InfluxDB 3.0 | 存储计算分离(Lakehouse) | Parquet 文件在对象存储 | 对象存储冗余 | 独立集群 |
| Prometheus | 单节点(无集群) | N/A | N/A | N/A |
| VictoriaMetrics | Shared-Nothing | 一致性哈希(metric+labels) | replicationFactor 顺序传递 | accountID:projectID |
| TimescaleDB | Postgres 流复制/Citus | 手动/第三方 | Postgres 复制 | DB 级别隔离 |