我的歌单

btrfs_rescue_blog

发表于
更新于
字数: 1.1k
时长: 11m
阅读: -
玉门

关于我的 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 -hbtrfs 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 能长治久安,我已经做好了开机后的待办清单。