上个月在园区跑无人车,录了两个小时的 ROS 2 bag,结果同事嫌风扇太吵,直接把终端里的录制进程杀了。回办公室一打开,ros2 bag info直接甩了一堆红字,当时的表情大概就是"完了,这数据白采了"。后来我花了整整一天,用各种工具和 Python 手撸方法把那个 mcap 格式的 bag 文件从半死不活的状态里捞了回来。如果你也踩到类似的坑,或者就是想提前知道遇到这种情况该怎么办,这篇记录大概率能帮你省掉一整天。
先说清楚,这篇聊的是 ROS 2 bag,重点关注 mcap 格式的文件损坏修复。mcap 是现在 ROS 2 默认推荐的录制格式之一,它天生适合大小时序数据,但"适合"不代表"不会坏"。进程被强杀、磁盘写满、机器断电、存储插件出 bug,任何一个环节抖一下,好好的 bag 文件都可能变成一堆"看起来有字但翻不动"的数据。下面我会按我实际的排查顺序来展开,包含诊断命令、修复脚本和一堆只有自己试过才懂的坑。
1. 从一次录制崩溃说起:mcap 文件为什么说坏就坏
很多人对 bag 文件有个误解:觉得它就是个"大文件",坏了最多是读不出来,里面内容应该还在。这个想法一半对一半错。mcap 格式的底层是连续的 record 序列,但它的打开方式不是从文件头一路扫到文件尾,而是依赖文件尾部的索引信息来快速定位各个 chunk。换句话说,mcap 像一本正文完整但没有目录的工具书,读取器必须先拿到目录(index),才知道去哪一页找内容。所以很多"文件损坏"并不是数据真的没了,而是目录没写全,导致读取器拒认。
1.1 mcap 的骨架:Header、Chunk、Index 和 Footer
为了讲清楚后面我要做的修复操作,这里快速过一遍 mcap 的核心结构。它不是一个数据库,而是一串带类型和长度的 record。一张简化后的结构表如下:
| 部分 | 作用 | 如果损坏会发生什么 |
|---|---|---|
| Header | 文件头,包含魔数和版本号 | ros2 bag info直接报 "bad magic" |
| Schema/Channel | 描述消息类型、topic、QoS 等信息 | 能读数据但无法反序列化,或者话题名/类型错乱 |
| Chunk | 一段连续的压缩或未压缩消息数据 | CRC 校验失败,读取到一半可能中断 |
| Index | 每个 topic 的消息索引,用于快速查找 | 读取器找不到索引,报 "missing index" |
| Statistics | 消息总数、chunk 数等统计信息 | 不影响读取,但ros2 bag info显示的数据可能不准 |
| Footer | 文件尾部标记,表示文件正常结束 | 进程中断时多半缺失,从而触发索引重建逻辑 |
我在踩坑时发现,90% 的 mcap 损坏都属于两种情况:一种是进程被 kill 或者断电,Footer 和最后的 Index 没来得及写全;另一种是磁盘空间不足,写入 chunk 时只写了一半,导致尾部数据截断。前一种通常还有救,后一种如果截断位置正好在一个消息中间,处理起来会麻烦一点。
1.2 五种常见的"病理性"表现
不同损坏原因在表面症状上区别很大。我整理了一下,基本逃不出下面这五类:
- 文件头不识别:文件大小正常,但
ros2 bag info报错类似Failed to read magic string,说明 Header 被覆盖或写坏了。这种情况多半是磁盘坏道、写了一半被人复制走,或者用了不靠谱的同步工具。 - 索引缺失:文件头部没问题,但打开时提示
no index records found,而且文件大小比正常录制的明显小一截。这通常是最后一段索引数据没来得及落盘,数据正文还在,修复成本最低。 - Chunk CRC 错误:读取到某个 chunk 时 CRC 对不上,读取器可能直接中断,也可能跳过一部分消息。这是中间数据损坏,常见于录制过程中系统崩溃或磁盘 IO 异常。
- 尾部截断:文件总大小比预期小,最后几个字节是半截 record。用
ros2 bag info可能也能通,但播放到最后一两秒时突然结束或卡住。 - Schema/Channel 引用悬空:有些 message 引用了不存在的 schema 或 channel 记录,播放时对应话题直接空白,或者一直报
Unknown type。
看到这些报错先别慌,也别急着装个"修复工具"一键处理。因为 mcap 没有统一的自动修复工具,必须根据具体症状决定下一步做什么。
2. 别急着跑脚本:先借这三个命令给 bag 做"伤情鉴定"
修复的原则很简单:先确认伤在哪,再做手术。最忌讳的是打开文件就开始乱写,这可能把原本能挽回的数据也搞坏。所以第一步不是修复,而是做一次完整的诊断。
2.1 第一步:备份原文件,并记录录制环境
不管文件看起来坏得多严重,先把它完整复制一份,然后用只读方式保存起来。我习惯这样做:
cp -a origin_bag /data/bag_backup_$(date +%Y%m%d_%H%M%S) chmod -R a-w /data/bag_backup_*备份不只是为了"留后路",更是为了方便对比修复前后的差异。如果后面修复脚本出了问题,至少还能回到原始状态重新来。
同时尽量回忆并记录录制环境:用的是哪个 ROS 2 版本,DDS 实现是 CycloneDDS 还是 Fast DDS,录制时有没有开启压缩,bag 原本应该包含哪些 topic,每类消息大概录了多久。这些信息看似和修复无关,但实际上对判断"缺了哪些数据"和"是否恢复成功"非常关键。比如原本有 12 个 topic,修复后只剩 8 个,那说明有 4 个 topic 的数据确实丢掉了或者读不出来,而不是修复脚本漏了。
2.2 第二关:ros2 bag info与mcap doctor定位
如果 bag 文件在 ROS 2 环境下还能被加载,先跑ros2 bag info看看它能吐多少信息:
ros2 bag info ./your_bag.mcap正常输出会列出 topic 名、类型、消息数量、频率等信息。损坏文件则可能出现多种异常:有的直接抛出异常,有的只显示部分 topic,还有的能显示全部 topic 但消息数明显不对。
拿到基础报错后,建议再用 mcap 官方 CLI 工具做更细的健康检查。mcap命令需要先安装,最简单的办法是用 pip:
pip install mcap mcap doctor ./your_bag.mcapmcap doctor会检查文件的结构完整性,包括 magic、索引、statistics 和 CRC 等。它的输出通常比较"口语化",会直接告诉你是缺少索引还是存在 CRC 错误。比如说遇到这样的提示:
[ERROR] missing MessageIndex for channel 3 [ERROR] chunk CRC mismatch at offset 0x...那基本可以判断是索引部分问题加局部 chunk 损坏。mcap doctor虽然不一定能修文件,但它是目前定位 mcap 问题最直接的命令行工具。
2.3 第三关:打开文件看数据段,区分"头坏"和"数据坏"
命令行的信息往往还不够细。我遇到的情况是ros2 bag info直接报错,mcap doctor也只说"header invalid"。这个时候就得动手把文件当作字节流慢慢扫一遍,看看到底是从哪里开始坏的。
我写了一个简单的 Python 片段,用mcap.reader尝试按 record 逐条读取,并在出错时打印出当前偏移:
from mcap.reader import make_reader from mcap.mcap0.exceptions import McapError def scan_mcap(path, max_records=100000): reader = make_reader(path) for i, record in enumerate(reader.iter_records()): if i >= max_records: break if i % 1000 == 0: print(f"scanning... record {i}") print(f"scanned {i+1} records") if __name__ == "__main__": try: scan_mcap("corrupt_bag.mcap") except McapError as e: print("McapError:", e)如果扫描中途抛出异常,说明损坏点在数据段内部;如果 scanner 能一直读到文件末尾,说明数据段整体是完整的,问题大概率出在索引或 footer,这种情况用重建索引就能解决。
要注意的是,make_reader在构造时可能就会要求加载索引。如果文件索引缺失,构造阶段就可能直接报错。遇到这种情况,可以用底层的mcap.reader的read_records手写循环,或者干脆用mcap convert类的工具先把数据段直接转走。这些细节放到下一章里一起说。
3. 三级修复方案:重建索引、切除尾部、逐条重写
诊断完就该动手术了。根据严重程度,我把修复方案分成三级。大多数情况下,可以先从最轻的方案开始试,不行再逐步升级。
下面这张表是我根据实际经验整理的方案选择参考:
| 损坏表现 | 推荐方案 | 原理 |
|---|---|---|
| 只有索引缺失,数据完整 | 重建索引 | 重新扫描 chunk,生成新的索引记录 |
| 尾部截断,最后一条记录不完整 | 切除尾部 | 把文件修正到最后一个合法 record 的边界 |
| 中间 chunk 损坏或 CRC 错 | 逐条重写 | 读取可用的 record,跳过坏区,写新 mcap |
| 头部 magic 损坏 | 逐条重写或手工补头 | 从数据段恢复,生成全新文件头 |
3.1 一级方案:重建索引
如果你的 bag 文件只是缺少索引,ROS 2 自带的一个子命令就能处理。在 ROS 2 Humble 之后的版本中,推荐先试:
ros2 bag reindex ./your_bag注意,这里的参数是 bag 目录而不是单个.mcap文件。ROS 2 的 bag 目录里通常包含metadata.yaml和.mcap文件,ros2 bag reindex会扫描现有数据并重建索引。运行结束后,再看一眼:
ros2 bag info ./your_bag如果 topic、消息数量都能正常显示,那就说明数据正文没丢,只是索引坏了。这种属于最幸运的情况,浪费的时间通常不超过两分钟。
如果ros2 bag reindex不支持当前 ROS 2 版本,或者直接报错,可以改用 mcap CLI 手动重建:
mcap index --reindex ./your_bag.mcapmcap index会读取 chunk 和 message,然后重新生成 MessageIndex、ChunkIndex 和 Statistics,并写回到文件尾部。它的副作用是会覆盖原文件,所以我强烈建议先备份再执行。
还有个小细节:如果你的文件是用 zstd 压缩的 chunk,mcap index需要依赖 zstd 库,没装的话可能会报错。安装 mcap 的完整依赖可以用:
pip install "mcap[compression]"这个依赖问题我踩过一次,明明文件是好的,但索引工具跑不起来,一度以为文件彻底没救了。
3.2 二级方案:切除尾部残缺记录
当文件尾部记录不完整时,重建索引可能失败,因为解析器在计算 chunk 长度时碰到一个"读不到指定字节"的异常。这个时候思路要反过来:既然尾部是残缺的,不如直接把残缺的部分切掉,保留前面完整的数据块。
先找到最后一个合法 record 的结束偏移。推荐用十六进制查看工具,比如xxd,把文件最后几百字节打出来:
xxd ./your_bag.mcap | tail -n 20mcap 的每条 record 都包含 1 字节 opcode、8 字节长度和 4 字节 CRC。你可以顺着文件往前找,看最后一个能闭合的 record 边界在哪里。找到了之后,用truncate把文件截断到那个边界:
truncate -s 0x... ./your_bag.mcap这里有一点必须提醒:截断的位置必须是 record 边界,不是随便找一个看起来像尾部的地方。如果你截断到了一个 record 中间,那么新文件的最后一条记录就是半条,mcap doctor依然会报 CRC 错误,重建索引也会失败。更稳妥的方式是用 Python 脚本去解析尾部的 record,而不是纯肉眼看十六进制:
import os from mcap.mcap0.records import Record def find_last_valid_offset(path): size = os.path.getsize(path) with open(path, "rb") as f: # 从文件末尾往前,每块扫描,尝试解析 record chunk_size = 4096 for back in range(0, size, chunk_size): start = max(0, size - back - chunk_size) f.seek(start) data = f.read() # 这里用真实 mcap 解析器去解析最后一个完整 record # 如果成功,返回对应 offset这部分需要一些二进制的耐心,但它的收益很高:切除尾部后,文件从"半坏"变成"能正常打开",而且没有任何数据丢失的代价。
3.3 三级方案:逐条重写为新 mcap
最复杂的情况是中间有 chunk 损坏,或者文件头完全不可读。此时不能再想着"原地修补",而是要换一种思路:把还能读到的 record 一条一条抽出来,写进一个新的 mcap 文件。
我写过一个可复用的 Python 修复脚本,核心逻辑是使用 mcap 库的低层 API,逐条遍历 record,遇到异常就跳过,直到文件末尾,然后生成全新的索引:
from mcap.reader import make_reader from mcap.mcap0.writer import Writer from mcap.mcap0.exceptions import McapError def repack_mcap(input_path, output_path): reader = make_reader(input_path) out = Writer(output_path) out.start() for record in reader.iter_records(): try: if record.type == b'\x04': # Schema out.write_schema(record.data) elif record.type == b'\x03': # Channel out.write_channel(record.data) elif record.type == b'\x02': # Message out.write_message(record.data) except McapError as e: print(f"skip bad record at {record.offset}: {e}") out.finish() repack_mcap("corrupt_bag.mcap", "repaired_bag.mcap")这段代码里最难的点在于:make_reader如果一开始需要索引才能工作,而索引又坏了,那iter_records()也可能跑不起来。正确的做法是先尝试用"流式读取"模式。mcap 库提供make_reader(path, options=ReaderOptions(read_chunks=True))这种方式不依赖索引,会按文件中的存储顺序顺序读取 record,代价是速度慢,但正好适合修复场景。
逐条重写还有一个额外好处:它会去掉原来的坏索引,由 Writer 在finish()时重新生成完整的索引、统计信息和 Footer。所以修复后的文件是一个结构完好的全新 mcap,不需要再额外做重建索引。
不过要记住,逐条重写的过程可能会跳过一些确实损坏的消息。修复完成后必须对比一下修复前后的可读记录数量,明确告诉团队"丢了多少数据"。真实项目里,丢了 5% 的传感器数据通常还能用,但如果丢的是关键时刻的数据,那就要评估是否值得重新采集。
4. 修复后不只是能播放:从 ROS 2 通信链路验证 bag 可信度
文件能打开、能播放,不代表数据就是可信的。修复后的 bag 往往存在局部消息丢失、时间戳缺口、话题数量不完整这些问题。如果直接把修复好的 bag 送去跑定位建图或者联合标定,很容易复现诡异的结果。所以在验收阶段,我会从 ROS 2 通信关系的角度做一套完整验证。
4.1 用ros2 bag play和话题频率做基础验证
第一步是把修复后的 bag 播一遍,看能不能正常回放。我一般这么做:
ros2 bag play ./repaired_bag然后另开一个终端,用ros2 topic hz盯着关键话题:
ros2 topic hz /camera/image_raw如果/camera/image_raw的频率和录制预期接近,说明数据流的连续性基本保住了。如果同一个话题刷不出数据,或者频率断断续续,那就要回过去检查这个 topic 在修复过程中的记录是否有丢失。
另外,回放时注意看/rosout里有没有新的错误日志,比如 QoS 不兼容、type 不匹配、时间戳回跳之类的提示。这些日志能帮你快速定位 channel 层面的问题。比如某个话题的type_hash在原始 mcap 里和当前 ROS 2 环境不一致,回放时就会提示无法匹配。
4.2 把 bag 里的通信关系画出来:QoS、topic 与时间戳对齐
很多修复版 bag 的问题是"能播放,但通信关系对不上"。什么叫通信关系对不上?简单说,原始系统里发布者和订阅者之间有一组 QoS 契约,包括 reliability、durability、depth 等;mcap 的 Channel record 会记录这些策略。如果你在重建索引或逐条重写时不小心丢掉了某些 channel 元数据,那么即使消息内容还在,ROS 2 回放时也无法按原始 QoS 关系发布。
验证 QoS 和 channel 信息最直接的方法是看mcap工具的 channel 概览:
mcap info ./repaired_bag.mcap --channels这个命令会把所有 schema ID、channel ID、消息数量以及 QoS 配置都打出来。和修复前从原始环境里记录的信息对比,如果 channel 数量少了,或者某个 topic 的 QoS reliability 从best_effort变成reliable,那就要注意了。因为激光雷达点云这类大消息通常用best_effort,而/tf、/cmd_vel用的是reliable,一旦互换,回放时会出现很离谱的丢包或延迟。
除了 QoS,还要看时间戳的对齐。ROS 2 的 bag 记录时每一条消息都有接收时间戳,消息内部通常还有header.stamp。修复后,这两类时间戳的单调性必须检查。我习惯写一个小脚本把同一 topic 的时间戳序列打印出来,观察有没有突变。
from mcap.reader import make_reader reader = make_reader("repaired_bag.mcap") stamps = [] for schema, channel, message in reader.iter_messages(topics=["/imu/data_raw"]): stamps.append(message.log_time) for i in range(1, len(stamps)): if stamps[i] < stamps[i-1]: print(f"time rewind at {i}: {stamps[i-1]} -> {stamps[i]}")如果时间戳大量回跳,说明在录制侧可能就存在时钟同步问题,修复只是把已经乱掉的数据重新打包了,并没有恢复真实的时序关系。这种情况下,不建议直接用这个 bag 做基于时序的标定。
4.3 时间同步的坑:时钟与时间戳异常如何暴露
这里要特别提一个我在实际项目中遇到的坑:bag 文件损坏后,某些消息的header.stamp依然保存着,但log_time(接收时间)因为录制中断变得不可靠。修复脚本如果把message.log_time当成唯一时间基准,回放出的消息顺序可能和传感器原始采样并不对齐。
所以验证时要区分"接收时间"和"数据自带时间"。对sensor_msgs/Image、sensor_msgs/PointCloud2这类自带硬件时间戳的话题,优先用header.stamp做时间轴分析;对/tf这类依赖系统时间的话题,则要看log_time和/clock是否一致。修复完的 bag 如果两个时间戳之间的差值出现跳变,就需要标记出来,宁可把这段数据切掉,也不要留着恶心下游算法。
顺带提一句,ROS 2 的 DDS 实现会影响 bag 记录时的时间精度。如果录制机上用的是多个进程,每个进程挂在不同的 DDS domain participant 下,各自身上的系统时钟可能还没做 PTP 同步,那么同一篇 bag 里的传感器数据时间基准就不统一。这个事和文件损坏无关,但会在修复后的验证阶段暴露出来,容易让人误判断是"修复造成的时间错乱"。
5. 我用血泪换来的录制规范
这篇写了这么多修复技巧,最后聊几句怎么从源头减少 mcap 损坏。这些规范不是从文档里抄的,是我在踩了多次坑之后沉淀下来的操作习惯。
5.1 分段录制 + 定期 flush
长时间录制是 bag 损坏的重灾区。我现在的做法是把录制任务尽量切成小段,每段 10-20 分钟,而不是一口气录两个小时。这样即使其中一段坏了,损失也是可控的。同时,录制过程中让底层存储插件及时把索引落盘,避免所有索引都堆在最后那一刻写。ROS 2 的ros2 bag record在正常关闭时才会写完整索引,但分段录制本身就能缩短单次索引窗口,降低崩溃时索引写不全的概率。
如果项目允许,还可以用--compression-mode和--compression-format开启压缩。压缩不仅能减小文件体积,还能让写入时的数据块更规整,在文件局部损坏时,压缩 chunk 的 CRC 校验能力也能帮助你快速定位坏点。
5.2 磁盘监控与掉电保护
很多 bag 损坏是磁盘满了才导致的。明明只录了一半,但磁盘空间到 0 后,写入库还在继续申请空间,最后留下一个没有 Footer 和索引的残缺文件。所以录制前和录制中,至少留出文件预期大小的两倍空间。
文件系统选择也有讲究。我在无人机和无人车上录 bag 时,优先用 ext4,因为它的 write barrier 机制在异常断电时相对稳。相比之下,某些旧挂载方式的 NFS 或者 exFAT 移动硬盘在写入中断时更容易留下脏数据。
如果你经常需要在户外录数据,建议配一个 UPS 或者用支持掉电保护的便携 SSD。贵是贵了点,但比野外数据全毁便宜多了。
5.3 常备工具清单
最后是我现在工位上一套随手就拿得到的工具清单,简单但有效:
ros2 bag reindex:ROS 2 自带的索引重建,先试它。mcapCLI:安装mcapPython 包后即可用,执行mcap doctor做健康检查和mcap info看 channel 信息。xxd:快速看二进制尾部和边界。truncate:修正截断文件的工具。- 短小的 Python 重写脚本:用 mcap Python 库把能读的 record 抽出来重新打包。
每次录完数据,我都会在拷贝之前先跑一条命令:
ros2 bag info ./bag && mcap doctor ./bag.mcap不到十秒,能提前发现 80% 的问题。这个习惯帮我挡掉了不止一次"第二天才发现数据坏了"的尴尬。
这次 mcap 修复折腾下来,最大的感受是:文件格式损坏并不等于数据丢失,大多数情况下只是"索引丢了"或者"尾巴缺了",养成先诊断、再备份、后修复的习惯,真的能把损失降到最低。