UEFI启动与RAID1配置:Linux系统引导的冗余解决方案

本文详细探讨了在UEFI环境下配置Linux的RAID1冗余启动分区的方法,包括mdadm元数据设置、GRUB安装问题解决及应对UEFI写入风险的策略,适合系统管理员和Linux爱好者参考。

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国际许可协议授权。

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