前言
某日在群里水群时和群友讨论在 Linux 上使用何种文件系统较理想。BtrFS 作为 Linux 的新一代文件系统,在各方面都相当出色。但 BtrFS 在出问题之后的维护似乎会很麻烦,容易不小心做出错误的操作。
有群友提出使用 ZFS 作为 Linux 的文件系统。ZFS 是 Sun Microsystems 为 Solaris 开发的文件系统。ZFS 也具有 BtrFS 所具有的许多功能,但 ZFS 在许多概念上还要再先进一些。尽管 ZFS 是为 Solaris 开发的文件系统,OpenZFS 使得 Linux 和 FreeBSD 上也能够使用 ZFS。
由于许可证不兼容(OpenZFS 使用 CDDL 许可证),因此对 ZFS 的支持无法整合到 Linux 内核中,而只能以内核模块的形式加载。这会让整个安装过程变得更加麻烦……但我并不介意折腾!
本次安装的流程参照 ArchWiki 上的这篇文章。此次安装将会使用到 Arch ZFS 这个非官方用户仓库。
ZFS 的一些基本概念
你说的对,但 ZFS 是一款由 Sun Microsystems 自主研发的一款全新卷系统管理器和文件系统。文件系统运行在一个被称作「Linux」的幻想世界,在这里,被 root 选中的存储池将被授予「lz4」,导引存储之力。你将扮演一位名为「系统管理员」的神秘角色,在自由的 zpool create 中邂逅功能各异、feature 独特的 vdevs 们,和他们一起「state: DEGRADED」,找回失散的数据集——同时,逐步发掘 「BtrFS」 的真相……
vdev
vdev (Virutal Device) 是组成存储池的基本单位。vdev 可以由一个或多个的硬盘、分区或文件组成(在实际应用中强烈不推荐使用文件)由多个硬盘、分区或文件组成的 vdev 可以像阵列一样设置 RAID 类型。ZFS 中的 vdev 支持条带化 (RAID 0)、镜像 (RAID 1) 和 ZFS 提供的阵列类型 RAID Z1/Z2/Z3 等。也就是说,一个 vdev 通常可以看作一是一个硬盘或一个阵列。
存储池 (Pool)
存储池可以视作一个存储管理器,也可以简单地视作一个大的存储设备。存储池也可以由一个或多个的 vdev 组成。存储池中的 vdev 可以用于存储数据、作为读/写缓存或作为冗余盘组。一个存储池中可以拥有多个用于存储数据的 vdev,但需要注意的是,这些 vdev 之间以类似 RAID 0 的方式组织。
数据集 (Dataset)
如果把存储池视作硬盘(准确来讲是文件系统),那么数据集就可以视作其中的文件夹。数据集也可以设置多种属性,如压缩或加密,还可以设置挂载点以挂载到特定的目录上。数据集的大小是非固定的,其大小将会随着所存储数据的大小变化。数据集的这些特性使得数据集可以用于分类不同的数据。
在 ZFS 上安装 Arch Linux
在 ArchISO 上加载 ZFS 模块
想要在 ZFS 上安装 Arch Linux,需要先在 ArchISO 环境下创建一个存储池。然而 ArchISO 显然并不会集成 ZFS 的支持,因此我们需要先在 ArchISO 上获取并加载 ZFS 模块。eoli3n/archiso-zfs
这个项目提供了一个用于在 ArchISO 环境下安装 ZFS 模块的脚本。这个脚本将会获取当前运行的 ArchISO 的内核版本,并尝试在 archzfs
仓库中寻找匹配的 ZFS 模块。如果没有找到匹配的模块,脚本将会使用 DKMS 来从源代码编译一个 ZFS 模块。
硬盘分区
硬盘的分区很大程度上与启动方式有关。在这里以使用 UEFI 启动和 GPT 分区表,并且使用 GRUB 作为 Bootloader 为例。
我们将会把硬盘分为三个区:
- EFI 系统分区 (存储 EFI 引导程序)
- Boot 分区 (存储 Linux 内核和 initrd)
- Arch Linux 系统分区 (我们将会在这里创建 ZFS 存储池)
需要注意的是,包含 ZFS 文件系统的分区的类型应该被设置为 Solaris Root (bf00
)。
然后就可以先在 EFI 系统分区和 Boot 分区上创建文件系统,别忘了将设备名称替换为你自己的设备名称。
$ mkfs.fat /dev/<esp-partition>
$ mkfs.ext4 /dev/<boot-partition>
配置 ZFS 文件系统
创建存储池
使用如下的命令来创建一个存储池。若设备的物理扇区大小为 4096 字节,则应设置 ashift=12
;若设备的物理扇区大小为 512 字节,则应设置 ashift=9
。
$ zpool create -f -o ashift=12 \
> -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/id-to-partition-partx
在这个存储池中的数据集也会继承存储池的一部分属性。
创建数据集
创建数据集可以将系统和用户数据分离开,这样可以单独为系统创建快照。在有需要时可以从指定的快照启动系统,但本文不会涉及如何配置从快照启动。
首先创建系统和用户数据的数据集。
$ 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/home
zroot/ROOT
将会用于存储所有的系统快照,而 zroot/ROOT/default
则是默认系统的数据集。
对于某些系统目录,还需要设置 canmount=off
。
$ zfs create -o mountpoint=/var -o canmount=off zroot/var
$ zfs create zroot/var/log
$ zfs create -o mountpoint=/var/lib -o canmount=off zroot/var/lib
$ zfs create zroot/var/lib/libvirt
$ zfs create zroot/var/lib/docker
你们也许会注意到,在这里我们创建了一个挂载点为 /var
的数据集 zroot/var
,但是却将其设置为不可挂载。
推测创建这样的一个数据集的目的是继承挂载点属性。mountpoint
属性是可继承的,而 canmount
属性是不可继承的。因此,尽管数据集 zroot/var
无法挂载,但数据集 zroot/var/log
却是可以挂载的,并且将会挂载到 /var/log
。
在此情况下,存储在 /var
中的其它文件将会存储在 zroot/ROOT/default
中,而存储在 /var/log
中的文件将会存储在 zroot/var/log
中。
导出/导入存储池
为了确保我们的配置没有问题,我们需要先导出存储池并重新导入存储池。
$ zpool export zroot
$ zpool import -d /dev/disk/by-id -R /mnt zroot -N
然后挂载所有数据集。
$ zfs mount zroot/ROOT/default
$ zfs mount -a
复制 zpool.cache
/etc/zfs/zpool.cache
会存储已经导入的存储池的配置信息。当导入一个存储池的时候,这个存储池的信息也会被添加到 zpool.cache
当中。对于已经导入过的存储池,需要再次导入时直接执行指令 zfs import
即可。否则需要使用 zfs import -d /dev/disk/by-id
来导入。
将 zpool.cache
复制到我们要安装的系统当中,这个文件在启动过程中是需要的。
$ cp /etc/zfs/zpool.cache /mnt/etc/zfs/zpool.cache
如果 zpool.cache
不存在的话,那就用这个指令创建。
$ zpool set cachefile=/etc/zfs/zpool.cache zroot
安装 Arch Linux
首先,你还需要手动挂载 EFI 系统分区和 Boot 分区,并生成 fstab
。
$ mount --mkdir /dev/esp-partition /mnt/efi
$ mount --mkdir /dev/boot-partition /mnt/boot
$ genfstab -U -p /mnt >> /mnt/etc/fstab
然后安装 Arch Linux。
$ pacstrap -K /mnt base linux linux-firmware ...
接下来 chroot 进入安装好的新系统中。
$ arch-chroot /mnt
安装 ZFS
此时的新系统中还没有安装 ZFS 的支持,所生成的 initrd 中自然也不包含对 ZFS 的支持。此时如果重新启动,是无法挂载根文件系统。我们要先安对 ZFS 的支持,首先需要将 Arch ZFS
仓库添加到 /etc/pacman.conf
中。然后安装 zfs
包。
在安装好 zfs
之后,你还需要修改 mkinitcpio
的配置文件,确保 ZFS 模块能够被集成到 initrd 中。请修改 hooks,将 zfs
添加到 filesystems
之前,并且把 keyboard
也移动到 zfs
之前。
HOOKS=(base udev autodetect modconf block keyboard zfs filesystems ...)
关于 ZFS 包的选择
有多个包可以提供 ZFS 支持,它们适配不同版本的 Linux 内核。例如 zfs-linux
适配 linux
、zfs-linux-lts
适配 linux-lts
。然而 Arch ZFS 的更新通常会比内核的更新要慢一截,因此在内核版本更新时,Arch ZFS 中的包可能无法及时跟上内核的版本而导致内核无法更新。zfs-dkms
可以通过从源代码编译 ZFS 模块来适配更多的内核版本,但当内核出现大版本更新时,也可能会跟不上内核版本。
个人推荐使用 zfs-dkms
,但请务必注意在每次更新 zfs-dkms
或 linux
包之后使用 mkinitcpio
重新创建 initrd。你可以通过 pacman hook 来实现更新后重新创建 initrd。
创建 /etc/pacman.d/hook/90-mkinitcpio-dkms-linux.hook
:
[Trigger]
Operation=Install
Operation=Upgrade
Operation=Remove
Type=Package
Target=zfs-dkms
Target=linux
[Action]
Description=Update Linux init cpio after dkms module update.
Depends=mkinitcpio
When=PostTransaction
NeedsTargets
Exec=/bin/sh -c 'while read -r trg; do case $trg in linux) exit 0; esac; done; /usr/bin/mkinitcpio -p linux'
遇到的麻烦
大约是半个月前,当时我正在试图把 Arch Linux 装到 ZFS 上(也就是本篇文章介绍的内容)。
刚刚安装好的时候一切顺利,内核版本为 6.2.11-arch1-1
,而 zfs-linux
正好也支持这个版本。然后我 pacman -Syu
了一下,发现上游内核更新到了 6.2.12-arch1-1
,但 zfs-linux
还没有更新。依赖关系无法满足,因此无法执行系统更新。
于是我就换用了 zfs-dkms
,然后执行系统更新。一切看起来都没什么问题,直到我某次重启过后喜提一个 Kernel Panic。我似乎忘了更新 initrd,dkms 编译好的 ZFS 模块实际上并没有被整合到 initrd 中。
于是我就不得不再进入 ArchISO 环境,安装 ZFS,然后把系统分区挂载上,再 chroot 进去用 mkinitcpio
重新构建 initrd。为了避免再次出现这样的情况,我添加了一个 hook 来让 dkms 模块或内核更新时自动重新构建 initrd.
更多的麻烦
在我写这篇文章的前一天,我也 pacman -Syu
了一下,到最后 mkinitcpio
的时候报错 module not found。
我心想可能是 zfs-dkms
的编译出了问题,于是我就试着重新编译了一下 zfs-dkms
。结果发现 linux
内核已经更新到了 6.3.1-arch1-1
,但 zfs-dkms
最高也只支持到 6.2
,这下就只能降级内核版本了:
$ pacman -U /var/cache/pacman/pkg/linux-6.2.12.arch1-1-x86_64.pkg.tar.zst /var/cache/pacman/pkg/linux-headers-6.2.12.arch1-1-x86_64.pkg.tar.zst
安装 Bootloader
在确保安装了 grub
包之后,使用如下的指令将 GRUB 安装到 EFI 系统分区上:
$ grub-install --target=x86_64-efi --efi-directory=/efi --bootloader-id=GRUB
然后生成 GRUB 的配置文件:
$ grub-mkconfig -o /efi/grub/grub.cfg
配置 ZFS 的自动导入和挂载
为了在启动时导入存储池,需要启动以下的服务:
zfs.target
zfs-import.target
zfs-import-cache.service
如果你想要让指定的存储池在启动时自动导入,使用如下的指令:
$ zpool set cachefile=/etc/zfs/zpool.cache <pool-name>
而挂载则有两种选择:
- 启动服务
zfs-mount.service
- 使用 zfs-mount-generator
如果先前配置了将数据集挂载到某些系统目录(例如 /var
)上,则需要使用 zfs-mount-generator。
使用 zfs-mount-generator
首先需要首先创建目录 /etc/zfs/zfs-list.cache
,并启动 ZFS Evene Daemon (ZED) 服务 (zfs-zed.service
)。
对于你想要自动挂载数据集的存储池,你需要创建一个空文件:
$ touch /etc/zfs/zfs-list.cache/<pool-name>
然后修改任意 ZFS 文件系统的属性,ZED 会捕捉到这样的修改,并更新在目录 /etc/zfs/zfs-list.cache
中的文件。
$ zfs set canmount=off zroot/anyDataset
$ zfs set canmount=on zroot/anyDataset
设置 hostid
ZFS 使用 hostid 来追踪存储池是在哪个设备上使用的。在挂载根文件系统的时候,hostid 是不可用的。我们可以将 hostid 写入内核启动参数中,或者将 hostid 写入 /etc/hostid
中,/etc/hostid
中记录的 hostid 将会在生成 initrd 时被写入 initrd 中。用下面的命令可以生成一个 hostid 并写入 /etc/hostid
中。
$ zgenhostid $(hostid)
当然,写入完成后别忘了用 mkinitcpio
重新生成 initrd。
导出存储池
退出 chroot 环境,首先取消挂载 EFI 系统分区和 Boot 分区:
$ umount /mnt/efi
$ umount /mnt/boot
接下来取消挂载所有数据集并导出存储池:
$ zfs umount -a
$ zpool export zroot
最后重启设备,安装流程就结束了。
结语
中间遇到的各种小麻烦和做错的地方也不少,所以为此我甚至通宵了一个晚上呢。虽然说很疲倦,不过总之是学到很多。