delphij's Chaos

选择chaos这个词是因为~~实在很难找到一个更合适的词来形容这儿了……

22 Apr 2024

如何:重建根 zpool

这篇和较早的 线上重做 FreeBSD GPT 引导分区 情况有些类似,但略有不同。

前段时间 Andriy Gapon 为 Samsung 860 / 870 SSD 增加了一个 quirk。 Samsung 的 SSD 内部使用的是 8K 或 16K 的存储页,但为了和业界标准兼容, 它的控制器为 4K 扇区做了优化。

当然这不是重点,重点是我老人家当年创建根存储池(纯爷们当然要从 ZFS 存储池启动系统)时, 竟然忘记了设置 ashift,而由于当年这个 SSD 汇报的扇区尺寸是 512 字节,因此 ZFS 也就愉快地创建了一个 512 字节扇区的根存储池。而经过改进之后,现在 GEOM 会正确地汇报扇区尺寸了:

$ geom disk list ada1
Geom name: ada1
Providers:
1. Name: ada1
   Mediasize: 1000204886016 (932G)
   Sectorsize: 512
   Stripesize: 4096
   Stripeoffset: 0
   Mode: r3w3e6
   descr: Samsung SSD 860 EVO 1TB
   lunid: XXXXXXXXXXXXXXXX
   ident: XXXXXXXXXXXXXXX
   rotationrate: 0
   fwsectors: 63
   fwheads: 16

这样一来, zpool status 就会汇报:

status: One or more devices are configured to use a non-native block size.
         Expect reduced performance.
action: Replace affected devices with devices that support the
         configured block size, or migrate data to a properly configured
         pool.

根存储池并不会经常读写,但看着实在是逼死强迫症,我觉得得做点什么。

备份我是做了的,而且我确定备份不是上回书小故事里的那样。 我不特别介意重装一遍,但是毕竟这得花更多的时间,而且因为我在笔记本上跑的是 -CURRENT, 还需要做一个启动的 U 盘,稍微有些麻烦。综合考虑了碎片、压缩等其他因素,我决定重建一个根存储池, 这样恰好把顺便数据整体重写一次(而不是做mirror)。

由于我的习惯是分配一个比较大(通常是和RAM相同尺寸的)的加密swap分区。现时的内存通常远大于根 pool 的尺寸, 因此可以把 swap 分区挪用来做临时存储空间。

首先是把加密的 swap 分区关闭:

$ sudo swapoff -a

然后是把swap分区尺寸改小:

$ sudo gpart show ada1
=>        40  1953525088  ada1  GPT  (932G)
          40      262144     1  efi  (128M)
      262184     8388608     2  freebsd-zfs  (4.0G)
     8650792    67108864     3  freebsd-swap  (32G)
    75759656  1877765472     4  freebsd-zfs  (895G)
$ sudo gpart resize -i 3 -s 28g ada1
$ sudo gpart add -t freebsd-zfs ada1

这样我们获得了一个新的、同样是 4GB / 8388608 扇区的 ada1p5。

将新设备作为 mirror 插入到根存储池,并拆下原先的 ada1p2:

$ sudo zpool attach p51-boot ada1p2 ada1p5
$ sudo zpool scrub p51-boot
$ zpool status p51-boot # 到scrub做完为止
$ sudo zpool detach p51-boot ada1p2

这样我们就把 p51-boot 存储池挪到了 ada1p5 上面。为了防止 zpool 再创建出 ashift=9 的存储池,修改 vfs.zfs.vdev.min_auto_ashift:

$ sudo sysctl vfs.zfs.min_auto_ashift=12

在 ada1p2 上重建根存储池:

$ sudo dd if=/dev/random of=/dev/ada1p2 bs=1m        # 抹去原来的数据,just in case
$ sudo zpool create -m none -o altroot=/tmp/1 -O atime=off -O setuid=off \
      -O compression=zstd -O canmount=off -O checksum=skein p51-boot2 /dev/ada1p2
$ sudo zfs create -o mountpoint=none p51-boot2/ROOT
$ sudo zfs create -o setuid=on -o mountpoint=legacy p51-boot2/ROOT/default

在原存储池上打一个快照以方便复制。

$ sudo zfs snapshot -r p51-boot2@20240422

(此处我是对整个存储池做了快照,但实际上我们只需要 ROOT/default 这一个文件系统,因此实际上也可以只快照它)

将快照复制到新的存储池:

$ sudo zfs send p51-boot/ROOT/default@20240422 | \
    sudo zfs receive -F -v -x compression p51-boot2/ROOT/default
$ sudo zfs destroy p51-boot2/ROOT/default@20240422

我之前使用的是 compression=on (lz4) 压缩,而新的存储池使用了 zstd。FreeBSD 支持从启用了这两种压缩算法的存储池引导系统,因此在接收快照时排除 compression 属性,直接使用本地的新值,这样重写时会以新的压缩算法重新压缩一遍。

我们可能需要修改一些东西。例如,我这个根存储池是从更早的旧式设计中迁移过来的,因此我的 /etc/fstab 中明确指定了 / 是来自 p51-boot (后来改成了 Boot Environment 风格的 p51-boot/ROOT/default)。现时已经不再需要明确指定(EFI引导加载器会根据 bootfs 正确设置),以及我们希望更新一下 /boot/zfs/zpool.cache 之类,所以挂载一下新的 /:

$ sudo mount -t zfs p51-boot2/ROOT/default /mnt
# 编辑必要的文件,例如删去 /etc/fstab 中关于 / 的定义,等等

这样基本上就做完迁移了,由于接下来的那次引导时引导加载器将看到两个未加密的 ZFS 存储池,因此有必要明确我们希望从 p51-boot2 启动系统:

$ sudo zpool set bootfs=p51-boot2/ROOT/default p51-boot2
$ sudo zpool set bootfs= p51-boot

重启,确定我们是从 p51-boot2 引导的系统之后就可以把原来的 p51-boot 删除掉了。我们顺便把加密swap分区归位:

$ sudo zpool destroy p51-boot                  # 毁掉原来的根存储池
$ sudo dd if=/dev/random of=/dev/ada1p5 bs=1m  # 抹去临时分区的数据,防止数据泄漏。
$ sudo swapoff -a                              # 卸下加密swap分区
$ sudo gpart delete -i 5                       # 删去临时的分区
$ sudo gpart resize -i 3 ada1                  # 不指定size表示充满此区域
$ sudo service swaplate start                  # 重新挂上swap分区

重启确认一下系统还能动,特别提醒,为了避免这一环节成为深夜提神节目,做本文所描述的任何操作之前, 请务必确认一下备份是完整的,勿谓言之不预。