Lance v2 实际数据文件的物理二进制格式详解
(基于 file2.proto 的逻辑定义 + 底层实现规范,落地到磁盘上的字节排列)
一、文件写入的核心逻辑
Lance v2 是流式写入的列式格式,严格遵循「先写数据,再写元数据,最后写索引」的顺序:
写入顺序:Data Buffers → Column Metadatas → CMO Table → GBO Table → Footer(最后写入,因为需要知道所有内容的偏移)
读取时则反过来:从文件尾部读 Footer,拿到所有索引,再随机访问目标数据。
┌──────────────────────┐
│ Data Pages │ ← 实际列数据
├──────────────────────┤
│ Column Metadatas │ ← 每列的 ColumnMetadata(CMO 索引)
├──────────────────────┤
│ CMO Table │ ← 列元数据索引表(B 偏移定位)
├──────────────────────┤
│ GBO Table │ ← 全局缓冲区索引表(C 偏移定位)★
├──────────────────────┤
│ Footer │ ← 记录 GBO 的 C 偏移 + 全局缓冲区数量
└──────────────────────┘
二、文件各区域的物理格式(从头到尾)
1. 数据缓冲区区(Data Pages)
物理布局:
- 存储所有列的实际数据内容(裸字节,无类型信息,由上层解码)
-
必须对齐:支持两种对齐模式(由 writer 配置)
-
64字节对齐:适配 SIMD 指令(加速列式计算) -
4096字节对齐:适配 Direct I/O(绕过页缓存,加速随机读写)
-
- 每个数据缓冲区之间插入填充字节(0x00) 满足对齐要求
实际内容:
底层兼容 Apache Arrow 内存布局,例如:
- 数值列:直接存 Arrow 的
Fixed-width Buffer(如 int64 是每元素8字节,小端) - 字符串列:存 Arrow 的
Offset Buffer(偏移数组)+Data Buffer(字符串字节)+Validity Buffer(有效性位图) - 缓冲区数量由列的编码方式决定(如字典编码需要「字典缓冲区 + 索引缓冲区」)
2. 列元数据区(Column Metadatas)
物理布局:
- 每一列的元数据是一个独立的 Protobuf 编码字节串(对应
ColumnMetadata消息) - 每个列元数据块必须4096字节扇区对齐(带
*标记,支持 Direct I/O) - 列元数据之间插入填充字节(0x00)满足对齐
每个列元数据的内容(Protobuf 编码后的字节):
- 包含该列的
Encoding(列级编码)、Pages[](所有页面的元数据)、Buffer Offsets/Sizes(列级缓冲区位置)
3. CMO 表(Column Metadata Offset Table)
物理布局:
- 固定格式的二进制表(不是 Protobuf,避免解码开销)
- 每个条目是 2个 u64(小端字节序):
字节数 类型(小端) 含义 8 u64 该列元数据的文件起始偏移(Position)(必须4096对齐) 8 u64 该列元数据的字节大小(Size) - 条目数 = 列数(CN),总大小 =
CN * 16 字节 - 无需额外对齐(内容是小端整数,不需要 SIMD/Direct I/O)
4. GBO 表(Global Buffers Offset Table)
物理布局:
- 与 CMO 表结构完全一致(固定格式二进制表)
- 每个条目是 2个 u64(小端字节序):
字节数 类型(小端) 含义 8 u64 全局缓冲区的文件起始偏移(Position)(必须4096对齐) 8 u64 全局缓冲区的字节大小(Size) - 条目数 = 全局缓冲区数(GN),总大小 =
GN * 16 字节
5. Footer(文件尾,核心索引)
物理布局(固定40字节,写在文件最末尾):
从文件最后40字节开始读取,小端字节序,结构如下(按字节位置从后往前,因为Footer在末尾):
| 字节偏移(相对文件末尾) | 字节数 | 类型(小端) | 含义 |
|---|---|---|---|
| -40 到 -32 | 8 | u64 | 全局缓冲区表(GBO)的起始偏移(C) |
| -32 到 -24 | 8 | u64 | 列元数据表(CMO)的起始偏移(B) |
| -24 到 -16 | 8 | u64 | 第0列元数据的起始偏移(A) |
| -16 到 -12 | 4 | u32 | 全局缓冲区数量(GN) |
| -12 到 -8 | 4 | u32 | 列数量(CN) |
| -8 到 -6 | 2 | u16 | 次版本号(Minor Version,当前为0) |
| -6 到 -4 | 2 | u16 | 主版本号(Major Version,当前为2) |
| -4 到 0 | 4 | u32 |
魔数(Magic Number):0x434E414C(ASCII 字符串 "LANC" 的小端存储) |
魔数校验:Reader 读取文件后,先检查最后4字节是否为
0x434E414C,若是则为有效 Lance v2 文件。
三、具体文件的字节布局示例
假设一个 Lance v2 文件有 2列、1个全局缓冲区,对齐方式为 4096字节:
| 字节偏移(相对文件头) | 内容 | 大小 | 对齐说明 |
|---|---|---|---|
| 0x0000 | Data Buffer 0(列0 Page 0 数据) | 1000字节 | 起始在0,已对齐 |
| 0x03E8 | 填充字节(0x00) | 3096字节 | 补齐到4096的倍数(0x1000) |
| 0x1000 | Data Buffer 1(列0 Page 1 数据) | 500字节 | 起始在0x1000,已对齐 |
| 0x11F4 | 填充字节(0x00) | 3596字节 | 补齐到0x2000 |
| 0x2000 | Data Buffer 2(列1 Page 0 数据) | 800字节 | 起始在0x2000,已对齐 |
| 0x2320 | 填充字节(0x00) | 3296字节 | 补齐到0x3000 |
| 0x3000 | Global Buffer 0(共享编码) | 2000字节 | 起始在0x3000,已对齐 |
| 0x37D0 | 填充字节(0x00) | 2296字节 | 补齐到0x4000 |
| 0x4000 | Column 0 Metadata(Protobuf 编码) | 500字节 | 起始在0x4000,已对齐 |
| 0x41F4 | 填充字节(0x00) | 3596字节 | 补齐到0x5000 |
| 0x5000 | Column 1 Metadata(Protobuf 编码) | 600字节 | 起始在0x5000,已对齐 |
| 0x5258 | 填充字节(0x00) | 3496字节 | 补齐到0x6000 |
| 0x6000 | CMO 表(2列×16字节) | 32字节 | 无需对齐 |
| 0x6020 | GBO 表(1个全局缓冲区×16字节) | 16字节 | 无需对齐 |
| 0x6030 | Footer(固定40字节) | 40字节 | 无需对齐 |
四、读写流程的物理实现
写入流程(Writer):
- 流式写入所有列的
Data Buffer,每写一个前补齐4096对齐 - 写入所有列的
Column Metadata(Protobuf 编码),每列前补齐4096对齐 - 写入
CMO 表(遍历列元数据的偏移和大小) - 写入
GBO 表(遍历全局缓冲区的偏移和大小) - 计算 Footer 的 A/B/C 偏移、CN/GN、版本号,写入文件最后40字节
读取流程(Reader):
- 随机读取文件最后40字节,校验魔数,解析 Footer
- 根据 Footer 的
B偏移读取 CMO 表(CN*16字节) - 根据 Footer 的
C偏移读取 GBO 表(GN*16字节) - 根据 CMO 表的条目,随机读取指定列的 Column Metadata(Protobuf 解码)
- 根据 Column Metadata 的
Pages[].Buffer Offsets,随机读取目标数据缓冲区 - 若需全局数据(如 Schema/共享编码),根据 GBO 表的条目读取对应缓冲区
五、设计亮点(物理层面)
- 随机访问优化:Footer 在末尾,Reader 无需顺序扫描,直接从尾部拿索引,再随机访问目标数据,完美适配列式存储的「按需读取」需求
- 对齐兼容:64/4096字节对齐的设计,既支持 SIMD 加速计算,又支持 Direct I/O 绕过页缓存,适配不同硬件环境
- 无锁写入:写入是流式的,只有最后写 Footer,不需要前向修改,支持高吞吐写入
- 极简解析:Footer/CMO/GBO 都是固定格式的小端整数,解析速度极快,无 Protobuf 解码开销(Protobuf 仅用于列元数据的灵活结构)