指南:将 EXT4 文件系统上的 Arch Linux 迁移至 ZFS

为了享受 ZFS 文件系统的 mirror 带来的数据安全保障,我将 Arch Linux 系统从 ext4 文件系统迁移至了 ZFS 文件系统。相关的限制条件包括:原来的硬盘是 三星 840 EVO 256G,需要与 金士顿 SA400S37 480G 组合成一个 mirror pool,也就意味着需要先从 三星 复制到 金士顿,再把 三星 做成 ZFS,将数据重新复制到三星,然后重启进系统把金士顿 attach 到 三星 变成一个 mirror pool;另外,我不想按照 Arch Linux 的指南对三星进行分区,想要让 ZFS 直接管理整个三星硬盘,所以需要外置引导设备。

flowchart TD A[三星 840 EVO(ext4)] -->|1. rsync 备份| B[金士顿 SA400S37 (ext4)] C[三星 840 EVO(ZFS)] A --> | 2. 格式化ZFS | C B --> | 3. rsync 恢复 | C B --> |4. attach 到三星| D[mirror zpool] C --> D

1. 制作支持 ZFS 的 archiso

官方的 archiso 是不支持 ZFS 的,和 Linux 内核一样,是因为开源协议不兼容所以不能内置支持。因此我们需要自己制作一个修改版的 archiso 支持识别和操作 ZFS。参考官方指导:ZFS官方指导:archiso,用 pacman 安装 archiso,创建一个目录用来制作 archiso,然后复制一套官方 iso 的配置文件到工作目录:

mkdir archlive
cp -r /usr/share/archiso/configs/releng/ archlive

然后修改 archlive/releng/pacman.conf 添加 archzfs 仓库:

[archzfs]
# Origin Server - Finland
Server = http://archzfs.com/$repo/$arch
# Mirror - Germany
Server = http://mirror.sum7.eu/archlinux/archzfs/$repo/$arch
# Mirror - Germany
Server = http://mirror.sunred.org/archzfs/$repo/$arch
# Mirror - Germany
Server = https://mirror.biocrafting.net/archlinux/archzfs/$repo/$arch
# Mirror - India
Server = https://mirror.in.themindsmaze.com/archzfs/$repo/$arch
# Mirror - US
Server = https://zxcvfdsa.com/archzfs/$repo/$archCode language: PHP (php)

可能由于我之前已经安装过 archzfs 的内容,所以不需要做一个 trust gpg key 的动作。接下来修改 archlive/releng/packages.x86_64,添加 linux-headerzfszfs-dkmszfs-utils 四个额外的包。完成之后,以 root 身份 cd 到 archlive 目录,执行 mkarchiso -v ./releng开始构建 archiso。完成之后的镜像在 archlive 目录下的 out 中,把它复制出来用 rufus (dd模式)写入到一个U盘中,这一步就完成了。

.
├── out
│   └── archlinux-2024.06.23-x86_64.iso
├── releng
│   ├── airootfs
│   ├── efiboot
│   ├── grub
│   ├── syslinux
│   ├── bootstrap_packages.x86_64
│   ├── packages.x86_64
│   ├── pacman.conf
│   └── profiledef.sh
└── workCode language: CSS (css)

2. 制作用于引导的ZFSBootMenu U盘

这个U盘不用很大,质量也不用很好,但是需要长期插在电脑上,所以最好是闲置不用的拿过来发挥余热。根据 Portable ZFSBootMenu 的指导,假设U盘是 /dev/sdf,那么用 fdisk 来进行分区、mkfs.fat 来格式化的示范如下:

sudo fdisk /dev/sdf

Command (m for help): g
Created a new GPT disklabel (GUID: 38E24376-CD07-4B87-85C5-28BC29ADF0A9).

Command (m for help): n
Partition number (1-128, default 1): 
First sector (2048-31129566, default 2048): 
Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-31129566, default 31127551): 

Created a new partition 1 of type 'Linux filesystem' and of size 14.8 GiB.

Command (m for help): t
Selected partition 1
Partition type or alias (type L to list all): 1
Changed type of partition 'Linux filesystem' to 'EFI System'.

Command (m for help): w

sudo mkfs.fat /dev/sdf1Code language: JavaScript (javascript)

把分区挂载到 /mnt/usb,创建EFI/BOOT路径并把 ZFSBootMenu 的 EFI 塞进去:

