7
25
2023
2

btrfs 元数据满了怎么办

上一篇《btrfs 翻车记》记叙了我们服务器上的 btrfs 出事的情况,好像吓到一些用户了 QAQ。其实那次情况比较特殊啦。一般来说,就算元数据用满了,也不至于改内核代码才能救回来。不过元数据满的问题确实困扰了许多用户,正好这些天群里有不少人遇到了,本文就记录一下元数据满了之后如何处置。

问题和处置

问题的现象是部分文件操作报错「No space left on device」,但是 df 等工具明明报告还有空间。btrfs filesystem usage 的输出是这样:

btrfs filesystem usage 截图

我们可以看到,还有 373G 的空闲空间(Free)呢。但是呢,「Device unallocated」已经不足 1G 了。在充分大的文件系统上,btrfs 会以 1G 为单位来分配块组(block group,简称 bg)。所以现在这个情况,已经无法分配新的 bg 啦。然后我们再往下看,「Metadata」的部分,总共 4.5G,已经用了 4G。还剩下 512M,刚好是 Global reserve 的大小。也就是说,不算保留空间的话,元数据已经没有空间可用了,所以才会报错。

那现在怎么办呢?如果文件系统上有不需要的很大的文件,并且没有快照,删除后空间可以立即释放的话,可以删除试试,看看能不能刚好空出来一个 bg。不然就试着跑一下 balance,像这样:

btrfs balance -dusage=0 截图

这个命令是说,把使用率为 0% 的数据 bg 整理一下。从输出「had to relocate 0 out of ...」可以看出,没有这样的 bg,操作没有效果。可以试试增加 -dusage 的值,看看能不能成功。很遗憾,这个案例中未能成功:

btrfs balance -dusage=1 截图

那只有另外添加一些空间来腾挪数据了。如果有闲置的分区(或者暂时用不上的 swap 分区)就可以拿来用。不然的话插个U盘也行。不需要多大,几个 G 就行。挪好数据就可以去掉了,不会长期使用的。另一种很有风险的做法是使用内存来暂存数据,但这样一旦死机或者断电,整个文件系统就完蛋了,不建议使用。

准备好空闲分区后,就可以 btrfs device add 添加上去了。这里通常要加 -f 参数,抹除分区里原有的数据。注意不要添加错设备了哦。

btrfs device add 截图

可以看到,设备添加上去之后,又可以往文件系统里写数据了。接下来跑 btrfs balance -dusage=10 之类的命令腾些数据 bg 出来就好了。这里要注意只 balance 数据 bg,不要动元数据的 bg,因为元数据越是集中存放,将来就越可能需要分配新的 bg,就越有可能遇到没 bg 可以分配的情况。

btrfs-heatmap 工具可以查看 bg 的分布和使用情况。以下是 balance 好之后的状态(使用 --curve linear 参数):

btrfs heatmap

图中,白色的是数据 bg,蓝色的是元数据 bg。颜色越亮,使用率越高。纯黑的是未分配空间。可以看到,这里有大量用得不多的数据 bg。balance 操作就是把它们给合并了一些,空出来不少黑色区域(最下方的黑色部分是新添加的设备上的未分配空间)。这是 usage 截图:

btrfs filesystem usage 截图

算一算,除去新添加设备的空间,原存储设备上也能有 46G 的未分配空间了(看上去新添加设备并没开始使用,只是让 btrfs 相信它有足够的空间用)。接下来把之前添加的设备删除就可以了:btrfs device del /dev/sdb1 /。等它运行完毕就可以拔掉该设备了(如果是可移动介质的话)。

预防

这个问题的本质就是 bg 的碎片化导致明明看上去有空间,但是元数据用不了,因此报错,需要手动处理。要识别即将出问题的文件系统也很简单:btrfs filesystem usage 看一看,如果「unallocated」很小(不足 1G)就要赶紧 balance 一下了(当然前提是有不少碎片化的空闲空间)。注意,这个时候千万不要删快照!删快照可能会快速消耗保留的元数据空间,从而导致添加设备都加不上、还报错只读的情况。

btrfs 最近添加了自动块组回收(automatic block group reclaim)功能,但默认并没有启用。因为是新功能,可能会有 bug,你也不知道它会什么时候运行,所以我暂时不建议使用。自己写个定时脚本,在系统空闲的时候运行也不错的。


