rosbag2 录制回放:分层架构、时间控制与存储格式
ros2 bag record 和 ros2 bag play 是 ROS2 里最常用的数据录制回放工具。它看起来是个简单的命令行程序,背后却是一套分层的架构:传输、存储、压缩各自解耦,插件化扩展。本文自顶向下拆解 rosbag2:工具的命令面与三层设计、现行代码的五层架构、Player/Recorder 组件的内部结构、回放时间控制系统的工作原理,以及 SQLite 存储的落盘格式。service 与 action 的录制回放涉及单独的 introspection 旁路机制,另见本站《ROS2 service 与 action 的录制与回放》,本文只做概述。
命令面
| 命令 | 功能 |
|---|---|
| ros2 bag record | 将 ROS 数据记录到数据包 |
| ros2 bag play | 从数据包回放 ROS 数据 |
| ros2 bag info | 打印数据包的信息 |
| ros2 bag convert | 按新设置转换写出数据包 |
| ros2 bag list | 打印可用插件的信息 |
| ros2 bag reindex | 为数据包重建元数据文件 |
录制侧的能力:动态发现新主题、正则/排除/显式列表三种过滤、自动匹配发布者 QoS、按大小或时长自动分割、可插拔的消息级/文件级压缩、按需触发的环形缓冲快照模式。
回放侧的能力:速率控制(--rate 或运行时服务)、向 /clock 发布仿真时间、基于时间戳的跳转定位、按主题过滤、循环回放。
三层设计
rosbag2 立项时的存储设计文档给出了三层抽象(现行代码的接口已演进为 rosbag2_storage 的 C++ 插件接口,但分层思想一致):
三层各自的约束:
- 存储 API 层与 ROS2 解耦,只读写抽象的二进制消息与键值元数据,不感知消息类型;每个主题只能有一种编码方式,不支持同一主题混合编码
- rosbag API 层负责把 ROS2 消息序列化后交给存储层,并支持任意编码格式的双向转换;录制时不指定
--encoding,则消息以中间件传来的二进制格式直接落盘,不做二次序列化 - 命令行层只是薄入口,逻辑都在 API 层
设计目标里值得注意的两条:回放必须是确定的(顺序播放 n 次收到相同的消息序列,靠录制时正确标记每条消息的时间戳保证);长期存储要能应对消息定义的变化(新增字段的迁移需要反序列化,不能在高效回放序列化数据的同时完成)。
现行五层架构
现行代码(rolling)的实际分层:
- User Layer 提供命令行与 launch 文件两种入口
- Player 和 Recorder 都实现为 rclcpp 组件,可加载进组件容器做进程内通信
- Core C++ 层实现存储 I/O、压缩与格式转换
- 存储与压缩都是插件:早期默认 SQLite,rolling 发行版默认已改为 MCAP
Player / Recorder 的内部结构
两个核心组件的构成是对称的:Recorder 侧由 rclcpp 节点 + RecorderImpl + 一组 GenericSubscription(运行时动态加载类型支持,按主题订阅任意类型消息)构成;Player 侧对应的是 GenericPublisher。控制服务都挂在节点私有命名空间下。
Player 控制服务(namespace ~/):pause、resume、toggle_paused、is_paused、get_rate、set_rate、seek、play_next、burst、play、stop。
Recorder 控制服务(namespace ~/):pause、resume、is_paused、split_bagfile、snapshot(仅快照模式启用)。
GenericSubscription/GenericPublisher 的类型支持动态加载机制(不在编译期绑定消息类型)正是 rosbag2 能录制任意主题的前提。
回放时间控制系统
回放的核心问题是:如何按录制时的时间戳,把消息以正确的速率、支持暂停与跳转地重新发布。rosbag2 的时间控制系统将时间管理与消息发布解耦(设计文档:rosbag2_playback_time.md)。
系统维护一个参考点,建立两个时间域之间的关系:
- ROS 时间:录制消息中使用的时间戳域(bag 内时间)
- 稳定时间:测量实际经过时间的现实世界时钟
参考点记录了「某个 ROS 时刻对应某个稳定时刻」,两个域之间的任何换算都经过它。回放循环对每条消息:取下一条消息的 ROS 时间戳,经参考点换算成稳定时间轴上的目标时刻,sleep_until 后发布。
三类操作都归结为参考点的调整:
- 变速:新速率下重新锚定参考点,后续换算按新斜率进行
- 暂停/恢复:暂停时记录当前稳定时刻,恢复时把暂停时长从参考点中扣除,ROS 时间相当于冻结
- 跳转(seek):直接把参考点的 ROS 侧平移到目标时刻,稳定时间侧不变,下一条消息立即从新位置开始换算
SQLite 存储格式
SQLite 存储插件把一个 bag 写成单文件数据库加一个 metadata.yaml:
rosbag2_2025_11_10-14_55_45
├── metadata.yaml
└── rosbag2_2025_11_10-14_55_45_0.db3 数据库包含 4 张表:
CREATE TABLE schema(schema_version INTEGER PRIMARY KEY,ros_distro TEXT NOT NULL);
CREATE TABLE metadata(id INTEGER PRIMARY KEY,metadata_version INTEGER NOT NULL,metadata TEXT NOT NULL);
CREATE TABLE topics(id INTEGER PRIMARY KEY,name TEXT NOT NULL,type TEXT NOT NULL,
serialization_format TEXT NOT NULL,offered_qos_profiles TEXT NOT NULL);
CREATE TABLE messages(id INTEGER PRIMARY KEY,topic_id INTEGER NOT NULL,
timestamp INTEGER NOT NULL, data BLOB NOT NULL);
CREATE INDEX timestamp_idx ON messages (timestamp ASC); schema:存储 schema 版本与 ROS 发行版topics:主题名、类型、序列化格式、发布时的 QoS(录制下来的 offered profile,回放时可用)messages:消息二进制数据(BLOB),topic_id外键指向topics.id构成一对多,timestamp建有索引支撑时间范围查询metadata:插件自用元数据
metadata.yaml 则记录 bag 级信息:存储标识、起止时间、消息数、每个主题的类型与计数、压缩格式与文件列表。ros2 bag info 的输出来自这里。
SQLite 作为早期默认存储的理由:依赖轻量、单文件、各平台测试充分、表结构灵活可扩展、能用标准 SQL 工具查询。代价是与直接转储二进制文件的格式相比写入路径更重——第三方基准(mcap.dev 的存储插件对比)显示在大量小消息的场景下,SQLite 插件的写入吞吐明显低于 MCAP 插件,这也是 rolling 发行版把默认存储换成 MCAP 的背景。需要 SQL 查询能力或与既有数据兼容时仍可显式指定 sqlite3。
Service / Action 的录制回放
service 和 action 不是可直接订阅的对象,rosbag2 通过 service introspection 机制把每次请求/响应作为事件发布到 服务名/_service_event 隐藏主题上,录制端按普通主题收录;action 由 3 个 service + 2 个 topic 组成,启用内省后同样落在服务事件主题里。回放侧 --publish-service-requests 可以把记录的请求重放为真正的服务调用。这套机制的消息结构、落盘细节与各发行版能力边界,展开见本站《ROS2 service 与 action 的录制与回放》。
要点
- rosbag2 是分层插件化架构:传输(Player/Recorder 组件)、核心(存储/压缩/转换)、存储插件三层解耦,命令行只是薄入口
- Player/Recorder 基于 GenericPublisher/GenericSubscription 实现类型无关的收发,控制接口以 ROS2 服务形式暴露在私有命名空间
- 回放的时间控制靠「ROS 时间 ↔ 稳定时间」参考点换算:变速、暂停、跳转都是参考点的调整,时间管理与消息发布解耦
- SQLite 存储是 4 张表 + metadata.yaml 的单文件布局,messages 按 topic_id 关联、timestamp 建索引;小消息高频场景性能弱于 MCAP,rolling 起默认存储已是 MCAP