线上重做 FreeBSD GPT 引导分区

• 本文约 2401 字,阅读大致需要 5 分钟 | Development | #FreeBSD | #ZFS | #gpart | #boot | #GPT | #Live Migration | #系统管理 | #System Administration

记一笔。

我现在这台服务器已经用了蛮久(超过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两个分区腾出空间来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 从存储池中摘下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池|

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

1
2
3
4
5
6
7
# 将重建后的分区接回存储池
$ 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了:

1
2
3
4
5
$ 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