sudo mkdir /mnt/usb
sudo mount /dev/sdf1 /mnt/usb
sudo mkdir -p /mnt/usb/EFI/BOOT
curl -LJ https://get.zfsbootmenu.org/efi/recovery -o /mnt/usb/EFI/BOOT/BOOTX64.EFI
sudo umount /mnt/usbCode language: JavaScript (javascript)

这样就获得了一个能够引导 ZFS 上的内核的U盘了。

3. 重启并迁移数据

首先要确保当前的系统已经能够操作 ZFS 再重启,由于我的系统对于数据盘早已使用 ZFS,所以不用额外装其他东西;如果你没有,那么请一定要装好 zfs-dkmszfs-utils以及你的内核对应的header再重启,否则数据迁移好了系统也是加载不了的。

先不要插 ZFSBootMenu 那个U盘,只插 archiso,重启进 iso 环境之后,根据 ls -lh /dev/disk/by-id/的输出来确认硬盘哪个是哪个,比如在我的机子上只有1个三星和1个金士顿,那么就会比较清晰(无关的硬盘已省略):

lrwxrwxrwx 1 root root  9 Jun 23 23:15 ata-KINGSTON_SA400S37480G_50026B7380698064 -> ../../sdb
lrwxrwxrwx 1 root root 10 Jun 23 23:15 ata-KINGSTON_SA400S37480G_50026B7380698064-part1 -> ../../sdb1
lrwxrwxrwx 1 root root 10 Jun 23 23:15 ata-KINGSTON_SA400S37480G_50026B7380698064-part9 -> ../../sdb9
lrwxrwxrwx 1 root root  9 Jun 23 23:15 ata-Samsung_SSD_840_EVO_250GB_S1DBNSAF222103J -> ../../sda
lrwxrwxrwx 1 root root 10 Jun 23 23:15 ata-Samsung_SSD_840_EVO_250GB_S1DBNSAF222103J-part1 -> ../../sda1
lrwxrwxrwx 1 root root 10 Jun 23 23:15 ata-Samsung_SSD_840_EVO_250GB_S1DBNSAF222103J-part9 -> ../../sda9

然后用fdisk -l /dev/sda来确定现在的三星上的分区,常见的比如 sda1 是 EFI,sda2 是 /boot,sda3 是 /

在进行接下来的操作之前,请确保金士顿的数据已经备份好了,它的所有数据都会被删掉的。

3.1 格式化金士顿、rsync 备份

由于三星是 ext4,所以我们把金士顿也格式化为 ext4,来确保文件系统属性兼容。根据上面的输出,金士顿是/dev/sdb,所以用 fdisk /dev/sdb 新建分区表、创建一个分区、保存分区表,然后mkfs.ext4 /dev/sdb1,再挂载到/king

接下来把三星挂载到 /mnt,根据前面的信息进行以下命令:

mount /dev/sda3 /mnt
mount /dev/sda2 /mnt/boot
mount /dev/sda1 /mnt/efi

然后用 rsync 把数据备份到金士顿:

rsync -a -P --xattrs --acls --numeric-ids --hard-links /mnt/ /king/Code language: JavaScript (javascript)

搞定之后 umount -R /mnt释放三星。

3.2 格式化三星为 ZFS、rsync 恢复

这一节参考Install Arch Linux on ZFS,但是简化了很多不必要的步骤。

用单设备创建 zpool 的命令把三星喂给 ZFS。注意,我们不走官方的分区模式,二是让 ZFS 直接接管这个硬盘;这里使用的是硬盘id:

zpool create -f         \
             -O acltype=posixacl       \
             -O relatime=on            \
             -O xattr=sa               \
             -O dnodesize=legacy       \
             -O normalization=formD    \
             -O mountpoint=none        \
             -O canmount=off           \
             -O devices=off            \
             -R /mnt                   \
             zroot /dev/disk/by-id/ata-Samsung_SSD_840_EVO_250GB_S1DBNSAF222103J

接下来是创建 dataset,类似于传统意义上的分区。我不搞那么复杂,只分开了 //home

zfs create -o mountpoint=none zroot/data
zfs create -o mountpoint=none zroot/ROOT
zfs create -o mountpoint=/ -o canmount=noauto zroot/ROOT/default
zfs create -o mountpoint=/home zroot/data/homeCode language: JavaScript (javascript)

然后 export、重新导入 pool。这几条命令可以保存为一个 shell 脚本,之后如果需要重新进入 iso 环境抢救的时候,导入就不用手打了:

