UEFI启动与RAID1
昨天我花了一些时间构建一台UEFI服务器,该系统驱动器没有板载硬件RAID。在这种情况下,我总是使用Linux的md RAID1作为根文件系统(和/或/boot)。这对于BIOS启动很有效,因为BIOS只是盲目地将控制权转移给它看到的任何磁盘的MBR(需要找到“可启动分区”标志等)。这意味着BIOS并不真正关心驱动器上的内容,它会将控制权交给MBR中的GRUB代码。
在UEFI中,启动固件实际上会检查GPT分区表,寻找标记有“EFI系统分区”(ESP)UUID的分区。然后它在那里寻找FAT32文件系统,并执行更多操作,如查看NVRAM启动条目,或直接从FAT32运行BOOT/EFI/BOOTX64.EFI。在Linux下,这个.EFI代码要么是GRUB本身,要么是加载GRUB的Shim。
所以,如果我想为根文件系统使用RAID1,那没问题(GRUB会读取md、LVM等),但我如何处理/boot/efi(UEFI ESP)?在我找到的所有回答这个问题的资料中,答案都是“哦,只需在每个RAID驱动器上手动创建一个ESP并复制文件,为每个驱动器添加一个单独的NVRAM条目(使用efibootmgr),你就没问题了!”我一点也不喜欢这个方案,因为它意味着副本之间可能会不同步等。
Linux md RAID的当前实现在分区的前面放置元数据。这解决的问题比它带来的问题多,但它意味着对于不了解元数据的东西来说,RAID不是“不可见的”。事实上,mdadm对此发出了相当响亮的警告:
1
2
3
4
5
6
|
# mdadm --create /dev/md0 --level 1 --raid-disks 2 /dev/sda1 /dev/sdb1
mdadm: Note: this array has metadata at the start and
may not be suitable as a boot device. If you plan to
store '/boot' on this device please ensure that
your boot-loader understands md/v1.x metadata, or use
--metadata=0.90
|
从mdadm手册页阅读:
1
2
3
4
5
6
7
8
9
10
11
12
|
-e, --metadata=
...
1, 1.0, 1.1, 1.2 default
Use the new version-1 format superblock. This has fewer
restrictions. It can easily be moved between hosts with
different endian-ness, and a recovery operation can be
checkpointed and restarted. The different sub-versions
store the superblock at different locations on the
device, either at the end (for 1.0), at the start (for
1.1) or 4K from the start (for 1.2). "1" is equivalent
to "1.2" (the commonly preferred 1.x format). "default"
is equivalent to "1.2".
|
首先我们在RAID上放置一个FAT32(mkfs.fat -F32 /dev/md0),查看结果,前4K全是零,文件命令看不到文件系统:
1
2
3
4
5
6
7
|
# dd if=/dev/sda1 bs=1K count=5 status=none | hexdump -C
00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00001000 fc 4e 2b a9 01 00 00 00 00 00 00 00 00 00 00 00 |.N+.............|
...
# file -s /dev/sda1
/dev/sda1: Linux Software RAID version 1.2 ...
|
所以,我们改用–metadata 1.0将RAID元数据放在末尾:
1
2
3
4
5
6
7
|
# mdadm --create /dev/md0 --level 1 --raid-disks 2 --metadata 1.0 /dev/sda1 /dev/sdb1
...
# mkfs.fat -F32 /dev/md0
# dd if=/dev/sda1 bs=1 skip=80 count=16 status=none | xxd
00000000: 2020 4641 5433 3220 2020 0e1f be77 7cac FAT32 ...w|.
# file -s /dev/sda1
/dev/sda1: ... FAT (32 bit)
|
现在我们在ESP上有了一个可见的FAT32文件系统。UEFI应该能够启动任何没有故障的磁盘,并且grub-install将写入挂载在/boot/efi的RAID。
然而,我们面临一个新问题:在(至少)Debian和Ubuntu上,grub-install尝试运行efibootmgr来记录UEFI应该从哪个磁盘启动。但这失败了,因为它期望一个单独的磁盘,而不是一个RAID集。事实上,它什么也不返回,并尝试用一个空的-d参数运行efibootmgr:
1
2
3
4
5
6
|
Installing for x86_64-efi platform.
efibootmgr: option requires an argument -- 'd'
...
grub-install: error: efibootmgr failed to register the boot entry: Operation not permitted.
Failed: grub-install --target=x86_64-efi
WARNING: Bootloader is not properly installed, system may not be bootable
|
幸运的是,我的UEFI在没有NVRAM条目的情况下启动,我可以通过运行时的“更新NVRAM变量以自动启动到Debian?”debconf提示禁用NVRAM写入:dpkg-reconfigure -p low grub-efi-amd64
所以,现在我的系统将在两个或任一驱动器存在时启动,并且从Linux到/boot/efi的更新在启动时在所有RAID成员上可见。然而,这种设置有一个讨厌的风险:如果UEFI向其中一个驱动器写入任何内容(这个固件在写出“启动变量缓存”文件时确实这样做了),一旦Linux挂载RAID,可能会导致损坏的结果(因为成员驱动器将不再具有FAT32的相同块级副本)。
为了处理这种“外部写入”情况,我看到一些解决方案:
- 在不在Linux下时将分区设为只读。(我认为这不是一个可行的方案。)
- 需要更高级别的根文件系统RAID配置知识,以手动同步一组文件系统,而不是进行块级RAID。(似乎需要很多工作,并且需要将/boot/efi重新设计为类似/boot/efi/booted、/boot/efi/spare1、/boot/efi/spare2等的东西。)
- 偏好一个RAID成员的/boot/efi副本,并在每次启动时重建RAID。如果没有外部写入,就没有问题。(但选择偏好的副本的真正正确方式是什么?)
由于mdadm有“–update=resync”组装选项,我实际上可以执行后一个选项。这需要更新/etc/mdadm/mdadm.conf,在RAID的ARRAY行添加以防止它自动启动:
1
|
ARRAY <ignore> metadata=1.0 UUID=123...
|
(由于被忽略,我选择了/dev/md100用于下面的手动组装。)然后我在/etc/fstab中的/boot/efi条目添加了noauto选项:
1
|
/dev/md100 /boot/efi vfat noauto,defaults 0 0
|
最后,我添加了一个systemd oneshot服务,该服务使用resync组装RAID并挂载它:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
[Unit]
Description=Resync /boot/efi RAID
DefaultDependencies=no
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/sbin/mdadm -A /dev/md100 --uuid=123... --update=resync
ExecStart=/bin/mount /boot/efi
RemainAfterExit=yes
[Install]
WantedBy=sysinit.target
|
(别忘了运行“update-initramfs -u”,这样initramfs就有/etc/mdadm/mdadm.conf的更新副本。)
如果mdadm.conf支持ARRAY行的“update=”选项,这将变得很简单。但查看源代码,这种更改看起来并不容易。我可以梦想!
如果我想保留一个UEFI无法更新的“原始”版本的/boot/efi,我可以更戏剧性地重新安排事情,将主RAID成员作为根文件系统中文件上的环回设备(例如/boot/efi.img)。这将使真实ESP中的所有外部更改在重新同步后消失。类似这样:
1
2
3
4
|
# truncate --size 512M /boot/efi.img
# losetup -f --show /boot/efi.img
/dev/loop0
# mdadm --create /dev/md100 --level 1 --raid-disks 3 --metadata 1.0 /dev/loop0 /dev/sda1 /dev/sdb1
|
并在启动时仅从/dev/loop0重建它,尽管我不确定如何“偏好”那个分区……
注意:我用来搞乱grub-install的命令:
1
2
|
echo "grub-pc grub2/update_nvram boolean false" | debconf-set-selections
echo "grub-pc grub-efi/install_devices multiselect /dev/md0" | debconf-set-selections && dpkg-reconfigure -p low grub-efi-amd64-signed
|
更新:Ubuntu Focal 20.04现在提供了一种方式,让GRUB可以安装到任意设备集合,因此不需要RAID。呼。
© 2018 – 2020, Kees Cook。本作品根据知识共享署名-相同方式共享4.0国际许可协议授权。