UEFI引导与RAID1配置实战:解决ESP冗余与同步问题

本文详细探讨了在UEFI环境下配置Linux md RAID1用于根文件系统和/boot/efi分区的技术挑战,包括元数据位置调整、FAT32文件系统兼容性、grub-install问题及外部写入风险,并提供了手动同步和系统服务解决方案。

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全是零,file命令看不到文件系统:

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条目的情况下可以引导,我可以通过运行时的“Update NVRAM variables to automatically boot into Debian?” debconf提示禁用NVRAM写入:dpkg-reconfigure -p low grub-efi-amd64

所以,现在我的系统将在两个或任一驱动器存在时引导,并且从Linux到/boot/efi的更新在引导时在所有RAID成员上可见。然而,这种设置有一个讨厌的风险:如果UEFI向其中一个驱动器写入任何内容(该固件在写出“引导变量缓存”文件时这样做了),一旦Linux挂载RAID,可能会导致损坏的结果(因为成员驱动器将不再具有FAT32的相同块级副本)。

为了处理这种“外部写入”情况,我看到一些解决方案:

  1. 在不在Linux下时使分区只读。(我认为这不是一件事。)
  2. 创建对根文件系统RAID配置的更高级了解,需要手动同步一组文件系统,而不是进行块级RAID。(似乎需要很多工作,并且需要将/boot/efi重新设计为类似/boot/efi/booted、/boot/efi/spare1、/boot/efi/spare2等)
  3. 偏好一个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有更新的/dev/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国际许可协议授权。

评论(5)

5条评论

使用非RAID的引导分区。容易得多。 评论者:odi — 2018年4月19日 @ 9:55 pm

但那没有处理我的任何要求。:P 评论者:kees — 2018年4月19日 @ 11:21 pm

看起来UEFI智囊团完全回避了ESP冗余的问题,并将其全部留给了硬件供应商。这 predictably 导致了供应商特定(通常是脑死亡的)解决方案,这是不幸的。 评论者:yaneti — 2018年4月19日 @ 11:44 pm

在UEFI中,引导固件实际上会检查GPT分区表,寻找标记为“EFI系统分区”(ESP)UUID的分区。 UEFI规范不要求这样做。 相反,UEFI固件完全可以跳过这一步,只需寻找具有FAT文件系统的GPT分区,该文件系统在正确的位置有正确的文件。 我目前使用的所有UEFI固件都不关心GPT ESP UUID(即Super micro、Lenovo、Open Virtual Machine Firmware – OVMF)。 因此,您也可以将ESP的GPT分区类型设置为“Linux RAID”UUID – 从而明确表示FS是RAID的一部分。 也许这甚至有助于防止固件写入“引导变量缓存文件”(尽管我即使在非RAID设置中也没有注意到这样的缓存文件)。 顺便说一句,当在Fedora安装程序中选择镜像配置时,它也会在RAID-1上创建ESP(使用超级块格式1.0),并将GPT分区类型UUID设置为“Linux RAID”。Fedora的efibootmgr在这种配置下工作良好。 评论者:Georg Sauthoff — 2018年4月20日 @ 12:18 am

从字里行间看,我的UEFI固件内置了Intel Matrix Raid驱动程序,因此它可以从RAID读取ESP。所以我们所需要的只是一个干净室的BSD-2实现的mdadm驱动程序用于OVMF/EDKII,并在OEM固件中提供,不是吗?它可以非常基本,跳过这么多头并从偏移量开始读取。也许不写入任何东西,或者如果必须的话写入所有驱动器。 评论者:DImitri John Ledkov — 2018年6月12日 @ 2:12 am

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计