ROS2 mcap bag文件损坏修复实战:诊断、抢救与验证
搞机器人开发和自动驾驶测试的估计都经历过这个“至暗时刻”熬夜录了三个小时的 ros2 bag第二天要用ros2 bag info直接报错要么提示无法打开存储要么播放到某个时间点突然退出。尤其是从 ROS 2 Humble 开始mcap 已经成了默认录制格式这类问题并不少见。mcap 比老款 sqlite3 格式更紧凑、更利于流式写入但正因为是“流式 索引”结构一旦录制进程异常终止、磁盘写满或者文件拷贝中断很容易出现“数据还在索引却坏了”的尴尬局面。这篇文章不是纯理论科普是我用 mcap bag 过程中真遇到文件损坏后尝试过的修复方法和踩坑记录。会从诊断思路、命令行自动恢复、Python 脚本手动重构到修复后的完整性验证都写清楚。适合一线机器人开发、SLAM 调试、自动驾驶数据采集的同学直接参考。如果你手头正好有一个打不开的 bag这部分内容基本可以拿来抄作业。1. mcap bag 为什么会“打不开”格式设计与损坏成因1.1 mcap 的文件骨架数据、索引和门牌号mcap 本质上是一串二进制记录流文件从头到尾由不同类型的小记录拼接而成。每条记录开头都有一小块标头写着这条记录的类型和长度有点像盖楼时的“预制板”一块一块拼起来。与老式 sqlite3 bag 不一样mcap 不是为了“随机查表”设计的而是为了“边录边写”设计的速度更快文件也更紧凑。里面几个关键结构需要先认识一下。Header 是文件开头的固定标志相当于集装箱箱号。Schema 记录保存消息类型定义比如 sensor_msgs/msg/Image 的完整消息定义。Channel 记录描述一个话题写上 topic 名字、消息编码格式以及它关联的 Schema。Message 就是实际的消息数据本体。Chunk 是把一段时间内的多条消息打包成一个数据块可以整体做 zstd 或 lz4 压缩减少磁盘占用。文件后半段还有 MessageIndex、ChunkIndex、Statistics、Footer。这些都属于“索引区”作用类似仓库门口的货架清单。ChunkIndex 告诉你哪一段数据在文件的哪个位置Statistics 统计消息总数和每个话题的消息数Footer 则标注了索引区从哪里开始。我习惯把 mcap 理解成仓库Chunk 和 Message 是仓库里真实存放的货物Summary 和 Footer 是贴在门口的货架清单。货物还在清单丢了我们还能按顺序把货一件件搬出来如果货物本身被压坏了那就真的只能看着办。理解这个结构是后面所有修复手段的基础。1.2 常见的损坏场景从 kill -9 到磁盘写满先说最常见的损坏原因九成以上都是“非正常结束”。ros2 bag record 在录制过程中数据是持续写进文件的但索引区和结尾的 Footer 并不会在每条消息后都更新而是等录制结束、进程收到 CtrlC 或者正常收尾时才统一落盘。如果你直接kill -9杀掉录制进程或者机器人断电、系统崩溃那么最后一个 Summary 和 Footer 根本没机会写进去。结果就是文件里堆满了消息数据但没有索引文件尾部也没有正常结束标记解析器打开时一头雾水。第二个高频原因是磁盘空间不足。机器人上的磁盘剩余空间往往不够宽裕录制大 topic 时瞬间写爆。磁盘写满时chunk 可能只写入一半操作系统返回写入失败但 mcap 写入端不一定能及时感知随后文件和实际磁盘状态就脱节了。这种损坏往往伴随 chunk 的 CRC 校验失败比单纯缺索引更难处理。第三个原因容易被忽视文件拷贝中断或跨文件系统搬运。用 scp 传一个大文件传到一半断掉文件尾部缺失拿 U 盘从工控机拷到开发机Windows 和 Linux 文件系统之间缓存机制不同也可能导致最后一段内容没有真实落盘。我甚至见过程序员用微信文件传输助手传 40GB 的 bag传完了一看文件大小对但打开就是报错。跨平台传大文件最好打包成压缩包再传别裸传。1.3 “打不开”不等于“没数据”修复思维的转变很多同学一看到ros2 bag info报错第一反应是“这个 bag 废了要重新录”。实际上绝大多数情况下数据区还是完好的只是索引或者尾部标记丢了。打个比方一本书被撕掉了目录和封底书里每页文字其实还在。你只要按顺序翻仍然能读完绝大部分内容。所以修复的核心思路不是“恢复原文件”而是“忽略损坏的索引从数据区重新把消息捞出来再重新打包成一份带完整索引的新 mcap”。一切修复工具和脚本本质上都在做这件事。把这个思维转过来你再看后面那些操作就会觉得顺理成章备份是为了保护现场诊断是为了判断“数据区和索引区到底谁坏了”修复则是看情况选择自动工具还是手写脚本。2. 修复前先诊断损伤分级与工具选择2.1 第一件事永远是备份与保护现场我见过有人拿到损坏的 bag上来就敲修复命令结果工具把原始文件改写了越修越坏最后连抢救的余地都没有。正确的做法是第一步先复制一份原始文件所有操作都在副本上进行。具体来说cp damaged.mcap /data/recovery/damaged_raw.mcap md5sum damaged.mcap /data/recovery/damaged_raw.mcap记录 md5 和后端文件大小后续修复后的文件可以和这份原始大小做对比判断到底丢了多少数据量。另外如果这个 bag 是从别人那里拿来的最好把原始文件保留在只读挂载的目录下不要随便给它写权限。这不是小题大做修复工具的行为有时候不可控多留一份备份你的试错空间就大很多。2.2 体检组合拳ros2 bag info 与 mcap doctor诊断阶段我会依次跑两条命令。第一条是ros2 bag info damaged.mcap这条命令能正常输出话题列表和消息统计说明索引区大概率还能读问题可能出在数据区某一段如果直接报错比如提示无法打开存储、无法读取记录说明 Summary 或 Footer 已经损坏。这时不要慌它只是说明索引没了不代表数据全丢。第二条命令用的是 mcap 官方 CLI 里的 doctor 子命令mcap doctor damaged.mcapmcap doctor会严格按照 mcap 格式规范逐项检查文件头、chunk、消息索引、统计信息、尾部标记、CRC 校验。输出会直接指出缺了哪些部分或者哪些 chunk 的 CRC 不对。这是整个修复过程中最有价值的一步因为它能帮你判断该走哪条修复路线。拿到 doctor 的报告你基本就知道问题是“缺索引”还是“数据区损坏”了。2.3 症状、原因与修复路线对照表我把日常工作中最容易碰到的几种情况整理成了一张对照表。表格不能替代mcap doctor的精确诊断但可以帮你快速判断方向症状可能原因最合适的修复方式ros2 bag info 报错无法打开存储Summary/Footer 未写入或损坏mcap recover 重建索引mcap doctor 提示 chunk CRC 错误chunk 写入中断或数据损坏跳过该 chunk保留其他消息能打开但播放到某时间点退出某个 chunk 中间读取出错时间范围裁剪绕过损坏段文件大小远小于录制日志的预期拷贝中断或系统崩溃截断从数据区抢救可读消息能打开但消息时间戳乱跳消息内部时间戳异常按时间戳过滤重写如果你发现同时命中多行那大概率是多重损伤叠加修复难度会高一些。比如“文件尾部被截断”加“某个 chunk 也写坏”那就得先抢救能读到的消息再想办法跳过坏数据。这种时候脚本方案比一条命令更灵活。2.4 问录制人三个问题省掉一半无用功如果这份 bag 是团队里其他人录的我建议在动手前先问清楚三件事。第一录制结束方式是正常 CtrlC 还是直接 kill -9第二当时磁盘剩余空间大概还有多少第三这个文件后续是否经过网络或存储介质拷贝这三个问题基本对应前面说的三类损坏原因。如果录制人说是正常结束但文件依然损坏那大概率是拷贝环节出了问题优先检查文件大小和尾部如果他说是磁盘满了那重点检查 chunk 完整性如果他说拷贝过程中断过那直接用 recover 往往能救回大部分数据。问清楚这几点可以避免你在错误的方向上反复试错省下大量时间。3. 数据抢救实操从一条命令到手工脚本3.1 首选方案mcap recover 重建索引最简单、也最值得先试的方法是用 mcap CLI 的 recover 子命令。它做的事情就是最核心的修复逻辑忽略损坏的索引从头到尾扫描文件里的合法记录能读多少读多少然后重新生成一份带完整索引的新 mcap 文件。安装 mcap CLI 有两种常见方式。一种是直接从官方 GitHub 仓库的 Releases 页面下载对应系统的可执行文件另一种是通过 Python 包安装pip install mcap安装完成后先跑 doctor 确认损伤范围然后执行恢复mcap doctor damaged.mcap mcap recover damaged.mcap repaired.mcaprecover 在执行时会打印一些处理日志告诉你它读到了多少条记录、跳过了哪些坏区域。处理完以后再跑一次 doctor 检查新文件error 级别的问题应该清零。需要说明的是recover 不是万能药。如果某个 chunk 内部的压缩数据流已经坏了它只能跳过这个 chunk这个 chunk 里面的消息就会丢失。但绝大多数“进程被杀、尾部索引没写”的场景数据区本身并没有真正损坏recover 能把绝大部分消息原样捞回来。我实测过几次在正常录制但被异常终止的 bag 上救回率通常能到 95% 以上。3.2 进阶方案Python 脚本按消息流重写 bag有时候 mcap recover 也会失效或者你只想保留部分话题、部分时间段的数据这时候就要自己写脚本。mcap 官方提供了 Python 库安装方式很简单pip install mcap下面是一个我实际用过的恢复脚本骨架思路是用make_reader打开损坏文件时明确告诉它不要依赖索引、不要校验 chunk CRC然后顺序遍历所有消息遇到能读到的消息就写入一个新的 Writer最后重新生成完整索引。from mcap.reader import make_reader from mcap.writer import Writer def recover_mcap(src_path, dst_path): reader make_reader( src_path, use_chunk_indexFalse, # 不走索引顺序读 verify_chunk_crcsFalse, # 不校验 chunk CRC尽量多读 ) writer Writer(dst_path) writer.start() schema_map {} channel_map {} try: for schema, channel, message in reader.iter_messages(): if schema is None and channel is None: continue new_schema_id None if schema is not None: new_schema_id schema_map.get(schema.id) if new_schema_id is None: new_schema_id writer.register_schema( schema.name, schema.encoding, schema.data ) schema_map[schema.id] new_schema_id if channel is None: continue new_channel_id channel_map.get(channel.id) if new_channel_id is None: new_channel_id writer.register_channel( channel.topic, channel.message_encoding, schema_idnew_schema_id, metadatachannel.metadata, ) channel_map[channel.id] new_channel_id writer.add_message( channel_idnew_channel_id, log_timemessage.log_time, datamessage.data, publish_timemessage.publish_time, ) except Exception as e: print(fwarning: {e}) writer.finish()这段脚本有一个明显的限制如果iter_messages()在读某个 chunk 时抛异常生成器会中断后面的数据全都会丢。所以这里更推荐搭配使用一个思路——先用 mcap recover 处理普通损坏再用脚本做定向过滤。真到了 recovery 都不能handle的场景你需要的就不是这种高层迭代 API而是更底层的 record 流解析按记录粒度捕获异常、跳过坏字节。相比起来投入产出比不高一般团队不需要碰那么深。3.3 定向抢救按时间范围裁剪和单话题筛选有一种情况很典型mcap doctor报告显示 35 分钟到 38 分钟之间的 chunk 损坏了前面和后面的数据都正常。这时候就没必要整文件重建可以只保留完好区间去掉损坏窗口之间的数据。在脚本里只加一个时间过滤条件就能做到。mcap 内部时间戳单位是纳秒注意别把秒和纳秒搞混start_ns int(35 * 60) * 1_000_000_000 end_ns int(38 * 60) * 1_000_000_000 for schema, channel, message in reader.iter_messages(): if start_ns message.log_time end_ns: continue # 跳过损坏区间 # 其余逻辑同上写入新文件如果你想按话题筛选只救回个别重要 topic就更简单。在循环里判断 channel.topic 就行。比如传感器融合算法最需要 /camera 和 /odom但 /pointcloud 已经坏了那就只把摄像头和里程计消息写进新文件避免把无用的坏数据带到下游。3.4 终极保底重新播放再录制别让数据真丢如果文件损坏程度已经大到连按需抢救都做不了手里又只剩这一份数据还有一个“土办法”把 bag 当作半录制数据源播放到损坏点之前同时重新录制一遍。具体操作是ros2 bag play damaged.mcap --start 0 --duration 500上面这个命令会尝试从头开始播放前 500 秒的数据。播放到损坏 chunk 时进程可能会退出但只要退出前已经把前 500 秒的数据发出来了你就可以在另一个终端同时执行ros2 bag record /topic1 /topic2 -o recovered_from_play这个方案有两个问题。第一消息时间戳不再是原始录制时间而是按当前系统时间重新生成下游算法如果对时间敏感会受影响。第二播放过程是实时速度500 秒的数据要等 500 秒。但好处是它能绕过解析器层面的“拒读”把数据区里还没坏的内容通过正常话题管道重新采集出来。属于兜底方案能救一点是一点。3.5 顺手而为的优化压缩算法与归档习惯修复文件时其实是个调整读取策略和压缩参数的窗口。mcap 文件里的 chunk 可以选择不压缩、zstd 或 lz4。zstd 压缩率更高适合长期归档lz4 速度更快适合高频回放。如果你修的这份 bag 是要拿来做算法评测、长期保存的建议在重写时显式指定 zstd。在 Python Writer 里注册时可以先设置全局压缩选项。比如 mcap 的 Writer 支持往Writer(...)传入压缩策略部分版本在writer.start()之前通过writer.compression之类的属性配置。不同版本语法略有差异最好查一下你安装的库的 API。如果不想折腾也可以用 mcap CLI 自带的 convert 命令统一处理mcap convert repaired.mcap repaired_zstd.mcap --compression zstd这种顺手优化能让修复后的文件在磁盘占用、读取性能之间取得一个更好的平衡。4. 修复完成不等于结束验证流程与常见问题4.1 修复后的三道验证关卡修复完的文件不要急着直接跑算法先过三道关卡。第一关是完整性检查mcap doctor repaired.mcap ros2 bag info repaired.mcapdoctor 没有 errorinfo 能正常列出话题和消息数说明文件结构已经是合法的 mcap 了。第二关是抽样播放。用ros2 bag play选几个关键话题播放 10 到 20 秒确认不会中途退出话题频率和消息内容基本正常ros2 bag play repaired.mcap --topics /camera /scan /odom第三关是转换验证。用ros2 bag convert把修复后的 mcap 转成 sqlite3 格式这一步非常有用因为转换过程相当于用另一种存储后端完整解析了一遍所有消息ros2 bag convert -i repaired.mcap -o repaired_db --storage sqlite3转换成功且结果目录大小符合预期基本可以放心使用。如果这三关哪一步有问题说明数据区损坏比预想的严重回到诊断阶段重新评估。4.2 我用过的修复合集典型问题与排查过程先说一个很常见的坑recover 成功后ros2 bag info能打开但某个 topic 的消息数明显偏少。这大概率是包含该话题的某个 chunk 损坏被跳过了。mcap 的默认 chunk 大小通常不大损坏一个 chunk影响的往往是一小段时间窗口的数据前后数据都能保留。遇到这种情况我一般直接接受损失因为被跳过的消息确实无法恢复。另一个问题是“修复后 topic 列表为空”。原因通常是数据区里的 Channel 记录也丢了。mcap 的 Message 记录只保存 channel_id不带话题名如果 Channel 记录找不回来消息即使还在也无法正确归类。这种情况除非你有原始 ROS 接口定义可以手工补 Channel否则基本无解。所以看到这个现象先确认一下原始文件是否在数据区也严重破损。还有一次遇到跨平台拷贝导致的玄学问题在 Windows 上用 WSL 录制的 bag拷到 Linux 服务器上打开报错。后来发现是 WSL 挂载盘与 Linux 原生文件系统之间的缓存差异导致文件尾部数据没完全刷盘。从那以后我定了一条规矩录制和拷贝全程在 Linux 原生文件系统上做跨平台转移一定先 tar 再传。4.3 长期主义从源头降低 bag 损坏概率修复再熟练也不如从源头减少损坏。我现在的做法有三条。第一条录制节点上预留磁盘空间确保剩余空间至少是预计 bag 大小的两倍同时在磁盘使用率超过 85% 的阈值时设置告警。第二条录制结束用优雅方式退出不要 kill -9ros2 bag record 的 CtrlC 会触发正常的索引和 Footer 写入。如果需要在无人值守场景下自动停止用超时参数或外部发送 SIGINT而不是直接杀掉进程。第三条录制完成后立刻跑一次mcap doctor有问题当场重录而不是等两周后要用数据才发现损坏。这个检查动作可以写成一个简单的 post-record 脚本bag 录制一结束就自动执行。另外重要数据建议同时保存一份 mcap 原始文件和一份转好的 sqlite3 副本两个文件同时损坏的概率远比一个文件损坏的概率低得多。最后再分享一个实际体会。碰到 mcap bag 损坏我的第一原则永远是“先备份、再诊断、后拯救”。多数情况下尾部索引丢失并不等于数据消失mcap recover能救回绝大多数消息真正救不回来的是那些 chunk 内部也写坏的数据。与其依赖事后修复不如把完整性检查前置到录制流程里。团队里准备一个顺手的小工具脚本录完就跑一下 doctor发现问题马上处理比任何修复方案都更省心。