Btrfs:认识、从Ext4迁移与快照方案

语言: CN / TW / HK

对于Arch系等依赖滚动更新的发行版,Btrfs的快照功能真的是太具有吸引力了。纵使我已经很久没有遇到“滚炸”、纵使就算“滚炸”去Manjaro论坛看一眼一般都能解决,但是这些都不如一个“后悔药”来得实在——遇到问题,重启、选择老快照、恢复,一切都是那么美好。因此,前阵子(指12月中旬)我就把系统分区迁移到Btrfs上了。这篇博客就主要记录了迁移与快照的各种实现方案。

Btrfs:现代Linux文件系统

本着Arch精神,我还是想先简单介绍一下Btrfs。Btrfs(我一般念 B tree FS )是最早由Oracle贡献的Linux文件系统,如今已经进入Linux内核许久,是最有希望(我认为)成为未来Linux主流文件系统的候选者。相较之下,类似的ZFS有License问题;潜力十足的ReiserFS因为作者谋杀入狱后开发进度就不太乐观,且v4也没被合入内核;XFS走的是类似Ext4的稳定路线,对新功能的支持较为保守(但是足够稳定,可以用于数据盘)。这个开源软件的命运啊,还真是不可预料。

Btrfs功能繁多,可以称得上是新文件系统功能的试验田。到目前为止,Btrfs已经有了包括但不限于如下的功能:

  • 写时复制(CoW)、事务
  • 在线卷管理:碎片整理、大小调整、跨设备卷中物理设备的增删
  • 子卷:可以单独挂载的文件树,类似LVM的逻辑卷
  • 软件RAID
  • 透明压缩:写磁盘时自动压缩
  • 快照
  • 支持数据校验和

组合这些功能,可以实现很多非常不错的功能。比如开启透明压缩、CoW后跨网络同步磁盘,可以最大限度的减少需要传输的数据量。此外,Btrfs也提供了一个非常不错的CLI接口来进行管理。不过Btrfs的缺点也很明显:数据恢复难度显著大于Ext4等传统FS、稳定性一般、读写速度也相对较差(主要是HDD,SSD基本没有问题)。这些问题就算到了Btrfs上述功能已经完成了将近15年的今天,也仍旧需要改善。好在目前Facebook、群晖等企业都在使用Btrfs,未来还是明朗的。

不过,这些小问题并不能阻挡我 作死 尝鲜的步伐。因为把系统分区迁移到Btrfs是有若干好处的:

  1. 快照是滚动更新的后悔药
  2. 大量系统文件并不性能敏感,开启透明压缩能节省不少空间
  3. CoW对SSD硬盘十分友好,而Btrfs的性能劣势在SSD的补足下并不明显(相反HDD可能有40%的性能损失)
  4. 系统分区也没什么重要数据,炸了也无所谓

子卷和快照

Btrfs一个非常重要的概念就是子卷。子卷可以理解成为“盘中盘”,可以单独挂载,行为类似一个文件夹(可以移动,但不能 unlink )。子卷是可以嵌套的,不过此时子卷相当于同时挂载在一颗文件树,因此并不会循环嵌套。子卷可以根据其相对于根子卷(Btrfs分区自带的子卷,路径是 / ,ID是5)的路径确定(比如 /subA ),不过因为子卷的路径可以被移动,因此具体子卷是通过ID定位的。

快照就是基于子卷的,创建快照等同于创建一个和原子卷共享文件的子卷。因此其实快照的原子卷的地位是平等的,创建快照后删除原子卷也没有什么问题。结合根据路径查找子卷,就可以实现快照某个路径了:把子卷 /sub 挂载到 /dest ,如果要恢复快照就用某个快照子卷替换掉子卷 /sub

从Ext4迁移

子卷规划

了解了Btrfs之后,就可以考虑如何规划子卷了。区分不同子卷的主要目的是为Btrfs提供的大量功能划定作用的粒度。比如对于系统文件(比如配置等等),我们可能不太在意它的存取效率,因此可以划分一个单独的子卷并在挂载的时候开启压缩;对于日志之类的文件,则没有快照的意义,因此另立子卷不进行快照。