本文中的图像素材均由遇到问题的群友提供。

Category: Linux | Tags: linux btrfs
7
6
2023
3

btrfs 翻车记

如标题所言,我用了多年的 btrfs,终于还是遇到翻车啦。由于文件系统翻车了,相关日志内容缺失,所以本文我仅凭记忆描述事件,就不提供准确的日志输出了。

事件经过

出事的是 archlinuxcn 的编译机。那天中午时分我就收到了 Grafana 给我发送的莫名其妙的报警邮件,称某个监控项无数据了。我去面板上瞅了半天,明明有数据的啊。不解,但是忙别的事情去了,也没有细究。晚些时候,我又收到了好些同类告警,遂登录机器打算检查 Grafana 日志。但操作过程中,退出 zsh 的时候我好像看到了写命令历史时出现「read-only filesystem」的字样?于是检查了一下,发生大事不好了,文件系统真变只读了!

这个 btrfs 是我上次迁移机器的时候换上的,因为我觉得过了这么多年,btrfs 挺稳定了啊,而定时快照很方便,devtools 也支持通过快照来快速创建打包用的 rootfs(虽然大部分时候都在 tmpfs 上打包了用不上)。但我们编译机一直以来有个问题:硬盘有多少用多少。前一任编译机用的 1T 硬盘,是刚刚够用,现在换 2T 了,结果用着用着又只剩下200多GiB啦……肥猫最近开始玩 bees 去重了,听说在其它机器上效果显著。不过这台编译机快照多,这「蜂群」嗡嗡了几天都没完事,还出事了。

btrfs filesystem usage 一看就发现,空闲200多G数据是真的(因此没有触发相关报警),但是元数据满了,也没有未分配空间了。这种事群里有多位群友遇到过了,问题不大,加个设备再 balance 一下就好了。我一开始是这么想的,刚好有两个挺大的 swap 分区能够用来腾挪。结果 btrfs 告诉我,文件系统只读,添加设备失败!那好,我 remount rw 一下。结果 btrfs 说文件系统出毛病了,不支持 remount rw!

这个时候我才感觉事情有点难办了。这个 btrfs 文件系统是 /,并不能卸载啊。没有找到在线修理的办法,只好呼叫凤凰卷,通过 iDRAC 进入 archiso。期间服务器重启了一次,但连过去依旧是只读的。进到 archiso 之后,尝试抢在报错之前添加设备,但是并没有成功。会卡住一会儿,然后报错「No space left on device」。按 farseerfc 的建议,clear_cache 和 zero-log 都试过了,但并没有解决问题。有人建议把大文件 truncate 一下,看看能不能刚好释放出 1G 的连续空间出来,但是我有定时快照呀,truncate 了也不会立即释放空间。最后卷直接下单了新机器,开始 btrfs send……

事后

服务器迁移还比较顺利。数据接收完毕,网络配置更新一下,引导器装好,重启,熟悉的编译机就回来啦~除了 nvchecker 好像跑得有点慢?怎么 ping Google 要 60ms 的?原来是忘记更新 /etc/resolv.conf 了,里边还写着旧 ISP 的 DNS 服务器地址呢。systemd-resolved 这次做了回好事,把 DNS 服务 fallback 到了 9.9.9.9。DNS 解析慢是 fallback 过程造成的,而 ping Google 延迟高,是因为 9.9.9.9 不知道怎么回事,给解析到比较远的地方去了。

新编译机 CPU 比之前那台快了不少,硬盘也增加到了 3.4T。挺好的,除了这时机不太好,旧编译机还有近一个月到期……另外由于是突发状况,所以没有及时缩短 DNS TTL,导致迁移完成之后 DNS 解析没有及时跟上(隔天我陆陆续续从另一台使用这台编译机转发邮件的机器那里收到了好些邮件,都是抱怨这编译机连不上的)。

蜂群(bees)也重新开始工作了。这次快照较少,我还专门为了它们暂停过自动快照,过了一段时间之后首次扫描终于完成了。之后它们就能很快跟上进度,不会消耗大量 CPU 了。

