线上重做 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,并且尽量不要搞到必须开去机房的程度。
原有的分区结构是:
| 编号 | 尺寸 | 类型 | 说明 |
|---|---|---|---|
| 1 | 128kB | freebsd-boot | 用于从ZFS读取引导加载器的第二级引导记录 |
| 2 | 16GB | freebsd-zfs | 根zpool池 |
| 3 | 32GB | freebsd-swap | 交换区 |
| 4 | 余下空间 | freebsd-zfs | cache |
首先删除3、4两个分区腾出空间来:
| |
删除后的分区表如下:
| 编号 | 尺寸 | 类型 | 说明 |
|---|---|---|---|
| 1 | 128kB | freebsd-boot | 用于从ZFS读取引导加载器的第二级引导记录 |
| 2 | 16GB | freebsd-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做完上述操作之后分区如下:
| 编号 | 尺寸 | 类型 | 说明 |
|---|---|---|---|
| 1 | 128kB | freebsd-boot | 用于从ZFS读取引导加载器的第二级引导记录 |
| 2 | 16GB | freebsd-zfs | 根zpool池 |
| 3 | 128kB | freebsd-swap | 占位分区 |
| 4 | 16GB | freebsd-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| 编号 | 尺寸 | 类型 | 说明 |
|---|---|---|---|
| 1 | 128kB | freebsd-boot | 用于从ZFS读取引导加载器的第二级引导记录 |
| 4 | 16GB | freebsd-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 | 256kB | freebsd-boot | 用于从ZFS读取引导加载器的第二级引导记录 |
| 2 | 16GB | freebsd-zfs | 根zpool池 |
接下来就可以重建swap和cache了:
| |
最终结果:
| 编号 | 尺寸 | 类型 | 说明 |
|---|---|---|---|
| 1 | 256kB | freebsd-boot | 用于从ZFS读取引导加载器的第二级引导记录 |
| 2 | 16GB | freebsd-zfs | 根zpool池 |
| 3 | 32GB | freebsd-swap | 交换区 |
| 4 | 余下空间 | freebsd-zfs | cache |
2021-05-29补充
事后研究了一下增加的到底是什么,答案是Toomas Soome去年实现的Framebuffer支持。这个支持做的功能还挺全的,在笔记本 (采用了EFI,因此相对来说对空间非常不敏感) 上第一次看到的时候没想过一下几十KB就出去了。
总结
总体上,这个问题应该是一个以后不会再遇到的问题:大部分新系统都支持EFI引导了,而EFI引导时,EFI分区可以有数百MB。由于不是实模式,对引导程序的尺寸要求也没有那么严格了。
与几年前我做FreeNAS升级器将UFS在线换成ZFS相比,由于ZFS本身支持了镜像并提供了灵活的拆装方法,因此并不需要引入一个额外的ramdisk设备来规避无法直接操作 / 所在分区的问题。
不过我还是得吐槽一下,这引导加载器的尺寸增加的简直就是曼德勃罗集无限放大啊!最后送给各位读者一首Jonathan Coulton的 Mandelbrot Set。