我一直是非常反对重装系统的。从技术上说,今天的折腾并不算是重装系统,不过因为把机器上所有的数据(是的,文件系统全部都拆掉重建了)都重写了一遍,所以还是算做了一次吧。
缘起
在采购 家里的路由器 的时候,选择了 WD 的 AV-25【1】 系列硬盘。我选的那款硬盘使用的是新式的 AF (4kiB扇区)格式。
FreeBSD 使用的主流文件系统 UFS 和 ZFS,以及 ahci(4) 驱动都 直接支持 4kiB 扇区。但是,目前市面上的AF硬盘,为了与先前的 BIOS 和操作系统(主要是 Windows XP)兼容,对于 ATA IDENTIFY 的回应,原先返回扇区尺寸的位置变成了逻辑扇区尺寸,这种做法俗称512e,即硬盘通过固件或其他方式模拟山区尺寸为512字节,并处理相关的回写操作。
以512字节为单位进行读写时,在AF格式的硬盘上是低效的。FreeBSD的 ahci(4) 驱动和对应的 ada(4) 驱动会设置 stripesize 以反映驱动器采用的实际物理扇区尺寸,但文件系统并不直接识别这个尺寸。
对于 ZFS 而言,其扇区尺寸是在创建时以 ashift 值写死的,目前在命令行没有办法指定这个值,也不能在创建 ZFS 之后修改。如果修改内核令其使用 GEOM 的 stripesize 来产生 ashift,对 AF 硬盘则会出现内核得到的 ashift 比先前已经存在的 ashift 大,从而导致 ZFS 无法识别的问题(如果创建 ZFS 时已经使用了更大的 ashift 则没有关系)。因此,必须想办法让 ZFS 在创建时就知道扇区尺寸是 4KiB。
FreeBSD 5.3-RELEASE 时新增了一个调试用的 GEOM class —- gnop。可以用它来封装其他 GEOM 对象,并改变扇区尺寸,方法是 gnop create -S 4096 /dev/gpt/store (此处 /dev/gpt/store 是一个按 4k 对齐的 GPT 分区的 label)。gnop会产生一个新的设备节点,/dev/gpt/store.nop,其向系统汇报的扇区尺寸是我们指定的 4096 字节,而不是驱动器汇报的逻辑扇区尺寸 512 字节。
使用这个设备节点创建的 ZFS 就会采用正确的 ashift 值了。
使用 zdb -C pool名字可以检查 ashift 值:对于扇区尺寸为 512 字节的 zpool,其 ashift 是 9,而我们希望的 ashift 值是12。
gnop节点在系统重启以后会消失,但 ZFS 会记住 ashift,因此并不会导致问题。此处也可以 zpool export,gnop destroy /dev/gpt/store.nop 然后再 zpool import 来验证。
经测试,ZFS在知道正确的扇区尺寸以后,持续写操作的性能可以提高至少一倍。
折腾
作为一个不折腾就会死星人,一倍的性能改善是有非常大的诱惑力的。因为我有两块硬盘,总容量刚好用到45%而且也没做成 mirror 或 stripe,所以正好倒腾一下。
首先是删除不必要的数据。凡是在家中有备份的数据一律删除(我自己的笔记本的系统文件和日常工作的数据备份共腾出约70GB)。
然后首先是把所有的数据复制到系统所在的 zpool 上。
制作快照:
zfs snapshot -r backup@pre-4k; zfs send -R backup@pre-4k | zfs receive -Fvd system/backup
确认备份可用且无数据校验错误之后,撤销备份卷:
zpool destroy backup
友情提示:这一步请在意识清醒的时候做。
然后为备份卷创建gnop设备:gnop create -S 4096 /dev/gpt/backup
由于之前创建这套系统时没有考虑引导部分的冗余,因此希望在重建系统时加入这方面的考虑。用gpart删掉备份卷所在分区,创建一个4GiB的freebsd-zfs类型的分区,剩余的空间作为另一个freebsd-zfs分区,并标记为新的备份卷设备:
gpart delete -i 3 ada1; gpart add -s 8388608 -t freebsd-zfs -l boot1 ada1; gpart add -t freebsd-zfs -l backup ada1
此处3、ada1是我的备份卷对应的分区编号和设备。boot1因为要做成mirror,因此暂时先不用。
重新创建备份卷:
zpool create backup /dev/gpt/backup.nop
用zdb确认ashift已经是12;
卸下备份卷并取消gnop设备:
zpool export backup; gnop destroy /dev/gpt/backup.nop
重新加载备份卷:
zpool import backup
此时可以再次用zdb检查ashift值(仍是12)。
从系统卷中恢复全部dataset
zfs send -R system/backup@pre-4k | zfs receive -Fvd backup
用类似的方法将系统卷复制到备份卷上。
由于系统卷在引导时一直在占用状态,因此不能直接在系统中将其卸下。使用插在主板上的USB stick上的系统引导之后:
删除系统卷所在的分区,并重新划分:
gpart delete -i 3 ada0; gpart add -s 8388608 -t freebsd-zfs -l boot0 ada0; gpart add -t freebsd-zfs -l neptune ada0
建立一个普通的系统卷【2】,使用两块盘上的boot0、1分区做成mirror:
zpool create -O canmount=off -O atime=off -O setuid=off system mirror /dev/gpt/boot[01]
建立用于引导系统的根dataset:
zfs create -m legacy -o compression=on -o setuid=on system/root
加载backup卷:
zpool import -R /mnt
挂载backup/system/root并将内容复制过来:
mkdir /tmp/src /tmp/dst; mount -t zfs backup/system/root /tmp/src; mount -t zfs system/root /tmp/dst; cd /tmp/src; tar cf – . | tar xf – -C /tmp/dst/
调整的具体操作在此不再赘述。简要说明:
原先的 system 卷包含了引导系统和日常运行的所有数据。此次调整将日常运行的其他数据独立到一个新的卷 neptune 上,以方便维护和备份(系统卷很少会出现变化,而其出现问题的修起来很麻烦,所以变为mirror卷)
除了system卷之外,所有的mountpoint均为canmount=off或inherit上层。
/usr/local、/home等拆到 neptune 卷上以方便备份。
创建了必要的符号链接。
迁回过程比预想的快的多(另外针对使用ashift=12和ashift=9两种情况都进行了测试)。
迁移过程中遇到的问题和教训
在拆分时发现之前的一个系统用户的 /home 目录不是 zfs,而是直接放在了根目录中。
在使用 zfs receive 恢复快照时,由于 -R 会采用完整的路径,因此单独创建了一个 recv 目录,然后将上层dataset以 rename 的方式放回 system 卷对应的位置时,由于上层 dataset 并没有从源处接受,导致 /usr 被空目录遮盖。远程 root 登录之后 zfs set canmount=off system/usr 后恢复。
由于事先做好了计划并留有备份,整个迁移过程只导致了短暂停机,而没有损失数据。
平时要多看代码和做性能测试,特别是在数据还不太多的时候。数百GB的数据即使只是在本地跑一遍也是很费时的操作。
其他说明事项
【1】请特别注意:WD的AV系列硬盘是针对流媒体而不是普通的数据应用设计的,在遇到数据错误时并不会像普通硬盘那样反复重试。在我的设计中采用的是异地冗余备份,如果只打算装一块硬盘,或不使用带校验功能的文件系统如ZFS,则不推荐这一系列。
【2】系统卷中的文件主要是引导系统时用到的,采用传统的512字节扇区。