btrfs_rescue_blog
关于我的 128G SSD 被 Btrfs 元数据饥饿与 Docker 联手「干废」的那件事
序言:清晨的惊魂“只读”
故事是这样开始的。在一个风和日丽的早晨,我按下电源键,期待着熟悉的主机霓虹灯与闪烁的屏幕。然而,屏幕上没有出现精致的 Manjaro 桌面,取而代之的是冰冷且绝望的 grub 报错,以及闪烁着的 Btrfs 文件系统错误。
“系统起不来了。”
这五个字对于任何一个 Linux 用户来说,严重程度堪比“你的代码在生产环境把数据库删了”。我熟练地掏出备用的 Ventoy U 盘,插上电脑,进入了 Live 环境。今天,我们就来聊聊,一个 128G 的 / 分区和 2T 的 /home 分区,是如何因为一次看似不起眼的写操作,上演了一场 Btrfs 树节点损坏与“元数据饥饿”的生死时速。
第一幕:初探险境——物理空间还有 44G,你跟我说没空间了?
进入 Live 环境后,我首先使用命令查看了磁盘布局:
sudo lsblk -o NAME,FSTYPE,SIZE,LABEL,UUID,MOUNTPOINTS很清晰:
nvme1n1p2:128G SSD,挂载为/(根分区)。nvme0n1p1:2T SSD,挂载为/home(家目录)。sda1:3.6T HDD,挂载为/backup(备份盘)。
我随手建了个 /mnt/root 目录,尝试挂载根分区:
sudo mount /dev/nvme1n1p2 /mnt/root挂载居然成功了,没有抛出致命错误。我立刻用 df -h 和 btrfs filesystem usage 查看空间,这一看,直接让我进入了黑人问号状态:
Filesystem Size Used Avail Use% Mounted on
/dev/nvme1n1p2 119G 72G 45G 62% /mnt/root
Unallocated:
/dev/nvme1n1p2 9.05MiB
等等,可用空间(Avail)明明还有 45GB(使用率 62%),但是未分配空间(Unallocated)竟然只有可怜的 9.05 MiB!
这里需要科普一个 Btrfs 的硬核机制:双层空间管理。 Btrfs 将物理磁盘划分为一个个“块组(Chunks)”,分为 Data Chunks(存放数据文件)和 Metadata Chunks(存放文件树、文件名、权限等元数据)。 由于我的系统开启了 Btrbk 自动快照,而且频繁运行各种容器,Btrfs 贪婪地把几乎所有的物理磁盘空间都分配给了 Data Chunks(占了 112GB)。而当元数据空间不足、需要向物理磁盘申请分配新的 Metadata Chunk(在 DUP 备份模式下需要 512MB~1GB 的连续未分配物理空间)时,发现未分配空间只剩 9MB 了!
这就是经典的 Btrfs 元数据饥饿(Metadata Starvation)。虽然你名义上还有 45G 空间(因为很多 Data Chunks 内部是空的),但因为没有空闲的物理空间分给 Metadata Chunks,整个文件系统只要尝试写入,就会直接报错“No space left on device”。
第二幕:雪上加霜——树结构损坏 (Tree First Key Mismatch)
如果只是没空间了,做一个 Balance(均衡)操作把 Data Chunks 的空闲空间退回给系统不就行了?
天真!我尝试删除受损文件或写入测试文件时,文件系统瞬间变成了 Read-Only(只读)。
查看 dmesg 发现,内核抛出了一个毁灭性的红色警告:
BTRFS error (device nvme1n1p2): tree first key mismatch detected, bytenr=156594716672 parent_transid=307019 key expected=(18446744073709551606,128,215364546560) has=(18446744073709551606,128,215364579328)
BTRFS error (device nvme1n1p2 state A): Transaction aborted (error -5)
BTRFS info (device nvme1n1p2 state EA): forced readonly
这是一个校验和树(csum tree)损坏错误! 解析这个报错里的硬核密码:
key expected=(18446744073709551606, 128, ...)- 在 Btrfs 内核源码中,
18446744073709551606就是-10(即BTRFS_CSUM_TREE_OBJECTID),而键类型128代表BTRFS_EXTENT_CSUM_KEY。 - 结论:我们的数据块校验和树在物理扇区上写坏了!
为什么会坏?因为之前系统运行中,Containerd(Docker 的底层运行时)正在往 /var/lib/containerd 写入一个 Prisma client 的 index.js 缓存文件。写入中途,Btrfs 撞上了元数据饥饿的南墙,无法分配元数据块,写入事务异常中断,校验和树在那个瞬间被强行写成了畸形。
这导致了恶性循环:
系统启动 -> 尝试挂载系统并重放日志/提交延迟引用(delayed refs) -> 读到损坏的 csum 树节点 -> 触发 tree first key mismatch -> 事务被迫 Abort -> 强制进入只读模式 -> 引导失败,挂掉。
第三幕:神魔大战——Live 下的死地求生
现在我们面临两个敌人:校验和树损坏(导致无法挂载为 rw 模式)和元数据空间耗尽(导致无法执行任何写操作和删除快照)。
为了解决校验和树损坏,我尝试使用备用树根挂载:
sudo mount -o rescue=usebackuproot /dev/nvme1n1p2 /mnt/root虽然这让我以 rw 挂载成功,但一旦尝试使用 btrfs subvolume delete 删除包含坏块的旧快照,挂载又瞬间崩溃变成 ro。
既然校验和树已经坏了,而且它是用于校验文件数据完整性的,我们只能使出杀手锏:重建校验和树(csum tree)。
步骤 1:祭出大杀器,重建校验和树
在确保分区未挂载的状态下,执行:
sudo btrfs check --init-csum-tree /dev/nvme1n1p2这个操作非常精准,它会把损坏的校验和树彻底抹去,然后扫描磁盘上所有的文件数据块,重新计算并写入一份完全健康的全新校验和树。由于文件内容和目录结构(fs roots)完好无损,因此这个操作对于个人数据是安全的。
几分钟的扫描后,终端弹出了 free space cache v2 cleared,重建成功!
步骤 2:成功挂载,斩除病灶
校验和树重建后,系统终于能稳定地以 rw 模式挂载了!我立刻挂载了 @ 子卷:
sudo mount -o subvol=@ /dev/nvme1n1p2 /mnt/root接着,把包含旧坏块的 4 个自动快照以及那个罪魁祸首的 containerd 缓存文件彻底删除:
# 删除 4 个系统快照
sudo btrfs subvolume delete /mnt/root/.btrbk_snapshots/@.20260708T0000
sudo btrfs subvolume delete /mnt/root/.btrbk_snapshots/@.20260713T2200
sudo btrfs subvolume delete /mnt/root/.btrbk_snapshots/@.20260713T2202
sudo btrfs subvolume delete /mnt/root/.btrbk_snapshots/@.20260713T2204
# 删除损坏的 containerd 缓存文件
sudo rm -f /mnt/root/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/78/fs/app/node_modules/prisma/prisma-client/generator-build/index.js同时在 @log 子卷中删除了两个没有正常关闭、留下校验和错误的归档日志:
sudo rm -f /mnt/root/journal/.../*.journal~步骤 3:乾坤大挪移——Btrfs 空间大平衡
虽然垃圾删了,但 Unallocated 仍然只有 9MB,因为空间还被空的数据 Chunks 霸占着。我们需要通过 Balance 逼迫 Btrfs 释放空闲的 Data Chunks。
为了避免空间不足导致 Balance 报错,我先做了一次温和的 10% 过滤平衡:
sudo btrfs balance start -dusage=10 /mnt/root成功释放了 1GB 的未分配空间! 有了这 1GB 作为缓冲,我立刻启动了更强力的 50% 过滤平衡:
sudo btrfs balance start -dusage=50 /mnt/root伴随着磁盘狂飙,58 个 Chunks 被成功重定位!再次用 btrfs filesystem usage 查看:
Device allocated: 61.94GiB
Device unallocated: 57.01GiB <-- 变成了 57G!
元数据空间的绞刑架被彻底拆除,/ 分区恢复了完美状态!
结语:这次战役给我们的教训
这是一次典型的由元数据空间不足引发的文件系统灾难。Btrfs 固然强大,但在没有预留足够 Unallocated 空间的情况下,伴随着 Docker 这种会产生海量碎片和写操作的软件,加上定期快照的锁定,随时可能触发类似的树损坏灾难。
为了防止历史重演,也为了让 128G 的小 SSD 能长治久安,我已经做好了开机后的待办清单。