元数据

TsFile 中的元数据分为两类:

  • 与数据一起保存的 Chunk Header、Page Header 和 Page Statistics。
  • 数据区之后的 Timeseries Metadata、Metadata Index 和 File Metadata。

Footer 元数据从 SEPARATOR (0x02) 之后开始。TimeseriesMetadata 范围与测量索引 节点可以交错出现;索引中的 offset 和 end_offset 决定每个范围的实际边界。所有 offset 都是从文件首字节开始计算的绝对偏移量。

查找数据时,读取器沿下列层级逐步缩小范围,从表和设备定位到可能包含目标值的 Page:

层级标识对象主要用途
文件元数据表及设备索引根进入正确的索引树
设备索引设备定位一个设备的测量索引
测量索引测量名称区间定位 Timeseries Metadata
Timeseries Metadata一个逻辑序列选择 Chunk 偏移及序列 Statistics
Chunk Metadata一个物理 Chunk跳转到数据并利用 Chunk Statistics 裁剪
Page Header一个 Page利用 Page Statistics 裁剪并读取载荷

统计信息

Page、Chunk 和时间序列级 Statistics 使用相同的类型相关结构:

Statistics :=
    UVarInt  count
    int64    start_time
    int64    end_time
    ...      type-specific fields

count 是该结构覆盖的值数。非对齐序列的值数等于点数;对齐值列的 count 是非 null 值数;对齐时间列的 count 是时间戳数。

类型专有字段按以下顺序紧随公共前缀:

数据类型类型专有字段
BOOLEANbool first, bool last, int64 true_count
INT32, DATEint32 min, int32 max, int32 first, int32 last, int64 sum
INT64, TIMESTAMPint64 min, int64 max, int64 first, int64 last, float64 sum
FLOATfloat32 min, float32 max, float32 first, float32 last, float64 sum
DOUBLEfloat64 min, float64 max, float64 first, float64 last, float64 sum
TEXTBinary32 first, Binary32 last
STRINGBinary32 first, Binary32 last, Binary32 min, Binary32 max
BLOB, OBJECT, VECTOR无类型专有字段

Statistics 自身不携带数据类型编号。解析 Statistics 时,数据类型来自 Chunk Header 或 Timeseries Metadata。

first 和 last 表示值的先后顺序,不等同于最小值和最大值。对于按时间有序的序列, 它们对应覆盖区间首尾时间戳处的值。start_time 和 end_time 均包含在统计区间内。数值 sum 用于聚合;其中 INT64 的 sum 以 float64 保存,数值较大时可能无法精确表示每个 整数值。

统计信息用于排除不可能匹配的数据。例如,Page 的时间范围完全早于查询范围时,可以 跳过该 Page。范围有重叠只说明“可能匹配”,仍需读取数据检查具体值。 BLOB、OBJECT、TEXT 等没有最小/最大值字段的类型不能通过 Statistics 做值范围裁剪。

Chunk Statistics 汇总该 Chunk 的所有 Page,序列级 Statistics 再汇总该序列的所有 Chunk。父层计数是各子层计数之和,时间范围也应覆盖各子层的时间范围。

时间序列元数据

每条时间序列对应一个 TimeseriesMetadata:

TimeseriesMetadata :=
    byte        timeseries_type
    VarString   measurement_id
    byte        data_type
    UVarInt     chunk_metadata_list_size
    Statistics  series_statistics
    byte        chunk_metadata_list[chunk_metadata_list_size]

timeseries_type 的高位表示列角色,低 6 位表示 Chunk Metadata 的布局:

bit 7 = 1: aligned time column
bit 6 = 1: aligned value column
low 6 bits = 0: exactly one Chunk; ChunkMetadata omits Statistics
low 6 bits = 1: more than one Chunk; every ChunkMetadata contains Statistics

Chunk 元数据

当 (timeseries_type & 0x3f) == 0 时,时间序列只有一个 Chunk:

ChunkMetadata := int64 offset_of_chunk_header

该 Chunk 的 Statistics 直接使用 series_statistics。

当 (timeseries_type & 0x3f) != 0 时:

ChunkMetadata :=
    int64      offset_of_chunk_header
    Statistics chunk_statistics

offset_of_chunk_header 指向 Chunk marker,即 Chunk Header 的第一个字节。

chunk_metadata_list_size 保存列表的序列化字节数,不是 Chunk 数量。将它加到列表起点 即可得到结束位置,然后逐条读取 Chunk Metadata,直到恰好到达该位置。

元数据索引

MetadataIndexNode :=
    UVarInt          child_count
    MetadataEntry    child[child_count]
    int64            end_offset
    byte             node_type

node_type 决定节点层级、Entry 的编码和 Entry 指向的数据:

值名称Entry 编码Entry 指向
0INTERNAL_DEVICEDeviceIndexEntry子设备索引节点
1LEAF_DEVICEDeviceIndexEntry设备的测量索引根节点
2INTERNAL_MEASUREMENTMeasurementIndexEntry子测量索引节点
3LEAF_MEASUREMENTMeasurementIndexEntry一段 Timeseries Metadata

Entry 格式为:

DeviceIndexEntry :=
    DeviceID  first_device_id
    int64     offset

MeasurementIndexEntry :=
    VarString first_measurement_id
    int64     offset

每个 Entry 描述半开字节区间 [offset, next_offset)。next_offset 取下一个 Entry 的 偏移量;最后一个 Entry 则取节点的 end_offset。结束偏移处的字节属于下一个区间。

entry[0].offset <= entry[1].offset <= ... <= end_offset

Entry 的 key 是其覆盖范围内的第一个 key。设备索引按 DeviceID 片段逐段排序,null 小于非 null;短元组在公共前缀相同时排在长元组之前。测量索引按测量名词典序排序。

索引查找

在索引节点中查找键 K 时,选择 first key 小于或等于 K 的最后一个 Entry。所选字节 区间从该 Entry 的 offset 开始,在下一 Entry 的 offset 处结束;最后一个 Entry 则在节点 end_offset 处结束。内部节点区间包含下一级索引节点,叶子区间包含上表中对应的数据对象。

如果没有 Entry 的键小于或等于 K,该节点中就不存在目标键。测量索引的一个叶子区间 可以包含多条 Timeseries Metadata,需要在该区间中继续查找完整的测量名。

沿索引继续读取前,检查:

  • child_count 与节点内完整 Entry 数量一致。
  • Entry key 按该节点的比较规则有序。
  • Entry offset 单调不减,每个引用区间长度非负。
  • 最后一个 Entry offset 不超过 end_offset。
  • 子节点层级与父节点的 node type 相符。

first-key 表示允许实现在节点内部对 Entry 使用二分查找,但文件格式不规定具体内存搜索 算法。

文件元数据

TsFileMetadata 位于文件尾部长度字段之前:

TsFileMetadata :=
    UVarInt          table_index_count
    TableIndexRoot   table_index[table_index_count]
    UVarInt          table_schema_count
    NamedTableSchema table_schema[table_schema_count]
    int64            meta_offset
    BloomFilter      bloom_filter
    VarInt           property_count
    Property         property[property_count]

TableIndexRoot :=
    VarString         table_name
    MetadataIndexNode device_index_root

NamedTableSchema :=
    VarString    table_name
    TableSchema  schema

Property :=
    VarString key
    VarBinary value

属性键是 UTF-8 字符串,值是可空的原始字节数组,不要求为 UTF-8。例如,值字节 ff fe 00 80 序列化为 08 ff fe 00 80,其中 08 是 VarInt(4)。null 值为 01(VarInt(-1)),空值为 00。未知属性值必须逐字节保留,仅在具体属性约定要求时 才按文本解码。

meta_offset 指向数据区末尾的 0x02 separator。TsFileMetadata 前没有 marker, 其起始位置由文件尾保存的 4 字节长度确定。

Map 项的物理顺序不参与文件语义。table_index 保存树模型和表模型的设备索引根; table_schema 保存显式表模型的 schema。

文件尾四字节元数据长度位于 TsFileMetadata 之外。设文件长度为 F、元数据长度为 L, 读取器使用下式得到精确字节区间:

file_metadata_start = F - 6 - 4 - L
file_metadata_end   = F - 6 - 4       # 半开区间结束位置

读取器必须恰好消费 L 字节。该检查可以在接受尾部魔数之前发现 Schema 不匹配或变长字段 损坏。

表 Schema

TableSchema :=
    UVarInt      column_count
    TableColumn  column[column_count]

TableColumn :=
    MeasurementSchema schema
    int32             column_category

MeasurementSchema :=
    String32       column_name
    byte           data_type
    byte           encoding_type
    byte           compression_type
    int32          property_count
    SchemaProperty property[property_count]

SchemaProperty :=
    String32 key
    String32 value

MeasurementSchema 使用 String32,而不是 Footer 其他位置常用的 VarString。

列类别编号为:

值类别
0TAG
1FIELD
2ATTRIBUTE
3TIME

Schema 保留列顺序,其中 TAG 列的顺序决定 DeviceID 片段的顺序。FIELD Chunk 通过 measurement_id 标识测点列,其物理排列顺序不必与 Schema 中 FIELD 列的顺序相同。

TAG 列标识设备,FIELD 列保存测量值,ATTRIBUTE 列描述实体,TIME 标识时间列。 列类别说明这一列的用途,data_type 说明它保存哪种值,两者共同决定列的含义。