2021年1月20日

从ext4到btrfs以及btrfs的基本玩法

作者 qsdrqs

Warning: 直接从 ext4 转换到 btrfs 存在转换失败可能,因此理想的方式应该是使用 rsync 等工具进行全盘备份后重新将硬盘格式化为 btrfs 文件系统

在 ArchLinux 上从 ext4 到 btrfs 的转换终于基本结束了,写这个博客记录一下全过程。

什么是 btrfs

btrfs 是由 Oracle 等开发的一种 CoW(写时复制)文件系统,着重于容错和自我修复等内容。对于用户来说,它有两个很明显的亮点:

  • 增量快照功能:可以随时对某一个“子卷”创建快照,并支持读写,回滚等操作。

  • 透明压缩:对指定“子卷”进行压缩处理,在牺牲一定性能(多线程和 / 或具有大文件 I/O 的 CPU 密集型任务,然而在单线程、重文件 I/O 反而还提升了性能)的前提下获取更多磁盘空间。

什么是写时复制

CoW(写时复制)原本是指计算机中解决资源冲突的一种方式。就拿文件系统举例,对于一个文件系统的资源,如果一组调用者同时请求该文件(读取写入),传统文件系统的做法是将文件资源拷贝成若干份分配给调用者,调用者写入时先写入副本,再由文件系统持久化存盘。这种方式的缺点就是如果某个资源请求者对该资源是不写入的,那么就没必要复制一份出来。因此写时复制的思想就是,每个调用者读取资源时获得的都是资源的一个引用,只有对资源做修改操作时才会从系统里获得一份资源的副本,而其他调用者所见到的资源保持不变。

Btrfs 是一个采用 CoW 技术的文件系统。在这种技术的加持下,它可以很容易的对若干文件甚至整个“子卷”创建快照。一个快照相当于对“子卷”的一个引用,同时其本身也是一个子卷,这个引用开始几乎没有空间消耗,但一旦有文件发生修改,被修改的文件就会自我复制一份,以确保快照中的文件保持不变。因此这个快照是“增量的”:被修改的文件越多,这个快照的大小越大。

在 ArchLinux 上从 ext4 转换到 btrfs

一般来说,如果没有刻意选择,你装的 linux 大多是 ext4 文件系统,ArchWiki 的 Installation guide 给出的示例也是 ext4 文件系统。对于 ext3/4 文件系统,btrfs-prog 提供了转换脚本。然而就像文章开头的Warning所说,有存在转换失败的可能。如果一定要和我一样无视这个Warning,请确保转换时的 linux 内核是最新的(最新的内核会减小转换失败的可能)。同时,本文需要结合 ArchWiki 相关内容 共同作为参考。

以及,这里默认你已经对 linux 的基本理论有一定的了解,如果你并不能看懂本教程后面的每一步操作的意义,为了自己的数据安全,建议再熟悉一段时间后 linux 再来考虑转换。

首先,从 U 盘启动到 archiso 环境,首先执行 fsck 来确保 ext4 文件系统正常。

fsck.ext4 /dev/nvme0n1p5 #(本文后面均以该位置举例,注意自行替换成自己的 linux 安装位置!)

随后,深吸一口气,开始转换

btrfs-convert /dev/nvme0n1p5

上一步的时间很长(取决于你现有的文件系统有多大),等待一段漫长的时间后,如果看到

convertion complete

说明你已经成功一半了!随后,挂载一下分区,检查是否有问题

mount /dev/nvme0n1p5 /mnt
mount /dev/nvme0n1p1 /mnt/boot
arch-chroot /mnt

