首页
/ Prometheus TSDB Tombstones 磁盘格式详解:Block 级删除标记文件的布局与读写实现

Prometheus TSDB Tombstones 磁盘格式详解:Block 级删除标记文件的布局与读写实现

2026-09-06 20:07:02作者:瞿蔚英Wynne

本文以 Prometheus 仓库中的 tombstones.md 文档为主体,完整讲解 TSDB Block 目录下 tombstones 文件的磁盘格式:文件头部魔数与版本字节、每条删除区间记录的 varint 编码方式、文件尾部校验结构,并结合 tsdb/tombstones/tombstones.go 源码剖析 WriteFile / ReadTombstones / MemTombstones 的实际实现。读完后,你将能够独立解读一个 Block 的 tombstones 文件布局,理解"软删除"在 TSDB 落盘与查询过滤链路中的完整位置,并在自行开发 TSDB 工具时正确复用其编解码约定。

Tombstones 在 TSDB 中的角色:删除不是物理移除

TSDB 的 Block 由若干文件组成(meta.jsonindexchunks/ 目录,以及位于 Block 顶层目录的 tombstones 文件,见 TSDB 格式总览)。当用户请求删除某时间范围内的序列时,Prometheus 不会去改写不可变的 chunk 数据,而是把"哪个序列引用(series ref)在哪个时间区间被删除"写入 tombstones 文件——即文档开头所说:"tombstones 文件放置在 block 的顶层目录"。

从源码看,这个机制贯穿了删除、读取、压实三个阶段:

  • 删除入口:HTTP API 的 /admin/tsdb/delete_series 路由(web/api/v1/api.go)最终调用 DB.Deletetsdb/db.go),它对所有时间范围有重叠的 Block 并发执行 Block.Delete,并对同样重叠的 Head 执行 head.Delete。源码注释明确其原子性保证是"per-block basis"(逐块级别)。
  • Block 级落盘Block.Deletetsdb/block.go)通过索引找出与 [mint, maxt] 区间有 chunk 重叠的序列,构造新的 MemTombstones,再调用 tombstones.WriteFile 整体重写该 Block 的 tombstones 文件,并同步更新 meta.json 中的 NumTombstones 统计。
  • 读取过滤OpenBlock 加载 Block 时即调用 tombstones.ReadTombstones(dir)tsdb/block.go)把删除区间读入内存,供查询器在扫描 chunk 时跳过被删除的时间范围。
  • 最终清理Block.CleanTombstonestsdb/block.go)在有 tombstone 的 Block 上触发压实重写;压实器写出新 Block 时会顺带生成一个空的 tombstones 文件(tsdb/compact.go)。

文件整体布局:魔数、版本、Stones 区与尾部字段

文档给出的整体结构如下(引自 tombstones.md):

┌────────────────────────────┬─────────────────────┐
│ magic(0x0130BA30) <4b>     │ version(1) <1 byte> │
├────────────────────────────┴─────────────────────┤
│ ┌──────────────────────────────────────────────┐ │
│ │                Tombstone 1                   │ │
│ ├──────────────────────────────────────────────┤ │
│ │                      ...                     │ │
│ ├──────────────────────────────────────────────┤ │
│ │                Tombstone N                   │ │
│ ├──────────────────────────────────────────────┤ │
│ │                  CRC<4b>                     │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘

文档中的两点关键说明:

  1. 文件头部是 4 字节大端魔数 0x0130BA30,紧接 1 字节版本号(当前为 1)。
  2. 文档文字部分提到 "The last 8 bytes specifies the offset to the start of Stones section",并说明 Stones 区按 4 字节 0 填充以便快速扫描。这里需要注意:从当前源码实现看,WriteFile 写出的文件尾部字段是 4 字节 CRC(对应图中 CRC<4b>),而文档文字描述的"尾部 8 字节偏移"字段在当前 tsdb/tombstones/tombstones.go 的写/读路径中并未体现——从源码结构看,该文字描述更可能对应历史版本或预留说明,理解格式时应以头部魔数、版本字节、变长 Stones 区、尾部校验这几个核心字段为准。

