delphij's Chaos

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

23 May 2021

线上重做 FreeBSD GPT 引导分区

记一笔。

我现在这台服务器已经用了蛮久(超过10年)了,刚刚安装的时候还是 FreeBSD 8.1 RC,一路升级到 FreeBSD 12.2。 前段时间因为工作比较忙,一直没顾上把它升级到 FreeBSD 13.0,今天总算是找了点时间来做升级。

总体上这次升级比预想的还要平稳一些,由于 FreeBSD 13.0 和 12.2 的内核接口变化不大,因此我也没按照正规的做法, 即升级内核、重启第一次、etcupdate、installworld,重启第二次、delete-old/delete-old-libs,而是在 installkernel 之后直接做了 etcupdate 和 installworld。

惊喜/惊吓

使用 ZFS 做 / 的系统,由于 ZFS 还在不断增加新的功能,因此在更新时,有可能需要更新系统的引导加载器等一系列部件。 这台机器因为是2010年左右的,并不支持 EFI 引导,因此我在装机的时候采用的是 gptzfsboot 的混合引导结构。 这个引导流程是 BIOS 读取 MBR (尽管采用的是GPT分区,但为了兼容旧的 BIOS,依然存在一个 protective MBR,在 BIOS看来它和普通的主引导记录并无二致,其唯一的功能是在盘上找到 freebsd-boot 分区,并从中读出 gptzfsboot 开始第二阶段的引导过程)。我们需要升级的是 gptzfsboot,因为它知道怎么从 ZFS 中读取数据, 这一点对于引导系统来说相当关键。

而我当年在这个 SSD 上留给 gptzfsboot 的空间只有 128kB。 这个倒不是抠门,而是因为实模式最多只能用 640kB 的内存,而 MBR 也就 512 字节大, 在里面进入保护模式,还要处理各种复杂情况不太现实。 gptzfsboot 在早期版本的 FreeBSD 中只有 40多kB,即使到 FreeBSD 12.2,也才 53kB 而已。 既然都不到 64kB,我感觉放 128kB 的分区已经富富有余了,结果果然和门叔犯了同样的错误。

为了支持新的 ZFS 功能,我准备升级 gptzfsboot 和 Protective MBR:

gpart bootcode -b /boot/pmbr -p /boot/gptzfsboot -i 1 ada0

系统提示:ada0p1空间不足。

再一看,gptzfsboot 已经从上一版本里 53kB 的小不点涨到了 155kB。想起了电影《让子弹飞》姜文的那句“什么叫惊喜”……

幸运的是 gpart 在开始操作前检查了一下而没有直接开始写入,因为如果那样的话,把引导分区破坏掉的话只要机器一重启就彻底完蛋了。

这个SSD上的其他内容有:加密的swap分区(32GB)以及加密的 ZFS L2ARC 缓存。必要时这两个都可以临时牺牲掉, 但是一个比较明显的问题是 / 所在的那个 zpool 需要往后挪至少 128kB,而并没有现成的工具这么做。

方案

总体的思路是:利用 ZFS 的镜像功能,先把可以删掉的分区删掉,然后创建一个临时的分区,加入镜像,删除掉原分区,调整引导分区尺寸,最后把临时分区的内容再搬回重建出来的根存储池分区, 最终恢复原状。目标是把 freebsd-boot 分区从 128k 增加到 256k,并且尽量不要搞到必须开去机房的程度。

原有的分区结构是:

编号尺寸类型说明
1128kBfreebsd-boot用于从 ZFS 读取引导加载器的第二级引导记录
216GBfreebsd-zfs根 zpool 池
332GBfreebsd-swap交换区
4余下空间freebsd-zfscache

首先删除3、4两个分区腾出空间来:

# 从存储池中摘下cache
$ sudo zpool remove sirius gpt/cache.eli
# 停止geli设备
$ sudo geli stop gpt/cache.eli
# 删除分区
$ sudo gpart delete -i 4 ada0
# 停用 swap
$ sudo swapoff -a
# 删除分区
$ sudo gpart delete -i 3 ada0

删除后的分区表如下:

编号尺寸类型说明
1128kBfreebsd-boot用于从 ZFS 读取引导加载器的第二级引导记录
216GBfreebsd-zfs根 zpool 池