参考Ubuntu风格的命名方式,我推荐如下的子卷格式:

  • @ :根子卷,对应 / 。存放需要快照的系统文件
  • @home :家子卷,对应 /home
  • @cache :缓存子卷,对应 /var/cache 。包含Pacman包缓存等没必要快照的文件
  • @log :日志子卷,对应 /var/log 。不快照日志,方便查错

这个规划方式主要是针对Manjaro等Arch系发行版。如果有其他需要,也可以添加如 @opt@srv 等子卷,反正不要钱也不麻烦(openSUSE默认有9个子卷)。

规划完子卷后,迁移时按图索骥即可。不过由于我只是想用到Btrfs的快照功能,加之比较担心自己的数据,因此并没有迁移家目录。如果有此需求,请自行扩充迁移过程。此外,我也不建议通过迁移工具来直接把ext4分区转为btrfs分区,因为至少在本文写作时还有见到迁移后使用不稳定的报告。

所以,我最终采用的是Timeshift的迁移方式,也可以使用等价的 rsync 指令。首先备份好根分区。之后,建立btrfs分区并建立根子卷 @ (假设目标分区是 /dev/nvme0n1p1 )。

sudo mkfs.btrfs /dev/nvme0n1p1  # 这一步也可以在分区工具里完成
sudo mount /dev/nvme0n1p1 /mnt/btrfs
sudo btrfs subvolume create /mnt/btrfs/@

然后就可以进入Live CD,把原系统分区扬掉了。之后通过Timeshift或 rsync 把备份好的根分区恢复到目录 /mnt/btrfs/@ ,这样就完成了文件的迁移。接下来,逐一的建立子卷。比如要建立 /var/cache 对应的 @cache 子卷:

sudo btrfs subvolume create /mnt/btrfs/@cache
mv -v /mnt/btrfs/@/var/cache/* /mnt/btrfs/@cache

完成子卷建立后,还需要修改 fstab 以正确挂载(这里编辑的是 /mnt/btrfs/@/etc/fstab )。注意每个子卷都需要增加对应的记录,但是对应的磁盘分区UUID都是同一个,指向Btrfs分区。分区UUID可以通过 blkid 指令查询。此外,非 ssd 磁盘需要去掉 ssd 选项。

UUID=...	/	btrfs	defaults,noatime,ssd,[email protected]	0	1
UUID=...	/var/log	btrfs	defaults,noatime,ssd,[email protected]	0	1
UUID=...	/var/cache	btrfs	defaults,noatime,ssd,[email protected]	0	1

最后更新一下引导就可以了。

manjaro-chroot /mnt/btrfs/@
grub-install --target=x86_64-efi --efi-directory=EFI文件夹 --bootloader-id=manjaro --recheck

此时重启系统就应该在根子卷了。

系统快照方案

简单梳理了下需求,并按照重要程度排序:

  • 方便的实时创建快照、实时回滚
  • 可以定时备份、清理
  • 可以启动进快照,以备滚炸之需
  • 在滚动更新前自动创建快照

各种方案们

经过了解与分析,主要的方案有如下几种。

  • Ubuntu风格:使用Timeshift,只能识别 @@home 子卷
  • openSUSE风格:使用Snapper,可以作用于任何子卷,快照存放在默认的 @/.snapshot
  • Arch Linux风格:使用Snapper,可以作用于任何子卷,但创建独立的 @snapshot 子卷

权衡利弊

至于为什么会有那么多方案?没错,因为每一种都多少有点缺陷,所以就有一帮人出来造轮子……

首先是Timeshift的解决方案,这个方案就非常有Ubuntu的稳定气质。它强制了子卷风格必须是 @@home ,也没有提供快照的复杂功能,但是在用户体验上做的很好(尤其是GUI)。在启动进快照时会自动弹出窗口提示回滚,在Live CD里也可以进行恢复操作。因此,对于安稳的使用来说,我最推荐的也就是Timeshift方案了(这也是Manjaro默认的方案)。

Snapper和Timeshift就很不同了,比起Timeshift做的类似TimeMachine的系统恢复工作,Snapper更像是一个快照管理工具。它提供了丰富的快照管理方式:可以设定快照类型,并按类型定制自动清理规则;支持pre-post的方式管理快照,即在进行某操作的前后进行快照;甚至可以分析两个快照之间文件系统发生的变化,可以说是非常的Geek。但更Geek的是,它的GUI甚至不包含快照恢复功能。对于openSUSE,快照恢复可以通过 snapper rollback 解决;而对于Arch Linux风格,你甚至需要手动输入一堆指令回滚。至于支持Live CD回滚?想都别想,老老实实敲指令!

究其因,Snapper是openSUSE的人做的,但是它们的办法却并不是很Arch。这里的分歧在于快照子卷。Arch Linux社区认为,恢复快照的方式应该是这样:首先删除老的子卷( @ ),然后“复制”某个快照为新的子卷(实际操作也是一次快照)。但是openSUSE社区的思路完全是逆转过来的,它们认为为啥要把系统放在固定的 @ 子卷呢?不如直接把系统放在一个可写入的快照里面(比如 @/.snapshot/0 )!每次回滚,比如说回滚到快照6,就当场给快照6创建一个可写快照8,然后我的系统就直接变成了快照8(比如 @/.snapshot/8 )。相当于所有快照构成一系列世界线,然后真的系统待在其中一个上。但问题是,这样需要修改 grub 的源码,这就很不Arch了。所以Arch Linux Wiki里给出的Snapper用法就是用Snapper管理快照,但是独立快照子卷且手动恢复快照。由于这是违背Snapper设计目的的用法,因此部分功能就是有问题的(rollback命令不可用、无法识别当前所在的快照)。不过我自己设计了一个Workaround,因此也能在Manjaro下实现openSUSE的方法,各位可以看完具体方案再做决定。

Ubuntu风格:Timeshift方案(推荐)

配置流程

非常滴简单,你只需要:

sudo pacman -S timeshift timeshift-autosnap-manjaro grub-btrfs
sudo systemctl enable --now grub-btrfs.path

然后打开Timeshift,按照配置向导的Btrfs设置即可。此时使用pacman安装包也会创建快照,如果不需要创建快照,就在指令之前加上 SKIP_AUTOSNAP= 。还可以编辑 /etc/timeshift-autosnap.conf 文件来控制快照行为:

  • 设置 updateGrubfalse ,因为不更新也会自动触发
  • maxSnapshots 可以设置大一点,免得安装时覆盖了滚系统前的快照。我设置了5

如果你使用的不是Manjaro,那安装 timeshift-autosnap 之后需要使用 sudo systemctl edit grub-btrfs.path 并修改为:

[Unit]
Description=Monitors for new snapshots of timeshift
DefaultDependencies=no
Requires=run-timeshift-backup.mount
After=run-timeshift-backup.mount
BindsTo=run-timeshift-backup.mount

[Path]
PathModified=/run/timeshift/backup/timeshift-btrfs/snapshots

[Install]
WantedBy=run-timeshift-backup.mount

Troubleshooting

如果创建快照时卡顿

建议在设定里关闭 启用配额组 功能,然后运行 sudo btrfs quota disable /

如果KDE下无法浏览快照文件

新版本KDE应该不会出现这个问题。如果打不开文件夹,可以安装一个其他文件浏览器,比如Nemo。

rEFInd支持快照启动

虽然有轮子,但是说实话效果一般且难看。所以我建议rEFInd引导Grub,然后Grub负责快照。如果实在看不惯两个界面,建议编辑 /etc/default/grub

GRUB_TIMEOUT=2
GRUB_TIMEOUT_STYLE=hidden

Arch Linux/openSUSE风格:Snapper方案

两种方案的麻烦程度大差不差。硬要说的话,我个人更喜欢openSUSE的方式,因为它能运用到Snapper本身的全部功能。虽然看完过程很可能被劝退,但是其实我真的挺喜欢Snapper强大的功能,非常值得一 试。

1. 安装Snapper

sudo pacman -S snapper snapper-gui
sudo snapper -c root create-config /

2. Arch Linux风格特有的配置

虽然看着很麻烦,但其实基本照做即可(假设目标分区是 /dev/nvme0n1p1 )。大致的操作就是把Snapper创建的 @/.snapshots 删掉,替换成独立的子卷 @snapshots

sudo umount /.snapshots
sudo rm -r /.snapshots
sudo btrfs subvolume delete /.snapshots
sudo mkdir /.snapshots
sudo mount -o subvol=/ /dev/nvme0n1p1 /mnt
sudo btrfs subvolume create /mnt/@snapshots

然后修改 /etc/fstab ,参考子卷的方式增加一条将子卷 [email protected] 挂载到 /.snapshots 的即可。运行 sudo mount -a 生效。

3. Snapper配置

首先是配置定时快照与定时清理。编辑 /etc/snapper/configs/root (也可以通过GUI配置: sudo snapper-gui ),可以参考这个配置:

TIMELINE_MIN_AGE="1800"
TIMELINE_LIMIT_HOURLY="0"
TIMELINE_LIMIT_DAILY="2"
TIMELINE_LIMIT_WEEKLY="4"
TIMELINE_LIMIT_MONTHLY="4"
TIMELINE_LIMIT_YEARLY="1"

然后启用定时任务:

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

然后需要配置允许非root用户创建、管理快照。也是先编辑 /etc/snapper/configs/root ,在 ALLOW_USERS 里增加自己的用户( whoami )。然后修改快照目录的权限:

sudo chmod 0750 /.snapshots/
sudo chown :用户 /.snapshots/

重启生效。

4.openSUSE风格特有配置

主要的操作是把系统转移进某一个可写的快照。

  1. 为了使用 manjaro-chroot ,安装包 manjaro-tools-base (运行 sudo pacman -S manjaro-tools-base )。其实不装也行,把指令换成 chroot 也一样。
  2. 编辑 /etc/fstab ,把挂载根目录一行的 subvol=/ 去掉;参考子卷的方式增加一条将子卷 subvol=/@/.snapshots 挂载到 /.snapshots 的配置。
  3. 创建一个快照作为当前的系统快照: snapper create --read-write --type single -d "Init snapshot" 。在指令输出中应该可以看到创建快照的快照号,把它设置为当前快照: sudo btrfs subvolume set-default /.snapshots/快照号/snapshot
  4. 更新一下Grub,让系统启动到这个快照: manjaro-chroot /.snapshots/快照号/snapshot bash -c "sudo grub-install && sudo update-grub"
  5. 重启。然后看看 findmnt / 里面显示的子卷是不是设置的那个快照号。
  6. 如果是,那就来一个刺激的狠活。先挂载它 sudo mount -o subvol=/ /dev/nvme0n1p1 /mnt ,然后进行一个巨大删除 sudo rm -rf /mnt/@/* (也可以手动删除,不过千万别把 .snapshot 删了)。

5.配置Grub启动到快照的菜单

安装 grub-btrfs 包( sudo pacman -S grub-btrfs ),运行 sudo systemctl edit grub-btrfs.path 并修改为:

[Unit]
Description=Monitors for new snapshots of snapper
DefaultDependencies=no
Requires=
After=
BindsTo=

[Path]
PathModified=/.snapshots

[Install]
WantedBy=multi-user.target

然后运行即可: sudo systemctl enable --now grub-btrfs.path

快照回滚

之前提到过,Snapper的快照回滚并不是一桩易事。不过这些问题都可以通过写脚本来解决。

Arch Linux风格

先来看看手动的方式吧(假设分区是 /dev/nvme0n1p1 ):

sudo mount /dev/nvme0n1p1 /mnt
sudo btrfs subvolume snapshot /mnt/@ /mnt/@bad
sudo btrfs subvolume delete /mnt/@
sudo btrfs subvolume snapshot /mnt/@snapshots/要恢复的快照号/snapshot /mnt/@

要恢复的快照号需要手工检查,如果在快照中可以通过 snapper ls 查看。如果在Live CD中,可以通过精准的猜测 和一点运气和奇迹 逐个查看快照文件夹内的 xml 得到。

由于过程确实很繁琐,于是我就写了一个脚本,建议保存在 /usr/local/bin/rollback ,这样就可以 rollback 快照号 了。脚本会创建一个快照,然后进行恢复操作。如果当前不在快照,还会创建 @old 子卷保存当前系统,需要重启后手动删除。当然,这个脚本 可以在Live CD里使用。

#!/bin/sh
set -e
if [[ x"$1" == x ]]; then
  echo "No snapshot number given." 1>&2
  echo "Usage: rollback [snapshot to rollback]" 1>&2
  exit 1
fi
root_dev=`findmnt -n -o SOURCE / | sed 's/\[.*\]//g'`
root_subvol=`findmnt -n -o SOURCE / | sed 's/.*\[\(.*\)\].*/\1/'`
echo ">= Rollback to #$1 on device $root_dev"
# create snapshot before
sudo snapper create --read-only --type single -d "Before rollback to #$1" --userdata important=yes
sudo mount -o subvol=/ $root_dev /mnt
# check enviornment
if [[ x"$root_subvol" == x/@ ]]; then
  echo "Warning: Not run in a snapshot, a subvolume @old will be created. You should consider remove it after reboot." 1>&2
  if [[ -d /mnt/@old ]]; then
    echo "Found last @old, remove it."
    sudo btrfs subvolume delete /mnt/@old >/dev/null
  fi
  sudo mv /mnt/@ /mnt/@old