头部与校验相关的常量定义在 tsdb/tombstones/tombstones.go

常量 含义
TombstonesFilename "tombstones" 文件名,位于 Block 顶层目录
MagicTombstone 0x0130BA30 4 字节魔数,文件头
tombstoneFormatV1 1 当前格式版本(1 字节)
tombstoneFormatVersionSize 1 版本字段长度
tombstonesHeaderSize 5 头部长度(4 字节魔数 + 1 字节版本)
tombstonesCRCSize 4 尾部 CRC 长度

CRC 采用 CRC32-Castagnoli 多项式,表在包 init 阶段预构建(tsdb/tombstones/tombstones.go),以便将来需要更换多项式时只需改一处。

Tombstone 记录编码:uvarint 序列引用 + varint 时间区间

每条 Tombstone 记录的结构(引自 tombstones.md):

┌───────────────────────┬─────────────────┬────────────────┐
│series ref <uvarint64> │ mint <varint64> │ maxt <varint64>│
└───────────────────────┴─────────────────┴────────────────┘

三个字段全部采用 LEB128 风格的变长编码,序列引用用无符号 uvarint64,起止时间用(zigzag 后的)varint64 以支持负时间戳。对应实现见 Encodetsdb/tombstones/tombstones.go):

func Encode(tr Reader) ([]byte, error) {
	buf := encoding.Encbuf{}
	buf.PutByte(tombstoneFormatV1)   // 版本字节
	err := tr.Iter(func(ref storage.SeriesRef, ivs Intervals) error {
		for _, iv := range ivs {
			buf.PutUvarint64(uint64(ref))
			buf.PutVarint64(iv.Mint)
			buf.PutVarint64(iv.Maxt)
		}
		return nil
	})
	return buf.Get(), err
}

即 Stones 区是"逐条区间"平铺的三元组序列:同一个序列引用可以有多个不重叠区间。反向解码在 Decodetsdb/tombstones/tombstones.go):先校验首字节必须等于版本 1(否则报 invalid tombstone format),随后以 d.Len() > 0 为循环条件依次读出 ref / mint / maxt 三元组,装入 MemTombstones。这种"读到缓冲区耗尽即结束"的自描述结构意味着解析端无需单独的长度前缀,也解释了文档中 Stones 区按 4 字节对齐填充便于快速扫描的动机——尾部填充字节不携带语义。

写入流程:WriteFile 的校验和与原子替换

tombstones.WriteFiletsdb/tombstones/tombstones.go)完整实现了文档布局的生成过程:

  1. 临时文件:先写 tombstones.tmp,任何失败路径下都会删除临时文件并关闭句柄(defer 兜底)。
  2. 写头部buf.PutBE32(MagicTombstone) 写入 4 字节大端魔数。
  3. 编码 Stones 区:调用 Encode 生成"版本字节 + 全部三元组"。
  4. 计算校验和:注意源码中的注释——计算 CRC 时跳过首字节(版本字节),只对 Stones 区内容做 hash;这一"忽略首字节"的兼容处理在写与读两端是对称的。
  5. 落盘与原子替换:依次写入编码结果和 4 字节 CRC 摘要,执行 f.Sync() 刷盘,最后通过 fileutil.Replace 将 tmp 原子重命名为 tombstones,保证读者永远看不到半写状态的文件。

返回值中的 int64(size) 是文件字节数,调用方(如 Block.Delete)用它更新 numBytesTombstone,从而纳入 Block.Size() 的统计(tsdb/block.go)。

读取流程:ReadTombstones 的三道校验

