Prometheus TSDB Chunks 磁盘格式全解:从 segment 文件到 XOR/XOR2/Histogram 编码的源码级剖析
TSDB block 中 chunks/ 目录下的数据文件承载着所有时序样本的原始字节,是 Prometheus 存储格式中最底层的一环。本文以仓库中的格式规范 tsdb/docs/format/chunks.md 为主体,完整讲解 chunk segment 文件的头部结构、单条 chunk 的 len + encoding + data + CRC-32C 布局、六种 chunk 编码(XOR、XOR2、histogram、floathistogram 及其 ST 变体)的逐字段位级格式,并结合 chunkenc 源码 与 chunks 读写实现 说明每个字段在代码中如何被写入和校验,帮助读者建立从磁盘字节到查询引擎的完整认知。
1. Chunks segment 文件:头部与引用机制
每个 block 在 chunks/ 目录下存放一组顺序编号的 segment 文件(000000、000001……),单个 segment 文件最大 512MiB。该值对应源码常量 DefaultChunkSegmentSize:
const (
// DefaultChunkSegmentSize is the default chunks segment size.
DefaultChunkSegmentSize = 512 * 1024 * 1024
)
segment 文件头部固定为 8 字节,格式文档中给出的布局与 cutSegmentFile 的写入逻辑一一对应:
┌──────────────────────────────┐
│ magic(0x85BD40DD) <4 byte> │
├──────────────────────────────┤
│ version(1) <1 byte> │
├──────────────────────────────┤
│ padding(0) <3 byte> │
├──────────────────────────────┤
│ ┌──────────────────────────┐ │
│ │ Chunk 1 │ │
│ ├──────────────────────────┤ │
│ │ ... │ │
│ ├──────────────────────────┤ │
│ │ Chunk N │ │
│ └──────────────────────────┘ │
└──────────────────────────────┘
对应源码常量定义在 chunks.go:
// MagicChunks is 4 bytes at the head of a series file.
MagicChunks = 0x85BD40DD
MagicChunksSize = 4
chunksFormatV1 = 1
ChunksFormatVersionSize = 1
segmentHeaderPaddingSize = 3
// SegmentHeaderSize defines the total size of the header part.
SegmentHeaderSize = MagicChunksSize + ChunksFormatVersionSize + segmentHeaderPaddingSize
写入时先预分配(preallocate)segmentSize 大小的文件、同步目录,再写入 magic + version 头部;关闭时把预分配的多余零字节截断(见 finalizeTail 与 cut)。读取侧 newReader 会逐段校验 magic 与版本号,任何不符都会直接报错,这是防止误读非 chunk 文件的第一道防线。
1.1 chunk 的引用方式:segment 序号 + 文件内偏移
格式文档指出:index 中对 chunk 的引用是一个 uint64,低 4 字节为文件内偏移(offset),高 4 字节为 segment 序号(sequence number)。源码中这一契约由 BlockChunkRef 精确表达:
// BlockChunkRef refers to a chunk within a persisted block.
// The upper 4 bytes are for the segment index and
// the lower 4 bytes are for the segment offset where the data starts for this chunk.
type BlockChunkRef uint64
func NewBlockChunkRef(fileIndex, fileOffset uint64) BlockChunkRef {
return BlockChunkRef(fileIndex<<32 | fileOffset)
}
写入 chunk 时,writeChunks 会为每条记录回填这个 ref:chk.Ref = ChunkRef(NewBlockChunkRef(seq, uint64(w.n))),其中 seq 是当前 segment 序号、w.n 是已写入的字节数。
2. 单条 chunk 的磁盘布局:len、encoding、data、checksum
文件内的每条 chunk 按如下结构排列:
┌───────────────┬───────────────────┬─────────────┬───────────────────┐
│ len <uvarint> │ encoding <1 byte> │ data <data> │ checksum <4 byte> │
└───────────────┴───────────────────┴─────────────┴───────────────────┘
各字段的含义与源码依据:
-
len(1~5 字节):chunk data 的字节长度,使用 unsigned varint 编码(binary.PutUvarint),最大 5 字节,对应常量MaxChunkLengthFieldSize = binary.MaxVarintLen32(见 chunks.go)。 -
encoding(1 字节):编码类型标识。当前取值与 chunk.go 中的枚举一致:const ( EncNone Encoding = iota EncXOR EncHistogram EncFloatHistogram EncXOR2 EncHistogramST EncFloatHistogramST )其中
XOR2由配置项storage.tsdb.chunk_encoding.floats选择;histogramST与floathistogramST则由实验性的histograms-st-encodingfeature flag 控制(见 feature flags 文档)。注意 CompatibleValues 表明 XOR 与 XOR2 同属一个"值编码家族",已打开的 chunk 在配置切换时可以继续追加,新编码只在新 chunk 生效。 -
data:具体编码见后文各节。 -
checksum(4 字节):对encoding与data计算的 CRC-32C(Castagnoli 多项式),序列化为无符号 32 位大端整数。
CRC-32C 在源码中通过共享的 castagnoliTable 初始化(chunks.go),写入时由 Meta.writeHash 把 encoding 字节 + 原始 data 送入哈希;读取时 checkCRC32 重算并比较,不匹配则返回 checksum mismatch 错误。Reader.ChunkOrIterable 的完整读取路径是:按 ref 解出 segment 与偏移 → 读 uvarint 长度 → 定位 data 边界 → 先校验 CRC 再从 chunkenc.Pool 取出对应编码的 chunk 对象,任何越界或校验失败都不会污染结果。
3. XOR chunk data:浮点样本的位级编码
┌──────────────────────┬───────────────┬───────────────┬──────────────────────┬──────────────────────┬──────────────────────┬──────────────────────┬─────┬──────────────────────┬──────────────────────┬──────────────────┐
│ num_samples <uint16> │ ts_0 <varint> │ v_0 <float64> │ ts_1_delta <uvarint> │ v_1_xor <varbit_xor> │ ts_2_dod <varbit_ts> │ v_2_xor <varbit_xor> │ ... │ ts_n_dod <varbit_ts> │ v_n_xor <varbit_xor> │ padding <x bits> │
└──────────────────────┴───────────────┴───────────────┴──────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┴─────┴──────────────────────┴──────────────────────┴──────────────────┘
要点:
ts是时间戳,v是值;num_samples为大端 uint16(2 字节)。- 从样本 2 起,时间戳改用 dod(delta of deltas):
ts_n_dod = (ts_n − ts_{n-1}) − (ts_{n-1} − ts_{n-2}),用<varbit_ts>变长位宽编码(1~68 bit)。对采样间隔稳定的序列,dod 通常为 0 或 ±1,只需极少比特。 - 值采用 XOR 差值编码
<varbit_xor>(1~77 bit):对当前值与前一值的 float64 位做 XOR,再利用"leading/trailing zero 窗口"只保存有效位段。XOR 为 0(值未变)时只写 1 bit。 - 末尾
padding为 0~7 bit,使整个 chunk data 字节对齐。 - 一个 chunk 最少可以只有 1 个样本,即
ts_1、v_1及之后的字段是可选的。
从 xorAppender.Append 的实现可以看到:第一个样本写绝对时间戳与绝对值,其后维护 tDelta(时间差)、v(上一值)与 leading/trailing(XOR 窗口)三个状态量,writeVDelta 则负责把 XOR 结果按窗口压缩写流。配套的容量约束在 chunk.go:XOR chunk 目标大小 1024 字节(MaxBytesPerXORChunk),切 chunk 时按最坏单样本 19 字节预留(MaxBytesPerXORChunkBeforeAppend)。
4. XOR2 chunk data:联合控制位与可选 Start Timestamp
XOR2 是 XOR 的增强编码:样本 0、1 的写法与 XOR 相同,从样本 2 起用一个联合控制前缀(joint control prefix)同时编码时间戳 dod 与值是否变化,并把常见的 dod 情况做成字节对齐,提升写入效率。它还可选地编码 Start Timestamp(ST)(例如 remote write 传递的 counter 起点时间)。
┌──────────────────────┬───────────────────┬───────────────┬───────────────┬────────────────┐
│ num_samples <uint16> │ st_header <uint8> │ ts_0 <varint> │ v_0 <float64> │ ?st_0 <varint> │
└──────────────────────┴───────────────────┴───────────────────┴─────────────────────────────┘
...
4.1 联合样本编码(n ≥ 2 时的 <joint_sample2>)
每个样本先写变长控制前缀,联合编码 dod 与值变化状态:
| 控制前缀 | dod | 后续的值编码 |
|---|---|---|
0 |
0 | (无,值未变) |
10 |
0 | <varbit_xor2_nn>(值已知非零且非 stale) |
110DDDDD + DDDDDDDD |
13 bit 有符号 [-4096, 4095] | <varbit_xor2> |
1110DDDD + DDDDDDDD + DDDDDDDD |
20 bit 有符号 [-524288, 524287] | <varbit_xor2> |
11110 + 64 bit dod |
精确值 | <varbit_xor2> |
11111 |
0 | (无值字段,stale NaN) |
110 与 1110 两种情况把前缀与 dod 的高位打包进首个字节,使整个 dod 字段字节对齐。这与 xor2Appender.encodeJoint 的分支完全一致:dod==0 时走 0/10/11111 三条短路径;|dod| ≤ 2^12−1 写 0b110 前缀加 2 字节;|dod| ≤ 2^19−1 写 0b1110 前缀加 3 字节;其余落入 11110 + 64 bit 逃逸路径。随后再按值是否变化决定是否跟随 <varbit_xor2>。
4.2 值差值编码 <varbit_xor2>
用于 dod≠0 的控制前缀之后,对当前值与前一值的 XOR 结果编码:
| 前缀 | 含义 |
|---|---|
0 |
XOR = 0(值未变) |
10 |
复用上一 leading/trailing 窗口;其后跟 sigbits 个值位 |
110 + leading(5) + sigbits(6) + value(sigbits) |
新的 leading/trailing 窗口 |
111 |
stale NaN 标记(3 bit) |
当控制前缀为 10(dod=0 且已知值已变化、非 stale)时,跳过 delta=0 检查,改用省 1 bit 的 <varbit_xor2_nn>:
| 前缀 | 含义 |
|---|---|
0 |
复用上一 leading/trailing 窗口;其后跟 sigbits 个值位 |
1 + leading(5) + sigbits(6) + value(sigbits) |
新的 leading/trailing 窗口 |
对应实现分别是 writeVDelta 与 writeVDeltaKnownNonZero:两者都维护 leading/trailing 窗口,窗口内可容纳新的 XOR 值时用 2 bit 前缀复用,否则写 3 bit 前缀加 5 bit leading、6 bit sigbits 与新值。
4.3 Start Timestamp(ST)编码
XOR2 与所有 histogram ST 编码共享同一套 ST 方案:
-
st_header为 1 字节:┌───────────────────────┬───────────────────────┐ │ first_st_known<1 bit> | st_changed_on<7 bits> │ └───────────────────────┴───────────────────────┘最高位
first_st_known表示st_0是否存在;低 7 位st_changed_on为 0 表示没有任何st_i (i>0),否则st_i (i≥st_changed_on)存在、st_i (0<i<st_changed_on)不存在。受 7 bit 限制,chunk 达到 127 个样本时st_changed_on固定取 127,从第 127/128 个样本起逐样本携带 ST。 -
st_0存在时以varint编码;st_1是相对st_0(或 0)的varbit_ts/varbit_int差值;st_i (i>1)是"差值的差值"(dod)编码。
源码中该 header 的读写集中在 st.go:writeHeaderFirstSTKnown / writeHeaderFirstSTChangeOn / readSTHeader 操作首字节,stEncoder.encode 负责逐样本的 ST 差值/dod 写入,XOR2 追加样本时在 xor2Appender.Append 中把首次 ST 变化位置回写进 header。
5. Histogram chunk data:直方图样本的位级编码
histogram 编码面向 native histogram(整数计数直方图)样本:
┌──────────────────────┬──────────────────────────┬───────────────────────────────┬─────────────────────┬──────────────────┬──────────────────┬──────────────────────┬────────────────┬──────────────────┐
│ num_samples <uint16> │ histogram_flags <1 byte> │ zero_threshold <1 or 9 bytes> │ schema <varbit_int> │ pos_spans <data> │ neg_spans <data> │ custom_values <data> │ samples <data> │ padding <x bits> │
└──────────────────────┴──────────────────────────┴───────────────────────────────┴─────────────────────┴──────────────────┴──────────────────┴──────────────────────┴────────────────┴──────────────────┘
5.1 正/负 spans 与 custom values
spans 数据以 num_spans 开头,其后交替排列 length_i <varbit_uint> 与 offset_i <varbit_int>,描述各 bucket 在样本中的位置。custom_values 目前只用于 schema −53(自定义 bucket 边界),其他 schema 下长度为 0:
┌──────────────────────────┬──────────────────┬──────────────────┬─────┬──────────────────┐
│ num_values <varbit_uint> │ value_0 <custom> │ value_1 <custom> │ ... │ value_n <custom> │
└──────────────────────────┴──────────────────┴──────────────────┴─────┴──────────────────┘
<custom> 的编码针对"人为设定的十进制小数边界"做了优化:先取 y = x × 1000,若 0 ≤ y ≤ 33554430 且为整数,则存 y + 1 的 <varbit_uint>(永远以 1 bit 开头);否则存一个 0 bit 加 64 bit 原始 float64(以 0 bit 开头)。上限 33554430 保证 varbit 结果不超过 4 字节,解码端凭首 bit 区分两种情况。
5.2 样本数据:绝对值、差值、dod 三段式
直方图样本编码同样是三段式:
- Sample 0:
ts <varbit_int>、count <varbit_uint>、zero_count <varbit_uint>、sum <float64>,随后是pos_bucket_0 <varbit_int> … pos_bucket_n、neg_bucket_0 <varbit_int> … neg_bucket_n; - Sample 1:
ts_delta、count_delta、zero_count_delta(均为<varbit_int>)加sum_xor <varbit_xor>,各 bucket 为差值; - Sample 2 及以后:
ts_dod、count_dod、zero_count_dod(dod 化<varbit_int>)加sum_xor,各 bucket 为 dod。
关键注记(与文档 Notes 一致):
histogram_flags当前只用前 2 bit:10表示相对上一 chunk 发生 counter reset;01表示无 reset;00表示状态未知;11表示这是 gauge histogram 的 chunk,不存在 counter reset。zero_threshold有专门编码:为 0 时仅 1 个零字节;若是 2⁻²⁴³ 到 2¹⁰ 之间的 2 的幂,则为 1~254 之间的单字节;否则写全 1 字节(255)后跟 8 字节 float64,共 9 字节。schema是 exposition format 定义的特定值:标准指数 schema 取−4 ≤ n ≤ 8,或−53(自定义 bucket 边界)。- bucket 天然是相对前一 bucket 的差值,只有
bucket_0是绝对计数。 - chunk 最少 1 个样本;spans 与 buckets 都可以少到 0 个。
容量策略与 XOR 类似:chunk.go 中 TargetBytesPerHistogramChunk = 1024,并规定 MinSamplesPerHistogramChunk = 10——因为单个直方图样本可能超过 1024 字节,仍要保住最小样本数以获得压缩收益。
5.3 变长位宽原语 <varbit_int> / <varbit_uint>
histogram 编码大量依赖 varbit.go 中的位桶化编码,两者桶宽相同:
| 前缀 | 取值范围(int / uint) | 总位数 |
|---|---|---|
0 |
精确 0 | 1 bit |
10 |
int:−3~4;uint:0~7 | 5 bit |
110 |
int:−31~32;uint:0~63 | 9 bit |
1110 |
int:−255~256;uint:0~511 | 13 bit |
11110 |
int:−2047~2048;uint:0~4095 | 17 bit |
111110 |
int:±131071;uint:0~262143 | 24 bit(3 字节) |
1111110 |
int:±16777215;uint:0~33554431 | 32 bit(4 字节) |
11111110 |
int:±36028797018963967;uint:0~2⁵⁶−1 | 64 bit(8 字节) |
11111111 |
完整 64 bit 逃逸 | 72 bit(9 字节,最坏情况) |
putVarbitInt 的注释说明了分桶的取向:按实际观测到的 bucket dod 分布优化,各分支不必覆盖前序分支的值域,理论上还能再省约 1% 空间,但当前取舍是更低的编解码开销(见 putVarbitInt)。<varbit_ts>(XOR/XOR2 的时间戳 dod,1~68 bit)与 <varbit_xor>(XOR 值差值,1~77 bit)也是同族的按位宽分桶编码,分别在 xor.go 与 histogram.go 中实现。
6. Histogram ST / Float histogram / Float histogram ST
6.1 Histogram ST chunk
histogram ST 编码在 histogram 格式之上追加可选 ST 数据,采用与 XOR2 相同的 ST 方案。样本编码与 histogram chunk 完全相同,但 3 字节头部重新排布:counter-reset 标志移入样本计数字节的最高 2 bit,腾出的字节 2 存放 st_header:
┌────────────────────────┬───────────────────────┬────────────────────┬───────────────────────────────┬─────────────────────┬──────────────────┬──────────────────┬──────────────────────┬────────────────┬──────────────────┐
│ counter_reset <2 bits> │ num_samples <14 bits> │ st_header <1 byte> │ zero_threshold <1 or 9 bytes> │ schema <varbit_int> │ pos_spans <data> │ neg_spans <data> │ custom_values <data> │ samples <data> │ padding <x bits> │
└────────────────────────┴───────────────────────┴────────────────────┴───────────────────────────────┴─────────────────────┴──────────────────┴──────────────────┴──────────────────────┴────────────────┴──────────────────┘
ST 部分规则与 4.3 节一致,差异在于 st_1 及之后的字段用 <varbit_int>(histogram ST 中样本数被压缩为 14 bit,故 st_changed_on 达到上限 127 时从第 128 个样本起存在 ST)。每个 sample_i <data> 之后紧跟可选的 ?st_i 字段,ST 缺失时不占空间。实现位于 histogram_st.go,编码选择逻辑见 ValueType.ChunkEncoding:useHistogramST 为真时返回 EncHistogramST / EncFloatHistogramST。
6.2 Float histogram chunk
浮点计数直方图(floathistogram)整体布局与 histogram 相同,只有样本内编码不同——count、zero_count 与所有 bucket 都是浮点数:
- Sample 0:
ts <varbit_int>+count <float64>、zero_count <float64>、sum <float64>、各 bucket 均为绝对<float64>; - Sample 1:
ts_delta <varbit_int>,其后count_xor、zero_count_xor、sum_xor与各 bucket 均为<varbit_xor>; - Sample 2 及以后:时间戳改
ts_dod,其余字段仍是<varbit_xor>。
即浮点字段不再做逐 bucket 差值/dod,而是统一用 XOR 窗口压缩,实现见 float_histogram.go。
6.3 Float histogram ST chunk
在 floathistogram 之上叠加可选 ST,3 字节头部布局与 Histogram ST 完全一致(counter_reset 2 bit + num_samples 14 bit + st_header 1 byte),样本编码不变,可选 ST 字段及 st_header 规则同 Histogram ST 与 XOR2 ST 编码。实现见 float_histogram_st.go。
7. 从写入到读取:格式如何被代码闭环
把上述格式放回整条数据链路:
- 写入:Head 中的 chunk 落盘时由 chunks.Writer.WriteChunks 批量写入。每个 chunk 按
len(uvarint) + encoding(1B) + data + crc32(4B)序列化,长度字段按 5 字节上限预占;当累计大小加上新 chunk 超过 segment 剩余空间时切新 segment(cutSegmentFile),并刻意避免在写入末尾产生空 segment。 - 读取:chunks.Reader 以 mmap 方式把各 segment 映射进内存,
ChunkOrIterable按 ref 精确定位、先 CRC-32C 校验再从 chunkenc.Pool 取复用对象——池按六种编码分别维护sync.Pool(xor / histogram / floatHistogram / xo2 / histogramST / floatHistogramST),显著降低解码分配开销。 - 兼容边界:
EncNone与非法编码在 IsValidEncoding 与 FromData 中被拒绝;magic/版本校验发生在 newReader。XOR→XOR2 的平滑切换由CompatibleValues保证已有 chunk 不受影响。
8. 小结
- chunk segment 文件 =
magic(0x85BD40DD) + version(1) + 3B padding+ 顺序排列的 chunk,单 segment 上限 512MiB;index 通过"高 4 字节 segment 序号 + 低 4 字节偏移"的 uint64 ref(BlockChunkRef)定位数据。 - 每条 chunk =
len(uvarint) + encoding(1B) + data + CRC-32C(4B),CRC-32C(Castagnoli)覆盖 encoding 与 data,读取前强制校验。 - 六种编码共享同一套位桶化原语(
varbit_int/uint/ts/xor):XOR/XOR2 面向浮点样本(XOR2 引入联合控制位与可选 ST),histogram 面向整数直方图(dod 三段式 + spans + 可选自定义边界),floathistogram 则把直方图字段换成 float64 + varbit_xor;三个 ST 变体(XOR2、histogramST、floathistogramST)共享st_header方案,由histograms-st-encodingfeature flag 或storage.tsdb.chunk_encoding.floats配置启用。 - 格式细节的最终裁决者是代码:编解码在 tsdb/chunkenc/(含各编码的
*_test.go往返测试),文件级读写与校验在 tsdb/chunks/(chunks.go、head_chunks.go),格式规范文档见 tsdb/docs/format/chunks.md 与 tsdb/docs/refs.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00