如果整体上没有什么大问题,后面就需要修改引导配置了:

  • 通过lsblk -f得到 linux 分区的新 uuid

  • 修改/etc/fstab,将 uuid 改成新的 uuid,再将type改为btrfs,同时将fs_passno(最后一列)改为 0。改好的 fstab 大概像这个样子 (space_cache=v2 是为了解决一些 bug)

    ...
    UUID=b9978dff-73f4-446a-bb51-fe517d7b0e9a   /           btrfs       rw,relatime,space_cache=v2  0 0
    ...
  • 由于转换的是根目录,需要使用mkinitcpio -p linux重建内存盘。我使用的是 linux-surface 内核,因此我执行mkinitcpio -p linux-surface

  • 如果使用的是 grub 的话,需要重新安装 grub 和生成配置

    # 对于 UEFI
    grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB
    grub-mkconfig -o /boot/grub/grub.cfg
  • 确认没有问题后,通过删除 ext2_saved 备份子卷完成转换的最后一步。请注意,如果没了它 (备份子卷),你将没办法还原回 ext3/4 文件系统。

    btrfs subvolume delete /ext2_saved
  • 最后通过 Balance 回收空间。Balance 将会通过分配器再次传递文件系统中的所有数据。它主要用于在添加或删除设备时跨设备重新平衡文件系统中的数据。(反正就是可以回收可用空间。)

    btrfs balance start /
    btrfs balance status /

    这里大概率会有报错,因为在转换的时候总会有些许文件转换失败。无论如何,先启动一个(后台运行的)包含 / 目录的文件系统在线检查任务:

    btrfs scrub start /
    btrfs scrub status /

    完成后根据报错内容提示的错误文件的logic number或是inode号,定位到错误的文件位置

    # logic number
    btrfs inspect-internal logical-resolve 253848752128 /
    # inode
    btrfs inspect-internal inode-resolve 8127262 /

    定位到文件之后,最简单的解决方法就是把它删了😄️。然后再次执行scrub任务检查。

  • 以及,如果你使用 TLP 作为电源管理方案,参考 archwki 的相关配置。

重启一下,如果系统一切正常就说明转换成功了!不要忘了启动每月一次的全面校验任务:

sudo systemctl enable btrfs-scrub@-.timer
sudo systemctl start btrfs-scrub@-.timer

子卷划分

前面一直提到“子卷”这个概念,"btrfs 子卷不是 (也不能看作) 块设备,一个子卷可以看作 POSIX 文件名字空间。这个名字空间可以通过子卷上层访问,也可以独立挂载." -- btrfs wiki

对 btrfs 子卷的个人理解:一般来说 linux 文件系统是一个以/为根节点的严格的树状结构,但实际上这个树状结构的物理实现是可以很多样的(最简单的例子就是你可以把 /dev/nvme0n1p5 挂载到 / 而把 /dev/nvme0n1p1 挂载到 /root,同时再把其他分区挂载到/mnt)。因此,可以理解成 btrfs 将整个系统划分成若干个独立的命名空间,在系统启动时分别挂载,而 btrfs 设备本身可能并不是树结构,更多会是一种森林结构。

下图是我的/dev/nvme0n1p5的文件结构 (@没有什么特别的意义,只是命名习惯而已):

.
├── @
│   ├── bin -> usr/bin
│   ├── boot
│   ├── btrfs_boot
│   ├── data
│   ├── dev
│   ├── etc
│   ├── home
│   ├── lib -> usr/lib
│   ├── lib64 -> usr/lib
    ...
├── @home
│   └── qsdrqs
├── @swap
│   └── swapfile
├── @var_cache
│   ├── app-info
    ...
├── @var_log
│   ├── audit
    ...
├── @var_tmp
│   ├── systemd-private-6ba6d943fd444f0aa3b04a9387aaba89-syncthing@qsdrqs.service-IeF2gg
    ...

如果还是没看懂的话,就读一读 btrfs wiki 吧,以及其他划分方案,都可以在 wiki 中找到。不过一般来说,都不把 btrfs 文件系统直接挂载到/下,除非你不想对/拍快照。

再次进入 archiso,挂载 btrfs 根目录,准备划分子卷并把数据挪进去。

cd /mnt
btrfs subvolume create @
mv * @ # 这里会有个报错,@不能移动到 @里去,但不用管它

通过这一步之后,你已经把整个文件根目录移到/@子卷中去了。后面根据需求再继续划分子卷,比如对于家目录:

btrfs subvolume create @home
mv @/home/* @home/

一般来说,对/var/log/var/tmp/var/cache这种数据经常变化但备份意义不大的的文件夹都可以进行单独分子卷,以减小备份体积 (不需要对/tmp特意分配子卷,因为/tmp是直接挂载在内存上的)。特别的,如果有使用交换文件的需求,就再分配一个 @swap 子卷。

对于我的系统,最终分配的子卷情况(已经删去了所有 snapshot 子卷):

ID 586 gen 18109 parent 5 top level 5 path @
ID 596 gen 18117 parent 5 top level 5 path @var_log
ID 597 gen 18017 parent 5 top level 5 path @var_tmp
ID 598 gen 18117 parent 5 top level 5 path @home
ID 627 gen 17372 parent 5 top level 5 path @swap
ID 628 gen 18069 parent 5 top level 5 path @var_cache
ID 645 gen 17665 parent 598 top level 598 path @home/qsdrqs/QEMU_KVM
ID 646 gen 18117 parent 598 top level 598 path @home/qsdrqs/.cache
ID 661 gen 18043 parent 598 top level 598 path @home/.snapshots
ID 673 gen 18117 parent 5 top level 5 path @tmp

分配完之后,使用 umount 解挂 /mnt,随后挂载子卷:

mount -o subvol=/@ /dev/nvme0n1p5 /mnt
mount -o subvol=/@home /dev/nvme0n1p5 /mnt/home
arch-chroot /mnt

修改一下/etc/fstab,这里要对所有顶级子卷都设置挂载,而子卷的子卷不需要设置挂载

...
UUID=b9978dff-73f4-446a-bb51-fe517d7b0e9a   /           btrfs       defaults,rw,relatime,space_cache=v2,subvol=/@   0 0
# home directory
UUID=b9978dff-73f4-446a-bb51-fe517d7b0e9a   /home           btrfs       rw,relatime,space_cache=v2,subvol=/@home    0 0
# global cache
UUID=b9978dff-73f4-446a-bb51-fe517d7b0e9a   /var/cache          btrfs       rw,relatime,space_cache=v2,subvol=/@var_cache   0 0
# global log
UUID=b9978dff-73f4-446a-bb51-fe517d7b0e9a   /var/log            btrfs       rw,relatime,space_cache=v2,subvol=/@var_log 0 0
# global tmp
UUID=b9978dff-73f4-446a-bb51-fe517d7b0e9a   /var/tmp            btrfs       rw,relatime,space_cache=v2,subvol=/@var_tmp 0 0
# swap
UUID=b9978dff-73f4-446a-bb51-fe517d7b0e9a   /swap           btrfs       rw,relatime,space_cache=v2,subvol=/@swap    0 0
...

随后像上文一样,重新安装和生成 grub 配置文件。重启进入系统!

进入系统后,如果很幸运地一切正常,就可以继续下面的步骤

透明压缩

使用透明压缩可以显著减小磁盘空间占用。(Archwiki 说压缩比例大概在 10%-20%,但我这里甚至高达 30%)

进入系统后,使用sudo btrfs filesystem defragment -r -czstd / 进行全盘压缩(一定确保是最新的内核)。等待一段时间后,压缩成功,在 fstab 中给希望压缩的分区添加compress=zstd的 option,比如:

UUID=b9978dff-73f4-446a-bb51-fe517d7b0e9a   /           btrfs       conpress=zstd,defaults,rw,relatime,space_cache=v2,subvol=/@ 0 0

注意,不要给 swap 分区开启压缩。

重启后,使用btrfs filesystem usage /查看文件系统的使用情况,可以使用compsize -x / 查看压缩比。

交换文件

交换空间有两个用处,一是将硬盘中的一部分空间大小用作内存使用,扩展了可用内存(但是用作内存的硬盘部分读写速度一般是远低于真正内存的)。二是可以使用systemctl hibernate实现断电休眠,同时对于双系统可以在不"关机"的情况下切换到另一个系统中去。

如果你有使用交换文件的需求,注意 Btrfs 是要特殊配置的。

输入以下命令

sudo cd /swap/swapfile # 进入到 swap 下
sudo truncate -s 0 ./swapfile
sudo chattr +C ./swapfile # 禁用写时复制
sudo btrfs property set ./swapfile compression none # 禁用压缩
sudo fallocate -l 16G ./swapfile # 内存大小自行设定
sudo chmod 0600 ./swapfile
sudo mkswap ./swapfile
sudo swapon ./swapfile

随后修改/etc/fstab,添加如下一行,使其开机自动挂载。

/swap/swapfile none swap defaults 0 0

hibernate

btrfs 也是支持休眠到交换文件的。

输入lsblk -f获得你的交换文件所在盘的 uuid(一般与你 linux 盘 uuid 相同)并记录下来。

此处 获取 btrfs 映射工具源代码文件。并用 gcc 编译运行

gcc -O2 -o btrfs_map_physical btrfs_map_physical.c
sudo ./btrfs_map_physical /swap/swapfile

执行结果长成这个样子:

FILE OFFSET FILE SIZE   EXTENT OFFSET   EXTENT TYPE LOGICAL SIZE    LOGICAL OFFSET  PHYSICAL SIZE   DEVID   PHYSICAL OFFSET
0   4096    0   regular 268435456   391855177728    268435456   1   84099760128
4096    268431360   4096    prealloc    268435456   391855177728    268435456   1   84099760128
268435456   268435456   0   prealloc    268435456   392863576064    268435456   1   82960674816
536870912   268435456   0   prealloc    268435456   393132011520    268435456   1   83229110272

记录第一行的 PHYSICAL OFFSET(最后一列,我的是 84099760128 )。使用getconf PAGESIZE得到页大小,我的是 4096。计算84099760128/4096,得到结果20532168。记下这个值作为resume_offset

带着记录的 UUID 和resume_offset,进入/etc/default/grub,添加 GRUB_CMDLINE_LINUX_DEFAULT 的参数(把 uuid 和 offset 改成自己的):

...
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 quiet resume=UUID=b9978dff-73f4-446a-bb51-fe517d7b0e9a resume_offset=20532168"
...

使用sudo grub-mkconfig -o /boot/grub/grub.cfg重新生成 grub 文件,重启后使用systemctl hibernate测试休眠是否成功。

快照管理

btrfs 对子卷具有增量快照功能,除了参照 btrfs-wiki 中对快照的手动创建和管理,还可以采用 snapper 进行自动管理。

snapper 的安装和配置参照 archwiki(目前中文文档不全)以及man snapper。使用

sudo snapper -c root create-config /
sudo snapper -c home create-config /home

为根目录和家目录子卷创建一个配置文件。

以时间线自动快照

可以对子卷设定定时快照功能,启动相关的 systemd-timer:

systemctl enable --now snapper-timeline.timer
systemctl enable --now snapper-cleanup.timer

编辑 /etc/snapper/configs/your_config 来改变定时快照的保留个数,比如,下面是一个保留 5 个每日快照和 7 个每周快照的设置。

TIMELINE_MIN_AGE="1800"
TIMELINE_LIMIT_HOURLY="5"
TIMELINE_LIMIT_DAILY="7"
TIMELINE_LIMIT_WEEKLY="0"
TIMELINE_LIMIT_MONTHLY="0"
TIMELINE_LIMIT_YEARLY="0"

基于事件的快照

snapper 还可以在特定事件后创建快照,参见 archwiki

可以安装 snap-pac 使得 pacman 在执行操作前进行快照,以此保证 pacman 操作的稳定。

使用 grub 进入快照

安装 grub-btrfs 来实现在 grub 界面进入/.snapshots里的快照。不过由于 snapper 创建的快照是只读的,因此不能做写入操作。要想修改读写权限,输入

sudo btrfs property set /path/to/.snapshots/<snapshot_num>/snapshot ro false

若希望 grub 选项随照创建自动更新,启动 grub-btrfs.path 服务

systemctl enable --now grub-btrfs.path

结语

到此,btrfs 的基本玩法就是这些了,要想探索更多玩法,就需要自己多看文档啦。

参考

  1. Archwiki 相关条目,https://wiki.archlinux.org/
  2. btrfs wiki 相关条目,https://btrfs.wiki.kernel.org
  3. 漩涡的博客,记一次 btrfs 的在线转换,https://xuanwo.io/2018/11/15/record-for-btrfs-conversion/