我添加了定时任务来执行 btrfs balance start -dusage=10 /,每周释放一些使用率低的数据块组,避免空间分配了又不怎么用,到最后明明有剩余空间却让元数据无处可写。

farseerfc 对出事的 btrfs 进行了更多不同方案的修复尝试,但依然未能修好。

一些抱怨

没想到我用了这么多年的 btrfs,还是被坑到了。明明还有不少空闲空间,但是 btrfs 不知道用。我看到最近有个「automatic block group reclaim」特性,支持自动回收块组了,但是搜索结果第一项结果是今年年初有人在邮件列表上报告说它有问题……出现问题 ro 挺好的,但是这个状态下不让进行维护操作就太难受了。作为 / 文件系统使用时,对于远程机器来说,即使有 iDRAC 或者 IPMI 之类的东西,用起来也费事,还不得不中断可能还活着的服务。而对于不支持远程访问的机器就更麻烦了,比如在家办公时办公室的机器,或者出差旅游探亲时在家的机器。我也考虑过在 initramfs 里配网络、开 sshd,但是并没有现成的工具,事发时再配的话,一次性成功的可能性太低了。

至于事发原因,蜂群(bees)只是加快了元数据空间的使用(dedupe 快照的结果),其本身并没有问题。出事重启之后,在再次被挂载为只读之前,还是写入了不少数据,包括一次成功的快照(后来查 pacman 数据库损坏的问题时发现的)。这可能是后续添加设备都无法成功的原因。

以前用的 ext4,在文件系统快满时只是碎片化严重、效率降低,它甚至还会给 root 保留一部分空间来处理问题。后来用 zfs,快满了就 0B/s,等于废掉。现在 btrfs 遇到空间不足也没有好太多,变只读了。(我还打算抱怨一下新文件系统可靠性不如旧的来着,想想前不久在群里看到 btrfs 抓到了位反转,还是不抱怨了。大家各有千秋。)


2023年07月08日更新:farseerfc 把它救活了!核心方法是把这里的 global reserve 大小由 512M 改成 2G。因为之前重启了一次,那时不仅成功创建了一个新快照,还删掉了一个旧的。然后它删着删着就把 512M 的 global reserve 给用完了,就报错、事务回滚,于是就过不去了。和邮件列表上这个问题是一样的:Global reserve and ENOSPC while deleting snapshots on 5.0.9 — Linux BTRFS

2023年07月25日更新:其实本文所述内容是罕见情况啦,并没有多少人会遇到的,大家不用害怕。另外新写了一篇《btrfs 元数据满了怎么办》,记录大多数人遇到的元数据满的问题如何解决。

Category: Linux | Tags: linux btrfs
12
29
2013
30

rsync+btrfs+dm-crypt 备份整个系统

生成目录!

目标:增量式备份整个系统

怎么做到增量呢?rsync + btrfs 快照。其实只用 rsync 也是可以做到增量式的1,但是支持子卷的 btrfs 可以做得更好:

  1. 快速删除旧的备份
  2. 更简单的备份逻辑
  3. 子卷可以设置成只读(这是个很重要的优点哦~)
  4. btrfs 支持压缩。系统里有好多文本文件的,遇到压缩效果不好的文件 btrfs 会自动放弃压缩

为什么要备份整个系统呢?——因为配置一个高度定制化的系统麻烦啊,只备份部分数据的话还可能漏掉需要的文件。另一个优点是可以直接启动到某个备份

备份了整个系统,包括各种公开或者隐私的数据,一堆 cookies 和帐号配置,邮件和聊天记录等。难道就不需要加密一下下吗?于是,在 btrfs 之下再加一层 dm-crypt 加密。

介绍完毕,下边进入正题。

准备工作

首先,需要一个足够新的 Linux 内核,因为 btrfs 还是「实验特性」,每个版本都会有大量改进。如果用比较旧的内核就有可能出事。我用的是 3.12.6 版本。其次,安装 rsync 和 cryptsetup。

当然还要准备硬件:一块希捷 BackupPlus 1T USB 3.0 移动硬盘,以及一枚Express Card 34mm 转 USB 3.0 扩展卡,因为我的笔记本没有 3.0 的接口。注意使用 USB 3.0 扩展卡,内核需要载入 pciehp 模块,否则会出现不能识别 3.0 的设备或者后续接上去的设备的情况。Arch 官方内核将这个模块直接编译进内核了,而我自己编译的很不幸没有,只好重新编译了下内核。

