Btrfs 只读事件

来自Arch Linux 中文社区 维护者 Wiki
跳转到导航 跳转到搜索

概述

2023年6月30日中午,编译机上的 Grafana 发出告警邮件,称某个监控项无数据了。晚些时候,由于收到多封告警邮件,开始检查情况。在退出 zsh 写命令历史时出现「read-only filesystem」的字样,lilydjwg 意识到它的 btrfs 文件系统成为强制只读状态。随即造成无法正常运行服务。[1]

2023年7月1日晚间22:00左右,在新购的机器上恢复了老编译机上传来的快照的方式,编译机复活,恢复服务。

2023年7月7日凌晨04:00左右,老编译机上尝试死马当活马医的方案,恢复了 btrfs 。

时间线

时刻 事件・操作 参考
2023-06-30 午间 Grafana 报警称某个监控项无数据,但面板显示有正常数据
2023-06-30 晚上 lilydjwg 更多 Grafana 告警邮件。退出 zsh 时发现 read-only filesystem ,察觉到 btrfs 被强迫只读了
2023-06-30 19:17 lilydjwg 请求 phoenixlzx 进入 archiso
2023-06-30 19:38 phoenixlzx 登入系统确认到 tty 大量只读导致的报错
2023-06-30 19:44 phoenixlzx 从 iDRAC 尝试挂载虚拟光盘重启进入 Archiso ,未识别到引导
2023-06-30 19:44 phoenixlzx archiso 的引导失败导致引导进入正常系统,依旧 ro 。此前或此时可能有一阵可写,快照脚本产生了额外快照并在 ro 前试图删除快照
2023-06-30 19:46 phoenixlzx 从荷兰再次尝试进入 archiso ,引导 copytoram
2023-06-30 20:49 lilydjwg 成功登入 archiso
2023-06-30 20:53 lilydjwg 初步尝试修复未果, farseerfc 建议尝试 clear_cache 和 zero-log 腾出一部分 metadata 空间,也未成功 见详述
2023-06-30 21:28 farseerfc 开始深入调查和更多尝试 见详述
2023-07-01 00:17 farseerfc 因为穷尽了比较稳妥的恢复方案,暂停尝试恢复,优先备份,开始发送最新快照(2023-06-30-18)到 vultr.farseerfc.me
众人 检讨更换机器
2023-07-01 01:35 phoenixlzx 下单 hetzner 新机
2023-07-01 02:14 farseerfc 发现新机的 hetzner 的 rescue system 里面自动 installimage 装了 archlinux minimal ,需要重新进入 rescue system ,暂时搁置
2023-07-01 14:33 phoenixlzx 以不自动装系统的模式重新进入 rescue system
2023-07-01 15:03 farseerfc 在新机上通过 installimage 重新安装 archlinux
2023-07-01 15:38 farseerfc 启动进入新机上的 archlinux 中,开始发送快照到新机上
2023-07-01 18:09 farseerfc 新机上的接收进度超过 vultr.farseerfc.me 上接收的量,所以停下了 vultr.farseerfc.me 上的发送
2023-07-01 20:47 farseerfc 新机上接收完毕,在快照基础上配置网络、微码、 grub 、 fstab
2023-07-01 21:34 farseerfc 新机上重启到新的根,新编译机复活
2023-07-01 21:48 lilydjwg 配置自动快照脚本,lilac 开始加班
2023-07-01 21:57 felixonmars 新机上开启 bees , build.archlinuxcn.org DNS cname 指向新机。TTL更新后编译机开始正常服务
2023-07-02 11:34 farseerfc 老机上继续尝试死马当活马医的方案,试图保留数据挪动分区,可惜挪动分区后状态依旧 见详述
2023-07-02 22:37 lilydjwg 发现新机失联
2023-07-02 23:57 phoenixlzx 向 hetzner 要求 KVM Console
2023-07-03 00:15 phoenixlzx 获得新机的 KVM Console 登入信息
2023-07-03 00:22 farseerfc 发现新机 KVM Console 没有视频信号,也没有重启权限,询问 phoenixlzx 有没有办法重启
2023-07-03 00:25 phoenixlzx 重启新机
2023-07-03 00:26 farseerfc 视频信号显示处于 BIOS Setup 界面,实施正常启动
众人 调查失联原因,没有明显证据解释失联,怀疑过热
2023-07-04 13:46 lilydjwg 观测到老机发送到新机上的快照有包数据库损坏的现象,猜测老机故障重启后有一段时间可写,并创建了快照。在新机上手动修复
2023-07-06 23:22 farseerfc 在老机上继续尝试死马当活马医 见详述
2023-07-07 04:05 farseerfc 成功恢复老机上的 btrfs,能够正常 balance 和正常写入 见详述

