TsFile 按列保存时序数据。写入时,数据按设备分组依次追加,索引则放在文件尾部。 读取时通常反过来:先从文件尾找到索引,定位目标时间序列,再跳转到保存数据的 Chunk。
本文介绍版本字节为 0x04 的 TsFile v4 布局,说明各结构的存储方式及相互关系。 编码章节列出了编码编号,并展开 PLAIN 的字节布局;其他值编码算法的完整位布局不在 本文范围内。
文件中的数据分为三个层级:
非对齐数据的每个测点列各自保存时间戳和值。对齐数据把共享的时间戳放在 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 | 由元数据偏移划分 |
TsFileMetadata | F - 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"
读取器无需扫描整个数据区,就能定位目标序列:
7 处是否有文件加密头, 据此确定 Chunk Header 和 Page 载荷的解析方式。F - 10 处读取 L,计算 file_metadata_pos = F - 10 - L。TsFileMetadata,检查 meta_offset 是否指向分隔符。这样可以避免读取无关 Page 的数据。单 Page Chunk 不保存 Page Statistics,筛选时要使用 Chunk 或序列级的统计信息。
读取时需要检查的主要关系包括:
7 处出现一次,且长度和版本有效。0 <= L <= F - 10,且 file_metadata_pos 位于分隔符之后。data_size 字节。compressed_size 字节,且没有越过 Chunk 边界。end_offset。position_count 等于该 Time Page 的位置数。不满足这些关系即为格式错误。
通用的长度和偏移量检查见二进制表示,Page AEAD 额外要求的 认证检查见加密。标记字节只在结构边界处有含义,不能把 Page 载荷中碰巧相同的字节当成标记。
初次阅读可以先看二进制表示、Chunk 与 Chunk Group 和数据页,再通过元数据了解索引查找过程。