PS: 这俩家伙一起配合工作,写入峰值能达到 110MiB/s,比我笔记本自身的硬盘还要快。

然后是分区、格式化。我使用了 GPT 分区表。为了安装 grub 以便启动,最好在开头分配 2M 空间给 grub 使用,不然会很麻烦2。记得给这个分区 bios_grub 标志(GParted「管理标志」里勾上即可)。下一个分区是 ext4 格式的启动分区。我会在这里放一个 Arch Linux Live 系统用于维护任务,以及用于启动到备份的内核和 initramfs。因为备份的分区会被加密,所以必须把内核和 initramfs 放在另外的地方。接下来的一个分区放加密过的备份数据用的。

像这样初始化加密分区:

cryptsetup luksFormat /dev/sdc3

密码要长,但也一定要记住密码,因为除了穷举外是没有办法恢复的。

初始化完毕之后就可以使用密码打开该设备了:

cryptsetup open /dev/sdc3 lilybackup

最后的参数是一个名字,它会是解密后的设备在 /dev/mapper 下的文件名。

如果一切完毕,要记得(在卸载文件系统之后)关闭该设备:

cryptsetup luksClose lilybackup

dm-crypt 是块设备级的加密。我们还要在其上建立文件系统:

mkfs.btrfs /dev/mapper/lilybackup

然后挂载之,并建立相应的目录和子卷结构。比如我的:

* backup (dir)
  * home (dir)
    * current (subvol, rw)
    * 20131016_1423 (subvol, ro)
    * 20131116_2012 (subvol, ro)
    * ...
  * root (dir)
    * current (subvol, rw)
    * 20131016_1821 (subvol, ro)
    * 20131116_2128 (subvol, ro)
    * ...
* run, for boot up directly, with edited /etc/fstab (dir)
  * home (dir)
    * 20131116 (subvol, rw)
    * ...
  * root (dir)
    * 20131116 (subvol, rw)
    * ...
* etc, store information and scripts (subvol, rw)

我把备份数据放到 backup 目录下,/ 和主目录 /home/lilydjwg 分开备份的。每个备份是使用日期和时间命名的快照子卷。除了用于每次同步的 current 目录外其它的子卷都是只读的,以免被意外修改。在 run 目录下是用于直接运行的,可写。这些可以按需建立。

开始备份

这是我备份 / 使用的脚本。是一个 zsh 脚本,这样可以避免 bash 中特殊字符可能带来的问题,虽然 bash 有 shellcheck 可以静态分析出可能有问题的地方。

这个脚本带两到三个参数。第一个是 / 的位置,因为我一般会直接从运行的系统执行备份,但也有可能使用另外的维护系统(比如系统滚挂掉的时候)。第三个参数是用于确认操作的。不加它的话会以 --dry-run 参数来运行 rsync。rsync 很复杂,所以最好先演习一遍以避免不小心手抖了做错事 =w=

为了避免日后对照着 rsync 手册来揣摸每个单字母选项的意义,我在这里全部使用了选项的完整形式。反正我是 zsh 用户,那么长的命令中大部分字符都是 zsh 给我补全出来的 =w=

#!/bin/zsh -e

cd $(dirname $0)