事发时现象

编译机一直以来有个问题:硬盘有多少用多少。前一任编译机用的 1T 硬盘,是刚刚够用,现在换 2T 了,结果 btrfs filesystem usage 一看就发现,空闲200多G数据是真的(因此没有触发相关报警),但是元数据满了,也没有未分配空间了。

编译机的 btrfs 配置是由两块 NVMe SSD 构成,data 使用 RAID0 ,metadata / system 使用 RAID1 ,事发时的占用情况如下:

# btrfs filesystem usage /mnt
Overall:
     Device size:                   1.72TiB
     Device allocated:              1.72TiB
     Device unallocated:            2.09MiB
     Device missing:                  0.00B
     Device slack:                 20.00GiB
     Used:                          1.49TiB
     Free (estimated):            238.78GiB      (min: 238.78GiB)
     Free (statfs, df):           238.78GiB
     Data ratio:                       1.00
     Metadata ratio:                   2.00
     Global reserve:              512.00MiB      (used: 0.00B)
     Multiple profiles:                  no

Data,RAID0: Size:1.60TiB, Used:1.36TiB (85.40%)
    /dev/nvme0n1p3        817.50GiB
    /dev/nvme1n1p2        817.50GiB

Metadata,RAID1: Size:64.00GiB, Used:63.30GiB (98.91%)
    /dev/nvme0n1p3         64.00GiB
    /dev/nvme1n1p2         64.00GiB

System,RAID1: Size:8.00MiB, Used:144.00KiB (1.76%)
    /dev/nvme0n1p3          8.00MiB
    /dev/nvme1n1p2          8.00MiB

Unallocated:
    /dev/nvme0n1p3          1.05MiB
    /dev/nvme1n1p2          1.05MiB

可见 data profile 尚有 245G 可用空间,而 metadata profile 仅剩约 716M 可用,不再有 Unallocated 导致不能再分配新的 metadata 块组。

此时可以 ro 方式挂载,在 ro 方式挂载时读取数据一切正常,然而一旦尝试 mount -oremount,rw 就会在一两分钟内变为只读,同时内核 dmesg 中报错说由于 metadata 空间不足导致 transaction 提交失败。

在几次 umount 和 mount 的过程中, farseerfc 发现报错强制变为 ro 后,metadata 的空间并没有完全用满,反而是 Global Reserve 空间接近满了:

# btrfs filesystem usage /mnt
Overall:
     Device size:                   1.72TiB
     Device allocated:              1.72TiB
     Device unallocated:            2.09MiB
     Device missing:                  0.00B
     Device slack:                 20.00GiB
     Used:                          1.49TiB
     Free (estimated):            238.78GiB      (min: 238.78GiB)
     Free (statfs, df):           238.78GiB
     Data ratio:                       1.00
     Metadata ratio:                   2.00
     Global reserve:              512.00MiB      (used: 511.48MiB)
     Multiple profiles:                  no

Data,RAID0: Size:1.60TiB, Used:1.36TiB (85.40%)
    /dev/nvme0n1p3        817.50GiB
    /dev/nvme1n1p2        817.50GiB

Metadata,RAID1: Size:64.00GiB, Used:63.30GiB (98.91%)
    /dev/nvme0n1p3         64.00GiB
    /dev/nvme1n1p2         64.00GiB

System,RAID1: Size:8.00MiB, Used:144.00KiB (1.76%)
    /dev/nvme0n1p3          8.00MiB
    /dev/nvme1n1p2          8.00MiB

Unallocated:
    /dev/nvme0n1p3          1.05MiB
    /dev/nvme1n1p2          1.05MiB