ReadTombstonestsdb/tombstones/tombstones.go)是加载入口,其防御顺序值得逐条对照:

  1. 文件不存在不是错误os.IsNotExist 时返回空的 NewMemTombstones()。这与"新 Block 初始可以没有删除记录"的语义一致(尽管压实器会主动写一个空文件,见 tsdb/compact.go)。
  2. 最小长度:文件短于 5 字节(头部长度)直接报 invalid size
  3. 魔数校验:读取前 4 字节大端值,不等于 0x0130BA30invalid magic number
  4. CRC 校验:对去掉尾部 4 字节 CRC 之后的内容(同样跳过首字节版本位)重新计算 CRC32-Castagnoli,与文件尾 4 字节大端比较,不一致报 checksum did not match
  5. 解码:通过校验后交给 Decode 解析成 MemTombstones

配套的往返测试 TestWriteAndReadbackTombstonestsdb/tombstones/tombstones_test.go)随机生成 100 组、每组最多 5 个删除区间,写盘再读回后断言两个 Reader 内容完全相等,验证了编码-解码的对称性。

MemTombstones:内存态的区间集合语义

MemTombstonestsdb/tombstones/tombstones.go)是磁盘格式与运行时之间的内存表示:map[storage.SeriesRef]Intervals 加一把 sync.RWMutex。几个实现细节与查询正确性直接相关:

  • Get 返回副本:为避免并发写导致的数据竞争,Get 会拷贝区间切片再返回(tsdb/tombstones/tombstones.go),测试 TestTombstonesGetWithCopy 专门验证了"对副本做 Add 会就地修改副本、而再次 Get 仍拿到原始内容"。
  • 区间为闭区间Interval.InBoundst >= Mint && t <= Maxttsdb/tombstones/tombstones.go),即删除端点时刻的样本也会被覆盖。
  • Add 自动合并重叠与相邻区间Intervals.Addtsdb/tombstones/tombstones.go)对有序区间做二分查找,把与新区间重叠的段合并;由于时间戳是离散整数,相邻仅差 1 的区间也可以合并成一段,并对 MinInt64 / MaxInt64 边界做了溢出保护。这保证了落盘前区间集合始终"递增且不重叠",编码结果紧凑。
  • TruncateBefore:删除 Maxt < beforeT 的历史区间,用于 Head 截断时同步清理过期的删除标记(行为用例见 tombstones_test.goTestTruncateBefore)。

一次端到端删除:从 API 到磁盘文件

把上述环节串起来,一次 delete_series 请求的落盘路径是:

  1. API 层收到 POST/PUT /admin/tsdb/delete_seriesweb/api/v1/api.go),携带 startend 时间参数与标签匹配器;
  2. DB.Delete 持有 compaction 互斥锁,选出与 [mint, maxt] 重叠的 Block,并行执行 Block.Deletetsdb/db.go);
  3. 每个 Block.Delete 从索引取匹配序列,仅当序列确实存在落在删除窗口内的 chunk 时才加入删除区间(用 clampInterval 把窗口限制在该序列实际数据范围内,见 tsdb/block.go),再叠加该 Block 已有的 tombstones,整体写入新的 tombstones 文件并刷新 meta.json
  4. 后续 CleanTombstones 压实(tsdb/db.go)会重写含 tombstone 的 Block,真正从新 Block 中移除被删数据——这也是"删除只保证逐块原子"的原因:重写完成前,旧 Block 与 tombstones 文件共同承担过滤职责。

小结

tombstones 文件是 TSDB"软删除"策略的落盘载体:5 字节头(魔数 0x0130BA30 + 版本 1)、变长 Stones 区(uvarint64 ref + varint64 mint + varint64 maxt 三元组平铺)、尾部 4 字节 CRC32-Castagnoli 校验,配合 tmp 文件加原子重命名的写入方式,构成一个自描述、可校验、读者永远安全的最小化格式。若你要对 Block 做外部工具处理,建议直接复用 tsdb/tombstones/tombstones.go 中的 Encode / Decode 约定;相关行为约束也可以从 tombstones_test.go 的往返、删除、截断测试中获得参考。

登录后查看全文
热门项目推荐
相关项目推荐