TSDB 存储原理:倒排索引、Block 结构、WAL 与 Compaction 机制,以及写入放大问题
Prometheus 的时序数据库(TSDB)是其核心组件,负责高效地存储和查询海量时间序列数据。理解 TSDB 的底层存储原理,是进行性能调优和资源配额规划的基础。本章将深入剖析倒排索引、Block 结构、WAL 与 Compaction 机制,并重点分析写入放大问题及其对性能的影响。
2.1 核心数据结构:倒排索引
倒排索引是 Prometheus 实现高效标签查询的关键。它通过将标签键值对映射到包含该标签的序列 ID 列表,从而在查询时能够快速定位目标序列,避免全表扫描。
{job="api-server", method="GET"})可以在毫秒级完成。
倒排索引在内存中以哈希表或 B+ 树形式维护,其结构如下:
- 正向索引(Postings List): 每个标签键值对对应一个有序的序列 ID 列表。例如,标签
job="api-server"对应一个包含所有该 job 下序列 ID 的列表。 - 倒排索引查找过程: 查询时,系统会分别查找每个标签对应的 Postings List,然后对多个列表执行交集(AND)或并集(OR)操作,最终得到满足所有条件的序列 ID 集合。
- 内存优化: 倒排索引完全驻留在内存中,以提供极低的查询延迟。其内存占用与标签基数(唯一标签值数量)和序列总数成正比。
2.2 磁盘存储单元:Block 结构
TSDB 在磁盘上以 Block 为基本单位组织数据。每个 Block 是一个自包含的目录,存储了一段时间范围内的所有序列数据及其索引。这种设计有利于数据压缩、快速删除和查询剪枝。
一个典型的 Block 目录结构如下:
01EM6Q6A1YPX4Z9X3X8X8X8X8X/
├── index # 倒排索引文件(内存映射)
├── meta.json # Block 元数据(时间范围、序列数、样本数等)
├── chunks/ # 原始样本数据块目录
│ ├── 000001 # Chunk 文件,每个包含多个压缩后的样本
│ └── ...
└── tombstones # 删除标记文件(逻辑删除)
| 组件 | 作用 | 性能影响 |
|---|---|---|
| index | 存储倒排索引、序列标签、时间戳范围等信息,支持内存映射访问。 | 索引大小直接影响查询启动速度和内存占用。 |
| chunks/ | 存放压缩后的样本数据。每个 Chunk 文件包含多个 Chunk,每个 Chunk 存储一个序列的连续样本。 | Chunk 大小和压缩算法影响 I/O 吞吐和 CPU 开销。 |
| meta.json | 记录 Block 的元数据,如最小/最大时间、序列数量、样本数量、数据来源等。 | 用于查询时快速跳过不相关的 Block。 |
| tombstones | 记录被删除的序列 ID 和时间范围,实现逻辑删除。 | 过多的删除标记会降低查询效率,需通过 Compaction 清理。 |
--storage.tsdb.retention.time 和 --storage.tsdb.retention.size 可以控制磁盘使用量。Block 大小通常为 2 小时数据,可通过 --storage.tsdb.min-block-duration 调整。
2.3 数据写入与恢复:WAL 机制
WAL(Write-Ahead Log)是 TSDB 保证数据持久性和崩溃恢复的关键组件。所有写入请求在写入内存中的 Block(Head Block)之前,必须先追加到 WAL 日志中。
WAL 的工作流程如下:
- 追加日志: 每个样本点到达时,首先被序列化为 WAL 记录,追加到当前活跃的 WAL 文件末尾。
- 内存写入: 日志写入成功后,样本点被更新到 Head Block 的内存结构中(包括倒排索引和 Chunk 缓存)。
- 定期截断: 当 Head Block 数据达到阈值(默认 2 小时)或 WAL 文件大小超过限制时,Head Block 被冻结并写入磁盘成为完整的 Block,同时 WAL 文件被截断。
- 崩溃恢复: 重启时,TSDB 会回放 WAL 日志,重建 Head Block 的内存状态,确保数据不丢失。
2.4 数据整理与优化:Compaction 机制
Compaction(压缩合并)是 TSDB 后台定期执行的数据整理任务,旨在合并小 Block、清理删除数据、优化索引结构,从而提升查询性能和降低磁盘空间占用。
Compaction 的主要工作包括:
- Block 合并: 将多个时间范围相邻的小 Block 合并成一个更大的 Block,减少 Block 数量,降低查询时需要扫描的 Block 数。
- 数据去重: 合并过程中,对同一序列同一时间戳的重复样本进行去重,保留最新值。
- 删除标记清理: 应用 tombstone 标记,物理删除被标记的序列数据,释放磁盘空间。
- 索引重建: 合并后重建倒排索引,优化索引结构,减少索引大小。
prometheus_tsdb_compactions_total 和 prometheus_tsdb_compaction_duration_seconds 指标。
2.5 写入放大问题
写入放大(Write Amplification)是指实际写入磁盘的数据量远大于逻辑写入的数据量。在 TSDB 中,写入放大主要由以下因素导致:
| 因素 | 描述 | 放大倍数参考 |
|---|---|---|
| WAL 写入 | 每个样本点先写入 WAL,再写入内存。WAL 写入是额外的 I/O 开销。 | 1x - 2x |
| Compaction 重写 | 合并 Block 时,需要读取源 Block 的所有数据,压缩后重新写入新 Block。 | 2x - 5x |
| Chunk 编码 | 样本在内存中以未压缩形式存在,写入磁盘时需进行压缩编码,产生临时数据。 | 1.5x - 3x |
| 索引更新 | 新序列加入时,倒排索引需要更新,可能触发索引文件的重新写入。 | 0.5x - 2x |
写入放大的影响:
- 磁盘 I/O 压力: 实际写入吞吐量远高于 scrape 速率,可能导致磁盘成为瓶颈。
- SSD 寿命损耗: 对于 SSD,写入放大直接缩短其使用寿命。
- CPU 开销: Compaction 和压缩过程消耗大量 CPU 资源。
--storage.tsdb.retention.size 限制总数据量;调整 --storage.tsdb.min-block-duration 和 --storage.tsdb.max-block-duration 控制 Block 大小,减少 Compaction 频率;监控 prometheus_tsdb_compaction_amplification 指标评估写入放大程度。
2.6 本章小结
TSDB 的存储设计通过倒排索引实现高效查询,通过 Block 结构实现数据隔离和压缩,通过 WAL 保证数据可靠性,通过 Compaction 优化存储布局。然而,这些机制也带来了写入放大问题,需要在资源规划和性能调优时予以重视。理解这些底层原理,有助于在生产环境中合理配置 Prometheus,平衡查询性能、写入吞吐和资源消耗。