Global reserve 用于安排一次 transaction 中写入的 metadata profile 的量,Global reserve 用满导致 transaction 不能正常提交,从而被强制转入 ro 。

本次问题解决难点在于 data 和 metadata 分别为 RAID0 和 RAID1 profile ,每次块组分配时需要在不同设备上同时分配各一块连续空间,所以需要在一个 transaction 内同时增加两个额外设备解决空间不足的问题。

恢复过程详述

初步尝试

首先尝试加个设备再 balance 一下,结果由于文件系统只读,添加设备失败。尝试 remount rw,btrfs 说文件系统的只读原因是因为报错,所以不支持 remount rw。

由于这个 btrfs 文件系统挂载为根目录,并不能在线卸载,只能尝试离线修理。重启后通过 iDRAC 进入 archiso 并尝试抢在报错之前添加两个设备。btrfs 占用的两个分区前各有一个 40G 左右的 swap 分区,此时没有被使用,可以用来添加设备。但是添加设备并没有成功。会卡住一会儿,然后报错「No space left on device」。按 farseerfc 的建议,clear_cache 和 zero-log 都试过了,但并没有解决问题。有人建议把大文件 truncate 一下,看看能不能刚好释放出 1G 的连续空间出来,但是编译机上有定时快照,truncate 了也不会立即释放空间。

深入调查

如上述事发时的现象所述,故障的直接原因是

  1. metadata 空间不足,没有 unallocated 可以分配新的 metadata
  2. global reserve 空间不足以安排一次 transaction 的提交

为缓解 1. metadata 空间不足,解决故障的一般性方案是通过 device add 增加新的设备,或者 filesystem resize 扩展现有设备的大小。而直接原因 2. global reserve 导致了 device add 添加新设备的尝试不成功。

这种情况下,为了压榨出一些额外的 metadata 空间,做过如下尝试:

1. zero-log

通过 btrfs rescue zero-log 扔掉日志树(tree log tree),可空出日志树所占用的一点空间。日志树记录着最新一次 transaction 之后,对文件系统进行的写入和 fsync 操作日志,扔掉这部分内容会导致文件系统状态回到最近一次 transaction 提交时的状态。

后续挂载时通过挂载选项 notreelog 可避免在挂载时重建 tree log tree 。

2. clear_cache,nospace_cache

通过挂载选项 clear_cache (或者 btrfs check --clear-space-cache v1|v2 命令),可以清理掉 free space cache (v1) 或者 free space tree (v2) 。free space cache (v1) 是 root tree 中的几个特殊 inode ,可能占用不了多少空间,而 free space tree (v2) 是独立的 btree ,理论上最多可以占用 1bit/4KiB 容量的空间。

后续挂载时通过挂载选项 nospace_cache 可避免在挂载时重建 free space cache / free space tree 。

3. nossd

SSD 优化试图先(在内存中)分配连续的一块地址区间,然后在这连续地址内进行小块分配。关闭 ssd 优化虽然会牺牲性能,不过潜在允许分配更零碎,见缝插针地使用 metadata profile 中所剩无几的空间。


结合如上挂载选项,仍未能避免问题。在尝试 device add 时可以看到内核中的类似如下的输出:

[  +0.000008] BTRFS info (device nvme0n1p3: state A): dumping space info:
[  +0.000007] BTRFS info (device nvme0n1p3: state A): space_info DATA
has 256392597504 free, is not full
[  +0.000007] BTRFS info (device nvme0n1p3: state A): space_info
total=1755577581568, used=1499184852992, pinned=0, reserved=0,
may_use=0, readonly=131072 zone_unusable=0
[  +0.000010] BTRFS info (device nvme0n1p3: state A): space_info METADATA has -540672 free, is full
[  +0.000006] BTRFS info (device nvme0n1p3: state A): space_info total=68719476736, used=67883679744, pinned=355762176, reserved=479903744, may_use=540672, readonly=131072 zone_unusable=0
[  +0.000009] BTRFS info (device nvme0n1p3: state A): space_info SYSTEM has 8208384 free, is not full
[  +0.000006] BTRFS info (device nvme0n1p3: state A): space_info total=8388608, used=147456, pinned=32768, reserved=0, may_use=0, readonly=0 zone_unusable=0
[  +0.000008] BTRFS info (device nvme0n1p3: state A): global_block_rsv: size 536870912 reserved 540672
[  +0.000006] BTRFS info (device nvme0n1p3: state A): trans_block_rsv: size 0 reserved 0
[  +0.000005] BTRFS info (device nvme0n1p3: state A): chunk_block_rsv: size 0 reserved 0
[  +0.000005] BTRFS info (device nvme0n1p3: state A): delayed_block_rsv: size 0 reserved 0
[  +0.000004] BTRFS info (device nvme0n1p3: state A): delayed_refs_rsv: size 14152892416 reserved 0
[  +0.000010] BTRFS: error (device nvme0n1p3: state A) in __btrfs_free_extent:3053: errno=-28 No space left
[  +0.001688] BTRFS info (device nvme0n1p3: state EA): forced readonly