if [[ $# -lt 2 || $# -gt 3 ]]; then
  echo "usage: $0 SRC_DIR DEST_DIR [-w]"
  exit 1
fi

src=$1
dest=$2
doit=$3

if [[ $doit == -w ]]; then
  dry=
else
  dry='-n'
fi

rsync --archive --one-file-system --inplace --hard-links \
  --human-readable --numeric-ids --delete --delete-excluded \
  --acls --xattrs --sparse \
  --itemize-changes --verbose --progress \
  --exclude='*~' --exclude=__pycache__ \
  --exclude-from=root.exclude \
  $src $dest $dry

比较重要的几个 rsync 选项:

--archive
我们要备份,所以请保留所有信息
--one-file-system
只备份这个文件系统的内容,不要跑到 /sys 啊 /proc 啊 /dev 啊 /tmp 这类目录里去了。这也省得自己手动排除
--numeric-ids
文件的所有者信息使用数字而不要解析成用户名/组名。避免在跨系统使用时出差错
--exclude-from=root.exclude
root.exclude文件中读取额外的排除列表
--acls --xattrs
保留文件 ACL 和扩展属性

我发现的 / 里需要排除的目录如下:

/var/cache/*/*
/var/tmp/
/var/abs/local/
/var/lib/mongodb/journal/

其中第一项写成那样是因为,我要保留 /var/cache 下的一级目录。

主目录的备份是类似的过程,只是更加复杂罢了。当我写我的主目录的备份脚本的时候,深切地体会到有圣人说过的一句话——过早的优化是万恶之源。因为我使用 eCryptfs 加密主目录时为了避免可以公开的文件被加密造成性能损失,做了一系列的软链接。它们一直在给我带来各种小麻烦和不爽……

注意这些脚本要以 root 的身份运行。待所有备份脚本跑完之后,对那个 current 子卷做一个只读快照就好了:

sudo btrfs subvolume snapshot -r current $(date +'%Y%m%d_%H%M')

下次要更新备份时是一样的步骤:跑同步脚本,创建新快照。第一次同步会比较慢,跑了一个多小时吧。后边的增量备份就比较快了,十几分钟就好。

从备份启动

要启动,首先把 grub 装过去。把 Arch Linux live 系统的配置写好,当然还有启动备份系统的配置,如下:

search --no-floppy --fs-uuid --set=root 090dcc64-2b6d-421c-8ef6-2ab3321aec62

menuentry "Archlinux-2013.12.01-dual.iso (x86_64)" {
    load_video
    set gfxpayload=keep
    insmod gzio
    insmod ext2

    set isofile="/images/archlinux-2013.12.01-dual.iso"
    echo "Setup loop device..."
    loopback loop $isofile
    echo "Loading kernel..."
    linux (loop)/arch/boot/x86_64/vmlinuz archisolabel=ARCH_201312 img_dev=/dev/disk/by-label/lilyboot img_loop=$isofile earlymodules=loop
    echo "Loading initrd..."
    initrd (loop)/arch/boot/x86_64/archiso.img
}

menuentry "Archlinux-2013.12.01-dual.iso (i686)" {
    load_video
    set gfxpayload=keep
    insmod gzio
    insmod ext2

    set isofile="/images/archlinux-2013.12.01-dual.iso"
    echo "Setup loop device..."
    loopback loop $isofile
    echo "Loading kernel..."
    linux (loop)/arch/boot/i686/vmlinuz archisolabel=ARCH_201312 img_dev=/dev/disk/by-label/lilyboot img_loop=$isofile earlymodules=loop
    echo "Loading initrd..."
    initrd (loop)/arch/boot/i686/archiso.img
}

set ver=3.12.6
menuentry "Arch Linux $ver backup" {
    load_video
    set gfxpayload=keep
    insmod gzio
    insmod ext2

    echo    'Loading Linux kernel ...'
    linux   /boot/vmlinuz-linux-lily-$ver root=/dev/mapper/lilybackup rw cryptdevice=/dev/disk/by-uuid/815d01ea-6390-460f-8c82-84c9e9497423:lilybackup rootflags=compress=lzo,subvolid=0 break=postmount
    echo    'Loading initramfs...'
    initrd  /boot/initramfs-$ver-backup.img
}

各个设备的卷标、UUID 和文件路径自己调整。最后一项需要的文件在后边准备,先介绍一下几个参数:

root
根分区所在的设备。是解密后的设备路径或者用 UUID 也可以。不过没关系的,这里不会有冲突的
cryptdevice
如果使用密码(而不是密钥文件)加密的话,这里是冒号分隔的两个参数:你的加密设备是哪个文件,以及它解密之后叫什么名字。
rootflags
这个是给我们的 btrfs 用的。指定要启用 lzo 算法压缩,使用根子卷。实际上根子卷里不是 Linux 系统的根。这里我只是让脚本把它挂载到/new_root上而已,脚本会因为找不到/sbin/init而进入一个 shell 的
break=postmount
即使根没有问题,也请在挂载好它之后给我一个 shell,我可能需要做一些调整

内核很简单,直接 cp 过去就好了。initramfs 要另外生成。这是我用来生成的 mkinitcpio.conf.crypt 文件:

MODULES="btrfs"
BINARIES="/usr/bin/btrfs"
FILES=""
HOOKS="base udev autodetect modconf block encrypt filesystems keyboard fsck shutdown"
COMPRESSION="xz"

重要的地方:内核模块 btrfs 一定是要的,另外我还需要 btrfs 程序来操作子卷。在 HOOKS 数组的 filesystems 前添加了 encrypt,用于在启动时询问密码并解码根分区。

我还准备添加 vi 程序来着,但是它说终端类型不认识,还说 /var/tmp 目录不存在。于是索性把自己静态链接的 vim 扔到内核一块去了。Vim 内建常见终端类型的数据,不那么挑剔的。zsh 所需要的文件太多,也放弃了。

然后使用 mkinitcpio 命令生成 initramfs 镜像:

sudo mkinitcpio -c mkinitcpio.conf.crypt -g initramfs-backup.img

然后把生成的文件复制到启动分区的相应路径下。

注意:如果你在 Arch Linux live 系统中为另外的内核生成该 initramfs,要指定内核和内核模块路径的根。一定不要将内核模块所在的目录软链接到/lib/modules,那样 mkinitcpio 不会添加任何块设备的内核模块的。我使用的命令如下:

mkinitcpio --kernel /run/archiso/img_dev/boot/vmlinuz-linux-lily-3.12.6 -r /mnt/backup/root/20131227_2044 --config mkinitcpio.conf -g /run/archiso/img_dev/boot/initramfs-3.12.6-backup.img

文件准备完毕,就可以启动过去了。注意我没有在备份分区的 run 目录下建立子卷,因为我准备进入 initramfs 之后再建立它们。

PS: 因为 BIOS 不支持,所以必须从 USB 2.0 来启动。在 Linux 内核启动的时候,可以将移动硬盘接到 USB 3.0 扩展卡上。具体时机是,initramfs 载入完毕,内核开始打印日志的时候。

进入备份系统

启动之后,会进入 initramfs 的 shell。在这个没有任务管理的 ash 中,使用 btrfs 命令在 /new_root/run 目录下建立新的子卷,注意不要加 -r 这个表示只读的选项了:

btrfs subvolume snapshot ../backup/root/20131016_1423 root/20131016
btrfs subvolume snapshot ../backup/home/20131016_1423 home/20131016

因为文件系统树的挂载结构变了,所以得拿准备好的 vim(或者 vi,如果你没准备 vim 的话)去编辑 root/20131016/etc/fstab 文件,将那些不会成功的挂载项都去掉,添加新的正确的项。PS: 如果使用 vim 的话,记得进去先set nocp一下,不然会是兼容模式,和 vi 一样只能撒消一步的。

/dev/mapper/lilybackup  /       btrfs   rw,relatime,compress=lzo,subvol=run/root/xxx     0 0
/dev/mapper/lilybackup  /home/lilydjwg  btrfs   rw,relatime,compress=lzo,subvol=run/home/xxx     0 0

然后 cd /,卸载 /new_root 并重新以子卷挂载之:

cd /
umount /new_root
mount -o compress=lzo,subvol=run/root/20131016 /dev/mapper/lilybackup /new_root

如果是因为找不到 /sbin/init 而进来这个 shell 的,那么就tail /init,最后那行是需要执行的命令(当然有些修改):

exec env -i "TERM=$TERM" /usr/bin/switch_root /new_root /sbin/init

如果是因为break=postmount参数而进来的,直接按Ctrl-D退出 shell 即可。

启动会继续进行,systemd 启动了,各种服务陆续启动中~~

如果很不幸地,忘记修改/etc/fstab了,或者有错,那么 systemd 会毫不留情地「Welcome to emergency shell」。不过现在更正也为时未晚。在编辑完 fstab 之后,要先执行下systemctl daemon-reload再退出那个 emergency shell。

接下来应该能一路顺利地到达指定时间的系统啦 =w=

时间机器打造完成哦耶~~

参考资料

Category: Linux | Tags: linux grub grub2 btrfs Arch Linux

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com