else
  sudo btrfs subvolume delete /mnt/@ >/dev/null
fi
sudo btrfs subvolume snapshot /mnt/@snapshots/$1/snapshot /mnt/@ >/dev/null
sudo umount /mnt

openSUSE风格

如果你在用openSUSE,那启动进一个快照后运行 snapper rollback 就完事了,但可惜我们并不是openSUSE。因为正常的Grub并不能在不更新的情况下启动到默认子卷,因此只能手动更新它了。同样,用一个脚本来完成这些操作,同样建议保存在 /usr/local/bin/rollback 。不过就不需要手动指定快照号了,直接 rollback 即可。

#!/bin/sh
set -e
root_dev=`findmnt -n -o SOURCE / | sed 's/\[.*\]//g'`
sudo snapper rollback $1
echo ">= Update GRUB on $root_dev"
sudo mount $root_dev /mnt
if [[ -d /boot/efi ]]; then
  manjaro-chroot /mnt grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=manjaro --recheck
else
  read -p "Enter MBR device (e.g. /dev/sda): " install_dev
  manjaro-chroot /mnt grub-install --force --target=i386-pc --recheck --boot-directory=/boot $install_dev
fi
manjaro-chroot /mnt update-grub
sudo umount /mnt

同样,脚本 能在Live CD里用。此外,虽然如今PC分区一般都不会是MBR格式,但如果真是则还需要手动输入引导设备。