最为关键的是 space_info METADATA has -540672 free, is full 这里指出,出现问题时 metadata 的缺口可能仅有 528K,33 个 metadata leafnode 大小。

另外如果尝试 rw 挂载并不做任何事情,等待内核自动提交事务,类似的输出中可发现 space_info METADATA has -32768 free, is full ,缺口仅有 32K , 2 个 leafnode 。


每次经过上述尝试之后,都需要 umount 卸载文件系统。好在 btrfs 使用的事务更新机制,每次卸载 btrfs 之后,状态都恢复到了出事时那一刻,在 btrfs 层面无论做什么尝试都不会再进一步毁坏数据。

以及为了打时间差,上述尝试使用以下这样的操作:

$ umount /mnt
$ mount -oro,nologreplay,... /dev/nvme0n1p3 /mnt   # 首先进行 ro 挂载,加上上述选项
$ mount -oremount,rw /mnt ; btrfs device add /dev/nvme0n1p2 /dev/nvme1n1p1 /mnt # 转为 rw 挂载后立刻实施操作

可惜都未能赶在 global reserve 用完 512M 前添加上设备。

迁移

因短时间内无法原地修复 btrfs,直接下单了位于 Hetzner 的新机器,并使用 btrfs send 迁移。7月1日晚22:00左右,服务恢复。

死马当活马医:保留数据挪动分区

前述恢复方案中使用 btrfs device add 而非 btrfs filesystem resize 是因为可用的空间(原本的 SWAP 分区)位于 btrfs 分区之前。

由于内核日志中显示的 metadata 缺口只有约 528K ,考虑 filesystem resize 所需额外 metadata 是否可能小于 device add 所需 metadata 空间,于是尝试挪动分区。

# cfdisk /dev/nvme0n1  # 缩小 nvme0n1p2 ,划出 10G 空间
# cfdisk /dev/nvme1n1  # 缩小 nvme1n1p1 ,划出 10G 空间
# echo "+,+" | sfdisk --move-data -N 3 /dev/nvme0n1  # 将 nvme0n1p3 向前扩展到填满空闲空间,并移动数据
# echo "+,+" | sfdisk --move-data -N 2 /dev/nvme1n1  # 将 nvme1n1p2 向前扩展到填满空闲空间,并移动数据

挪动分区后,正常挂载时,如前依旧会在一两分钟内转入 ro 。

尝试 btrfs rescue fix-device-size ,未能通过修复的方式改写设备大小。

然后尝试 btrfs filesystem resize max:

$ mount -oro,nologreplay,nossd,skip_balance,nospace_cache,notreelog /dev/nvme0n1p3 /mnt
$ mount -oremount,rw /mnt ; btrfs filesystem resize max /mnt

这次尝试后,能成功 resize 一个设备,在尝试 resize 第二个设备前 transaction 提交失败,转入 ro 状态。此后 btrfs filesystem usage 显示的利用率如下:

Overall:
     Device size:                   1.73TiB
     Device allocated:              1.72TiB
     Device unallocated:           10.00GiB
     Device missing:                  0.00B
     Device slack:                 10.00GiB
     Used:                          1.49TiB
     Free (estimated):            248.79GiB      (min: 243.79GiB)
     Free (statfs, df):           248.78GiB
     Data ratio:                       1.00
     Metadata ratio:                   2.00
     Global reserve:              512.00MiB      (used: 511.48MiB)
     Multiple profiles:                  no

