文件格式

TsFile 按列保存时序数据。写入时,数据按设备分组依次追加,索引则放在文件尾部。 读取时通常反过来:先从文件尾找到索引,定位目标时间序列,再跳转到保存数据的 Chunk。

本文介绍版本字节为 0x04 的 TsFile v4 布局,说明各结构的存储方式及相互关系。 编码章节列出了编码编号,并展开 PLAIN 的字节布局;其他值编码算法的完整位布局不在 本文范围内。

范围与术语

文件中的数据分为三个层级:

  • Page 保存一组编码后的值,是独立压缩的单位。
  • Chunk 保存某个物理列的所有 Page,头部记录数据类型、编码和压缩方式。
  • Chunk Group 保存同一设备的一组 Chunk。随着数据分批刷出,一个设备可以在文件中 出现多次,对应多个 Chunk Group。

非对齐数据的每个测点列各自保存时间戳和值。对齐数据把共享的时间戳放在 Time Chunk 中,各测点列分别放在 Value Chunk 中,再用位图记录哪些位置有值。 两种布局的细节见数据页。

物理区域

文件以六字节魔数 TsFile 和一字节版本号开头。Page AEAD 文件随后还有一个可选的 文件加密头。数据区由唯一的 0x02 分隔符结束,之后是索引和文件元数据。 最后十个字节保存元数据长度和尾部魔数。

下表使用以下记号:

  • F:文件总长度。
  • L:TsFileMetadata 的序列化长度,由文件尾的四字节长度字段保存。
  • E:可选文件加密头的总序列化长度;不存在时为 0。
  • data_start = 7 + E:数据区的起点。
  • meta_offset:分隔符的偏移量,保存在 TsFileMetadata 中。
区域包含首尾的字节区间定位方式
头部魔数0 … 5固定位置
版本6固定位置
可选文件加密头7 … 6 + E仅 Page AEAD 存在,marker 为 0x06
数据区data_start … meta_offset - 1在分隔符前结束
分隔符meta_offset值为 0x02,由 TsFileMetadata 引用
索引元数据meta_offset + 1 … F - 11 - L由元数据偏移划分
TsFileMetadataF - 10 - L … F - 11根据文件尾长度反向定位
元数据长度F - 10 … F - 7相对文件尾的固定位置
尾部魔数F - 6 … F - 1相对文件尾的固定位置

偏移量从文件的第一个字节开始计算。上表的区间包含首尾;本文其余位置使用的 [start, end) 表示左闭右开区间。除非另有说明,长度不包含长度字段自身。E = 0 时,文件加密头对应 的区间不存在。

索引元数据区中的 Timeseries Metadata 和索引节点可以交错存放,读取时应跟随偏移量, 不依赖它们的物理排列顺序。TsFileMetadata 自身没有标记,需要通过文件尾保存的长度 反向定位。

写入模型

写入器先追加各个 Chunk Group,结束文件时再写索引和文件尾。Page 大小、刷出阈值和 索引扇出会影响结构边界,但这些边界都能从头部和偏移量中读出。时间单位和非对齐时间 编码仍需读写双方约定,详见配置。

头部和尾部魔数使用相同的字节:

54 73 46 69 6c 65    # "TsFile"

元数据优先读取算法

读取器无需扫描整个数据区,就能定位目标序列:

  1. 校验头部魔数和版本,检查偏移 7 处是否有文件加密头, 据此确定 Chunk Header 和 Page 载荷的解析方式。
  2. 校验尾部魔数,在 F - 10 处读取 L,计算 file_metadata_pos = F - 10 - L。
  3. 读取 TsFileMetadata,检查 meta_offset 是否指向分隔符。
  4. 可以先用完整路径查询布隆过滤器。只有确认字符集、路径构造规则和 哈希行为与写入端一致时,才能据此判断路径不存在。
  5. 依次查找表/设备索引、测量索引,定位目标序列的元数据。
  6. 根据序列和 Chunk 的统计信息选择 Chunk,跳转后再用可用的 Page 统计信息缩小范围。
  7. 读取选中 Page 的载荷,按需解密,然后解压和解码。

这样可以避免读取无关 Page 的数据。单 Page Chunk 不保存 Page Statistics,筛选时要使用 Chunk 或序列级的统计信息。

结构不变量

读取时需要检查的主要关系包括:

  • 头尾魔数和版本有效。
  • 可选文件加密头只在偏移 7 处出现一次,且长度和版本有效。
  • 0 <= L <= F - 10,且 file_metadata_pos 位于分隔符之后。
  • 一个 Chunk 的全部 Page 恰好占据完整 Chunk Header 之后的 data_size 字节。
  • 每个非空 Page 的载荷恰好占据 compressed_size 字节,且没有越过 Chunk 边界。
  • 同一索引节点内的偏移量单调不减,且不超过 end_offset。
  • 对齐 Value Page 和相同序号的 Time Page 覆盖同一组位置,且 position_count 等于该 Time Page 的位置数。

不满足这些关系即为格式错误。

通用的长度和偏移量检查见二进制表示,Page AEAD 额外要求的 认证检查见加密。标记字节只在结构边界处有含义,不能把 Page 载荷中碰巧相同的字节当成标记。

阅读顺序与相关章节

初次阅读可以先看二进制表示、Chunk 与 Chunk Group 和数据页,再通过元数据了解索引查找过程。