3. 记录文件格式解析

在 Cyber RT 的数据闭环中,record 文件是承载感知、定位、控制等全链路消息的核心容器。理解它的物理结构,直接决定了我们能否高效地做回放、切片、检索与二次分析。record 并不是一个简单的“消息堆叠文件”,而是一套带有元数据、索引和分块机制的序列化存储格式。本节从文件结构入手,逐层拆解消息序列化、元数据、索引与分块存储的设计逻辑。

一、record 文件的整体结构

一个 record 文件在物理上可以划分为三个区域:文件头、数据块区、索引与元数据区。文件头记录格式版本、通道数量等全局信息;数据块区按 chunk 组织,每个 chunk 内部包含若干条消息记录;索引与元数据区则保存通道定义、消息时间戳索引以及 chunk 的偏移信息。读取时,框架先加载元数据与索引,再按需定位到具体 chunk,而不是从头顺序扫描。

Cyber RT record 文件结构示意 文件头 Header magic / version / channel_count / chunk_interval / compression 数据块区 Chunk 0 … Chunk N Chunk 0 begin_time / end_time msg records + CRC Chunk 1 begin_time / end_time msg records + CRC Chunk N begin_time / end_time msg records + CRC 索引与元数据区 Index & Metadata channel 定义 / 时间戳索引 / chunk 偏移表 / 消息计数

二、消息序列化方式

record 中的每条消息并不是原始内存拷贝,而是经过序列化的字节流。Cyber RT 默认采用 Protobuf 作为消息描述与序列化方案,通道上传输的 message 会被编码为紧凑的二进制格式。序列化时,框架会写入消息长度、通道 ID、时间戳以及序列化后的 payload。这样做的好处是跨语言、跨进程可解析,同时字段可扩展。

需要注意的是,record 文件本身并不保存 .proto 定义,它只保存通道名和序列化后的数据。回放或解析时必须提供对应的 proto 描述,否则只能看到二进制字节。这也是很多初学者“能读到消息却解析不出字段”的根本原因。

// 典型 record 消息记录布局(逻辑视图)
message RecordMessage {
  uint64 timestamp_ns;   // 消息时间戳
  uint32 channel_id;     // 通道编号
  uint32 payload_size;   // 序列化后长度
  bytes  payload;        // Protobuf 序列化数据
}

三、元数据与索引机制

元数据是 record 的“目录”。它主要包含三类信息:通道元数据、时间索引和 chunk 索引。通道元数据描述每个 channel 的名称、消息类型、proto 描述符引用;时间索引把消息时间戳映射到具体的 chunk 和偏移;chunk 索引则记录每个 chunk 的起始偏移、结束偏移、起止时间和消息数量。

有了索引,回放工具就能实现“按时间区间快速定位”。例如指定从 100s 到 120s 回放,框架先查时间索引找到覆盖该区间的 chunk 列表,再只读取这些 chunk,而不必扫描整个文件。索引通常以内存映射或轻量结构加载,因此打开大文件的速度远快于顺序读取。

关键点:索引不是消息本身,而是“消息在哪里”的导航信息。索引损坏时,文件可能仍可顺序读取,但无法随机定位。

四、分块存储机制

分块(chunk)是 record 写入与读取的基本单位。写入时,框架按时间或大小阈值把消息聚合成 chunk,达到阈值后刷盘并生成该 chunk 的索引。读取时以 chunk 为粒度加载,既减少 I/O 次数,又便于并行处理。chunk 内部通常按时间有序,chunk 之间也保持整体有序。

分块带来的直接收益是:支持流式写入、支持按块压缩、支持按块校验。常见配置中,chunk_interval 控制单个 chunk 覆盖的时间跨度,compression 控制是否对 chunk 做压缩。压缩以 chunk 为单位,因此随机读取时只需解压目标块,不会牵动全文件。

结构元素 作用 典型参数/字段 读取影响
Header 标识格式与全局配置 version、channel_count 打开文件时首先校验
Chunk 消息聚合与 I/O 单位 begin_time、end_time、CRC 决定随机读取粒度
Channel Metadata 描述通道与消息类型 channel_name、message_type 解析 payload 的前提
Time Index 时间戳到 chunk 的映射 timestamp → chunk_id 支持按时间区间定位
Chunk Index 记录块偏移与统计 offset、msg_count 支持跳读与并行加载

五、案例:一次按时间切片的回放

假设有一个 8GB 的 record 文件,包含 12 个通道,chunk_interval 为 1s。现在需要回放第 300s 到第 305s 的数据。解析流程如下:

  1. 加载元数据,确认 12 个通道的 proto 描述均可用。
  2. 查询时间索引,定位覆盖 300s–305s 的 chunk 列表,假设为 chunk 300 到 chunk 305。
  3. 根据 chunk 索引读取这 6 个 chunk 的偏移,按需解压。
  4. 在每个 chunk 内按时间戳过滤,丢弃区间外的消息。
  5. 按通道分发到回放器,恢复原始时间间隔或加速回放。

整个过程只读取了约 6 个 chunk,而不是 8GB 全量数据。这正是索引与分块机制带来的效率提升。

实践建议:做数据分析前,先用 cyber_recorder info 查看通道、消息数、起止时间和 chunk 分布,再决定切片策略。

cyber_recorder info demo.record
# 输出示例:
# channel: /apollo/perception/obstacles  msgs: 12000
# channel: /apollo/localization/pose   msgs: 60000
# duration: 120.5s   chunks: 121   size: 2.3GB

六、常见误区与复盘

误区一:把 record 当成纯文本日志。record 是二进制序列化格式,必须用对应工具和 proto 解析,直接文本打开只会看到乱码。

误区二:忽略 proto 版本一致性。如果回放时使用的 proto 与录制时字段编号不一致,会出现字段错位或解析失败。复盘时应固定 proto 版本并纳入版本管理。

误区三:认为索引永远可靠。异常断电或写入中断可能导致索引不完整。此时可尝试顺序扫描恢复,但会丢失随机定位能力。工程上应保证录制进程正常关闭,或定期做文件完整性校验。

误区四:chunk 越大越好。chunk 过大虽然减少 I/O 次数,但随机读取时解压和加载的冗余数据更多;chunk 过小则索引膨胀、打开变慢。需要根据消息频率和回放粒度权衡 chunk_interval。

排障提示:若回放时提示“channel not found”或“failed to parse message”,优先检查 proto 描述是否加载、通道名是否匹配、消息类型是否一致,而不是怀疑文件损坏。

总结来看,record 文件 = 头部 + 分块数据 + 元数据与索引。消息以 Protobuf 序列化后写入 chunk,元数据描述通道,索引提供时间与偏移导航,分块机制兼顾写入效率与随机读取。掌握这四层结构,后续做回放控制、数据切片和离线分析时,就能从“黑盒读取”转变为“按结构精准操作”。