Data,RAID0: Size:1.60TiB, Used:1.36TiB (85.40%)
    /dev/nvme0n1p3        817.50GiB
    /dev/nvme1n1p2        817.50GiB

Metadata,RAID1: Size:64.00GiB, Used:63.20GiB (98.76%)
    /dev/nvme0n1p3         64.00GiB
    /dev/nvme1n1p2         64.00GiB

System,RAID1: Size:8.00MiB, Used:144.00KiB (1.76%)
    /dev/nvme0n1p3          8.00MiB
    /dev/nvme1n1p2          8.00MiB

Unallocated:
    /dev/nvme0n1p3         10.00GiB
    /dev/nvme1n1p2          1.05MiB

根源问题依旧是 global resize 用满导致 transaction 提交无法继续。

死马当活马医:尝试 6.4.1 内核版本

6.4.1 内核相比 6.3.x 内核系列,在 global resize 计算方面多了两个修正:

  1. 修复 global reserve 空间预估时,unlink 所需开销的计算方式:unlink 所需开销从 5.19 开始变大了 10 -> 12 ,使得 global reserve 空间预估可能不准确。
    1. btrfs: remove check for NULL block reserve at btrfs_block_rsv_check()
    2. btrfs: simplify variables in btrfs_block_rsv_refill()
    3. btrfs: fix calculation of the global block reserve's size
    4. btrfs: use a constant for the number of metadata units needed for an unlink
    5. btrfs: calculate the right space for delayed refs when updating global reserve
  2. 修复 global reserve 空间预估中可能的 qgroup 空间泄漏(leak)
    1. btrfs: don't free qgroup space unless specified

是否是因为 global reserve 空间预估不准确,导致内核在一个事务中过度自信地过度提交(overcommit)了太多 metadata 写入,导致 global reserve 空间用超?

由于 20230701 的 archiso 中仍未带入 6.4.1 内核,所以构建了新 archiso ,从 iDRAC 引导进新的 archiso 中,重复尝试上述 filesystem resize max 。然而情况并未改善。

死马当活马医:改大 global reserve

搜到 linux-btrfs 邮件列表中 5.0.9 上一个非常类似的问题报告:Global reserve and ENOSPC while deleting snapshots on 5.0.9

其中提到 global reserve 用尽可能的原因是, btrfs 最近的 transaction 中记录下了未完成的 subvolume delete 操作,挂载后 btrfs-cleaner 在后台试图继续删除 subvolume ,导致 global reserve 用尽从而无法正常 device add 。

确认到老编译机上确实有未完成的 orphan subvolume :

# btrfs-orphan-cleaner-progress /mnt
1 orphans left to clean
dropping root 36480 for at least 0 sec drop_progress (439534 EXTENT_DATA 0)

解决方案似乎只有 patch btrfs 代码,直接改大 global reserve 的常数大小限制:

diff --git a/fs/btrfs/block-rsv.c b/fs/btrfs/block-rsv.c
index ac18c43fadad..b82b954fcbf4 100644
--- a/fs/btrfs/block-rsv.c
+++ b/fs/btrfs/block-rsv.c
@@ -368,7 +368,7 @@ void btrfs_update_global_block_rsv(struct btrfs_fs_info *fs_info)
        spin_lock(&sinfo->lock);
        spin_lock(&block_rsv->lock);