接下来创建一个与根zpool同样大的镜像分区。为了方便起见,我们在中间再插一块128kb的空地(省得计算新分区的起始坐标),并在后面创建一个16GB的ZFS分区:

# 占位分区
$ sudo gpart add -t freebsd-swap -s 256 ada0
# 临时根存储池分区
$ sudo gpart add -t freebsd-zfs -l sirius-boot2 -s 16g ada0

做完上述操作之后分区如下:

编号尺寸类型说明
1128kBfreebsd-boot用于从 ZFS 读取引导加载器的第二级引导记录
216GBfreebsd-zfs根 zpool 池
3128kBfreebsd-swap占位分区
416GBfreebsd-zfs根 zpool 池

之后将新创建的freebsd-zfs分区加到根存储池:

$ sudo zpool attach sirius-boot gpt/sirius-boot /dev/gpt/sirius-boot2

确认根存储池可用(保险起见,可以在resilver完成之后再做一次 zpool scrub),摘掉旧分区:

$ sudo zpool detach sirius-boot /dev/gpt/sirius-boot
$ sudo gpart delete -i 2 ada0
编号尺寸类型说明
1128kBfreebsd-boot用于从 ZFS 读取引导加载器的第二级引导记录
416GBfreebsd-zfs根 zpool 池

调整第二级引导记录所在的分区尺寸,并重建根存储池的分区:

$ sudo sudo gpart resize -i 1 -s 512 ada0
$ sudo gpart add -t freebsd-zfs -l sirius-boot -s 16g ada0

注意此处创建分区尺寸为 16GB。如果不指定 -s 16g,系统会使用余下的全部空间。 这里要小心:如果不慎添加了一个更大的设备到镜像中,有些操作,例如 zpool online -e 会把对应的虚拟设备增大,那样的话就无法再恢复原来的样子了。

|编号|尺寸|类型|说明| |1|256kB|freebsd-boot|用于从 ZFS 读取引导加载器的第二级引导记录| |2|16GB|freebsd-zfs|根 zpool 池| |4|16GB|freebsd-zfs|根 zpool 池|

将重建的分区接回根存储池,并将临时分区摘下、删除:

# 将重建后的分区接回存储池
$ sudo zpool attach sirius-boot gpt/sirius-boot2 /dev/gpt/sirius-boot
$ sudo zpool scrub sirius-boot
# 等待scrub做完,摘下临时分区
$ sudo zpool detach sirius-boot /dev/gpt/sirius-boot2
# 删掉临时分区
$ sudo gpart delete -i 4 ada0
编号尺寸类型说明
1256kBfreebsd-boot用于从 ZFS 读取引导加载器的第二级引导记录
216GBfreebsd-zfs根 zpool 池

接下来就可以重建 swap 和 cache了:

$ sudo gpart add -l swap -s 32g -t freebsd-swap ada0
$ swapon -a
# (检查 geli 设备的创建情况)
$ sudo gpart add -l cache -a 4k -t freebsd-zfs ada0
$ sudo geli init ...

最终结果:

编号尺寸类型说明
1256kBfreebsd-boot用于从 ZFS 读取引导加载器的第二级引导记录
216GBfreebsd-zfs根 zpool 池
332GBfreebsd-swap交换区
4余下空间freebsd-zfscache

2021-05-29补充

事后研究了一下增加的到底是什么,答案是 Toomas Soome 去年实现的 Framebuffer 支持。 这个支持做的功能还挺全的,在笔记本 (采用了EFI,因此相对来说对空间非常不敏感) 上第一次看到的时候没想过一下几十KB就出去了。

总结

总体上,这个问题应该是一个以后不会再遇到的问题:大部分新系统都支持 EFI 引导了,而 EFI 引导时,EFI 分区可以有数百MB。 由于不是实模式,对引导程序的尺寸要求也没有那么严格了。

与几年前我做 FreeNAS 升级器将 UFS 在线换成 ZFS 相比,由于 ZFS 本身支持了镜像并提供了灵活的拆装方法, 因此并不需要引入一个额外的 ramdisk 设备来规避无法直接操作 / 所在分区的问题。

不过我还是得吐槽一下,这引导加载器的尺寸增加的简直就是曼德勃罗集无限放大啊!最后送给各位读者一首 Jonathan Coulton 的 Mandelbrot Set