zpool export zroot
zpool import -d /dev/disk/by-id -R /mnt zroot -N
zfs mount zroot/ROOT/default
zfs mount -aCode language: JavaScript (javascript)

然后从金士顿把文件恢复回来:

rsync -a -P --xattrs --acls --numeric-ids --hard-links /king/ /mnt/Code language: JavaScript (javascript)

4. 修改配置

下面的这些操作都是arch-chroot /mnt 之后操作的,改的是原来的系统的配置而不是 archiso 的配置。

4.1 修改/etc/fstab

要把 /etc/fstab 中关于三星、金士顿的条目都删掉,因为我们这个配置方法不是 legacy 挂载点,开机的时候 ZFS 会负责处理挂载的事情,不需要 fstab 来做。

4.2 修改 /etc/mkinitcpio.conf

要把 zfs 添加到 MODULES=() 中。也要在 HOOKS=block 后面添加 zfs。在我的系统上,这两个是这样的:

MODULES=(zfs)
HOOKS=(base udev autodetect microcode modconf kms keyboard keymap consolefont block zfs filesystems)

由于我不需要在 initrd 中使用 systemd,所以官方指南里的sd-zfs、systemd就直接跳过了。

暂时先不做重新生成 initramfs 的操作,还有其他文件要改,现在生成等下还要重新生成。

4.3 设置 bootfs 、挂载读写(rw)模式、开机自动挂载

虽然我们使用外置 ZFSBootMenu 似乎不需要设置 bootfs,但是我没有试过不做这一步的结果,所以就写上了。执行 zpool set bootfs=zroot/ROOT/default zroot 来将 zroot 的 bootfs 设置成我们存放 / 的 dataset。

接下来是非常重要的一步,设置 zroot/ROOT 在 import 时的属性为 读写(rw)模式,否则会默认 import 为 只读(ro)模式,启动了之后是改不了的——会提示只有 import 的时候才能设置这个 property:

zfs set org.zfsbootmenu:commandline="rw" zroot/ROOTCode language: JavaScript (javascript)

然后是开机自动挂载 zroot:

zpool set cachefile=/etc/zfs/zpool.cache zrootCode language: JavaScript (javascript)

要 enable 一些 service 和 target 才能开机时自动挂载:

systemctl enable zfs.target zfs-import-cache.service zfs-mount.service zfs-import.targetCode language: CSS (css)

根据官方指南还需要设置 hostid,用以下命令生成、写入 /etc/hostid

zgenhostid $(hostid)Code language: JavaScript (javascript)

4.4 重新生成 initramfs

完成了上面的所有配置之后,就可以重新生成 initramfs 了:mkinitcpio -P

5. 重启进入系统

首先退出 chroot 环境,回到 archiso 环境,然后卸载并 export zroot——根据官方指南,如果忘了 export 的话,重启之后加载系统的时候会提示该 pool 不能 import,正在被其他系统使用。确实是这样的,包括之前在用的数据盘们所属的 pool,我在做第3步之前没有 export,所以重启之后需要 zpool import --force 才能用回来;但是根文件系统就没办法 "force" 了,所以必须 export。

zfs umount -a
zpool export zrootCode language: JavaScript (javascript)

接下来用 poweroff 关机,拔掉 archiso,插上第2步制作的 ZFSBootMenu U盘,按开关开机;一切正常的话会正确显示 ZFSBootMenu 的界面,提示倒计时10秒后引导 zroot 上的那个内核。老天保佑一次过,就能成功进入系统了。

6. 重新 import 其他 zpool、把金士顿 attach 组成 mirror pool

之前忘了 export 其他 zpool 的话,开机进系统之后用 zpool import 显示可以导入的 pool,然后通常来说都要 zpool import -f 池子名字 来强制导入的。

接下来用 attach 来将金士顿跟三星组合。根据第3步开头的那些信息(如果不确定,可以用 zpool status -v来确认 zroot 现在的设备),注意参数顺序,是 三星 在前面,金士顿 在后面:

zpool attach -f zroot ata-Samsung_SSD_840_EVO_250GB_S1DBNSAF222103J ata-KINGSTON_SA400S37480G_50026B7380698064

如果拿不准,就先不加-f参数,应该会产生提示信息类似于:“金士顿这个设备上有ext4文件系统,如果确定要使用它的话请加上 -f 参数”。

就搞定了。

发表评论