-       block_rsv->size = min_t(u64, num_bytes, SZ_512M);
+       block_rsv->size = min_t(u64, num_bytes, SZ_2G);

        if (block_rsv->reserved < block_rsv->size) {
                num_bytes = block_rsv->size - block_rsv->reserved;
成功添加上设备时的截图

改到 2G 的 global reserve ,重新编译内核,重新制作 Archiso 使用新内核,然后让 iDRAC 使用新 Archiso 启动。

这次,成功进行了 filesystem resize max 和 device add 。正常 btrfs balance -duage=10 之后 device delete 也如期完成。

btrfs 恢复正常。

经验感想

独立服务器的带外远程控制,感觉都挺难用的

老编译机提供了 Dell 的 iDRAC 管理,可惜获得的账户权限很低,很多 iDRAC 应有的操作(比如安排下一次启动时的虚拟光盘,比如远程重启)都不能在给予的用户权限下进行,并且给予的用户访问时间有限。

另外还有个挺严重的问题是原本应该能从虚拟光盘启动,但是不知为何,phoenixlzx 和 farseerfc (皆位于日本)的网络环境下,从虚拟光盘启动都会失败。解决方案是找一台距离老编译机网络环境较近的 VPS ,从 VPS 开启 iDRAC 并且挂载虚拟光盘。这样能正常启动,只不过 archiso 的 copytoram 仍然耗时良久(半小时)。花在启动 archiso 的时间上很多,并且可能因为没能启动 archiso 导致系统重启到正常系统了一次,产生了删除快照和创建快照的写入后让故障更难恢复了。

新编译机没有 iDRAC ,需要申请 KVM Console 访问,获得的 KVM Console 也不能操作电源状态。hetzner 提供的安装系统的方式是 rescue system 基于 debian ,默认的 archlinux minimal 模板不太能符合需求,好在 installimage https://github.com/hetzneronline/installimage 开源并且还算好用。

btrfs 的 global reserve 机制,比我之前的理解更为复杂

global reserve 存在的目的,似乎是为了节流限制单次 metadata 写入的量,所以默认 global reserve 不能太大。

目前代码中上限被硬编码到 512M ,想要提高限制的唯一方案是重新编译内核,提高了修改它的门槛。

虽然有 /sys/fs/btrfs/<FSUUID>/allocation/global_rsv_reserved 但是似乎只能读不能写。

就在最近 6.4 内核中,这方面还有挺大的改动和修复。

不过这次遇到的情况是 delayed ref 太多导致 global reserve 用完, delayed ref 是在对 btree 子节点做 CoW 时,从叶子节点递归往上产生的,属于没法预知会产生多少那一类的,所以也很难说这里的问题是计算上的 bug 还是设计如此没有办法避免。

一个可能的设计上的改进方式是计划中的 extent tree v2 ,准备在 extent tree 中只记录 data extent 而跳过 metadata extent ,从而能切断很多 CoW 导致的递归写入。 extent tree v2 将会是未来一两年 btrfs 开发的大工程,希望同时能改善这次这种极端问题。

btrfs 的 auto bg reclaim 还没正式投入使用

5.12 内核为 zoned 设备引入了 auto blockgroup reclaim ,在后台默默检查 bg 的使用率,低于一定阈值之后自动触发 balance 机制。

5.13~5.18 在为 auto bg reclaim 实施微调。

5.19 内核将 auto bg reclaim 公开给非 zoned 的一般设备。记得当初确认时有 50% 的阈值,不过印象中好像触发机制有些不符合期待。

事发的 6.3 内核中 /sys/fs/btrfs/<FSUUID>/allocation/data/bg_reclaim_threshold 默认阈值为 0 ,没有开启 auto bg reclaim ,在 6.4 内核上依旧是 0 ,默认还未启用 auto bg reclaim 特性。

值得尝试一下启用这个会有什么效果,以及是否会产生大量写入放大,平白损耗 SSD 寿命。

还是需要监控 metadata 大小

监控的对象应该是 min(unallocated by device) + metadata.unused ,当它小于 1G 的时候(足够大的设备上一个 blockgroup 的大小)就需要人工处理了。当它小于 512M (global reserve) 的时候就非常危险,需要立刻处理。

处理的方式还是 btrfs balance -dusage=xx ,这里需要注意目的是整理 data profile 腾出 unallocated ,所以不要加 -m 参数。

另外 btrfsmaintenance 项目中有 btrfs-balance.timer 也可尝试。

参考资料

脚注

  1. 后检查日志发现下午两点(UTC+8)多开始 vnstat 零星报告写入耗时达数秒,Grafana 偶有 SQLite 数据库被锁定的报错,journald 服务经常因无响应而触发 watchdog 重启服务。在更晚些的时候,journald 停止服务超时,此时日志变得异常混乱。