openSUSE风格在Live CD中的快照回滚

还是基于之前的原因,本来很简单的操作因为Grub的缘故还要增加一步更新操作。此外,Live CD中就需要指定快照号了,确定方法和Arch的一样。不过还好,一般的小场面也用不到Live CD。

sudo mount /dev/nvme0n1p1 /mnt
sudo btrfs subvolume set-default /mnt/@snapshots/快照号/snapshot
manjaro-chroot /mnt grub-install --target=x86_64-efi --efi-directory=EFI文件夹 --bootloader-id=manjaro --recheck

成功进去系统后建议运行一次 rollback

Troubleshooting

如果创建快照、列出快照时卡顿

真的巨卡无比,建议运行 sudo btrfs quota disable / 就没事了。

rEFInd支持快照启动

类似Timeshift,理由、解决方案也一样。

Reference

  1. http://forum.manjaro.org/t/howto-btrfs-and-snapper/25417
  2. http://wiki.archlinux.org/title/snapper
  3. http://bbs.archlinux.org/viewtopic.php?id=194491
  4. http://www.reddit.com/r/openSUSE/comments/l015pb/snapper_rollback_problem/
  5. http://forum.manjaro.org/t/btrfs-snapper-the-suse-way-with-rollback/52279
  6. http://wiki.manjaro.org/index.php/Restore_the_GRUB_Bootloader
  7. http://github.com/teejee2008/timeshift/issues/606