(2)lance 文件结构

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):

  1. 流式写入所有列的 Data Buffer,每写一个前补齐4096对齐
  2. 写入所有列的 Column Metadata(Protobuf 编码),每列前补齐4096对齐
  3. 写入 CMO 表(遍历列元数据的偏移和大小)
  4. 写入 GBO 表(遍历全局缓冲区的偏移和大小)
  5. 计算 Footer 的 A/B/C 偏移、CN/GN、版本号,写入文件最后40字节

读取流程(Reader):

  1. 随机读取文件最后40字节,校验魔数,解析 Footer
  2. 根据 Footer 的 B偏移 读取 CMO 表(CN*16字节)
  3. 根据 Footer 的 C偏移 读取 GBO 表(GN*16字节)
  4. 根据 CMO 表的条目,随机读取指定列的 Column Metadata(Protobuf 解码)
  5. 根据 Column Metadata 的 Pages[].Buffer Offsets,随机读取目标数据缓冲区
  6. 若需全局数据(如 Schema/共享编码),根据 GBO 表的条目读取对应缓冲区

五、设计亮点(物理层面)

  1. 随机访问优化:Footer 在末尾,Reader 无需顺序扫描,直接从尾部拿索引,再随机访问目标数据,完美适配列式存储的「按需读取」需求
  2. 对齐兼容:64/4096字节对齐的设计,既支持 SIMD 加速计算,又支持 Direct I/O 绕过页缓存,适配不同硬件环境
  3. 无锁写入:写入是流式的,只有最后写 Footer,不需要前向修改,支持高吞吐写入
  4. 极简解析:Footer/CMO/GBO 都是固定格式的小端整数,解析速度极快,无 Protobuf 解码开销(Protobuf 仅用于列元数据的灵活结构)
最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容