9 minutes
GRUB mirrored boots sur Nixos
Ayant 3 NVMe, dont deux Western Digital SN850 de 1Tio qui me servent pour un special device ZFS, et un dernier (vieil)
NVME de 120Gio, avec des performances faibles, qui me sert de support de démarrage (/boot et /boot/efi) ainsi que de
swap.
Après quelques vérifications sur mon dashboard de monitoring de disques, j’ai remarqué que la latence sur le p’tit dernier, le NVMe qui sert de swap, sont terribles : des pics à >1s de latence de lecture lorsqu’il est sollicité pour swapper (entre 10MB/s et 60MB/s de lecture et d’écriture), rendant le serveur inopérant sur ces périodes (>10s de latence en SSH).
On va décomissionner le disque, et en profiter pour répliquer les partitions de démarrage.
GRUB mirrored boots
Plusieurs solutions sont possibles afin de redonder les partitions de démarrage (/boot, qui contient le kernel et les
modules Linux, et l’ESP /boot/efi qui contient les fichiers de bootloader GRUB pour l’UEFI). Par le passé, j’ai pu
configurer un RAID logiciel sur ces partitions pour les conserver identiques (via LVM), un peu contraignant à maintenir
en cas de remplacement de disques (car non déclaratif).
Évidemment, impossible d’utiliser ZFS puisque l’UEFI doit pouvoir charger les bootloaders .efi depuis une partition FAT32, pour pouvoir
démarrer GRUB. Pas de drivers disque avancés ici.
Aujourd’hui, NixOS propose une solution clé-en-main pour configurer plusieurs disques de démarrage, les garder synchronisés, sans a priori passer par une solution de RAID.
boot.loader.grub.mirroredBoots
Pour citer la documentation :
Mirror the boot configuration to multiple partitions and install grub to the respective devices corresponding to those partitions.
1[
2 {
3 devices = [
4 "/dev/disk/by-id/wwn-0x500001234567890a"
5 ];
6 path = "/boot1";
7 }
8 {
9 devices = [
10 "/dev/disk/by-id/wwn-0x500009876543210a"
11 ];
12 path = "/boot2";
13 }
14]
La solution permet de générer et d’écrire la configuration GRUB sur plusieurs partitions différentes.
La configuration boot.loader.grub.mirroredBoots.*.devices, au même titre que boot.loader.grub.device, n’est utile
que pour une installation MBR et non pas en UEFI. Ça n’est pas utile dans notre cas (et risque même de bloquer le boot).
Préparation des partitions
Le partitionnement étant toujours risqué, faisons les choses de manière sûre :
watch zpool status -vdans un onglet pour s’assurer qu’on ne casse pas les partitions ZFS- partitionnement d’un seul NVMe dans un premier temps
- installation du mirrored boot sur le premier NVMe
- redémarrage et vérification que tout fonctionne
- partitionnement du second disque et rebelotte
- décomissionnement du NVMe en fin de vie
En trois étapes :
0. /boot/efi (actuellement)
/boot/efiet/boot1/efi/boot/efi,/boot1/efiet/boot2/efi/boot1/efiet/boot2/efi(cible)
Note : mes disques n’ayant pas été initialement partitionnés pour accueillir le boot et l’ESP, mais ayant conservé de l’espace en fin de disque, les deux partitions seront positionnées à la fin du disque. Ce n’est pas un problème pour le démarrage, tant que l’ESP est positionné dans les premiers 2.2TB du disque.
Partitionnement
Rien de très fancy, je trouve parted hyper adapté pour ce genre de partitionnement où on a juste besoin d’éditer les
tables de partition. Avec les valeurs brutes de fdisk -l, ça permet très facilement de pouvoir revenir en arrière sans
perdre de données.
fdisk indiquant les positions et tailles des partitions en secteurs, c’est facile de retrouver son compte avec le
suffixe s sous parted pour l’unité en secteurs. Ça permet aussi de vérifier que deux disques en mirroir sont
alignés.
(Pour ces raisons, toujours penser à faire une copie de la sortie de fdisk -l avant de perdre l’historique du
terminal !)
# parted /dev/disk/by-id/nvme-XXXX
GNU Parted 3.4
Utilisation de /dev/nvme1n1
Bienvenue sur GNU Parted ! Tapez « help » pour voir la liste des commandes.
(parted) print
Modèle : WDS100T1X0E-00AFY0 (nvme)
Disque /dev/nvme1n1 : 1000GB
Taille des secteurs (logiques/physiques) : 512B/512B
Table de partitions : gpt
Drapeaux de disque :
Numéro Début Fin Taille Système de fichiers Nom Drapeaux
1 1049kB 876GB 876GB
2 876GB 905GB 29,0GB linux-swap(v1)
(parted) mkpart
Nom de la partition ? []? efi
Type de système de fichiers ? [ext2]? fat32
Début ? 1767972864s
Fin ? 1770072063s
(parted) print
Modèle : WDS100T1X0E-00AFY0 (nvme)
Disque /dev/nvme1n1 : 1000GB
Taille des secteurs (logiques/physiques) : 512B/512B
Table de partitions : gpt
Drapeaux de disque :
Numéro Début Fin Taille Système de fichiers Nom Drapeaux
1 1049kB 876GB 876GB
2 876GB 905GB 29,0GB linux-swap(v1)
3 905GB 906GB 1075MB fat32 efi
(parted) mkpart
Nom de la partition ? []? boot
Type de système de fichiers ? [ext2]? fat32
Début ? 1770072064s
Fin ? 1772171263s
(parted) print
Modèle : WDS100T1X0E-00AFY0 (nvme)
Disque /dev/nvme1n1 : 1000GB
Taille des secteurs (logiques/physiques) : 512B/512B
Table de partitions : gpt
Drapeaux de disque :
Numéro Début Fin Taille Système de fichiers Nom Drapeaux
1 1049kB 876GB 876GB
2 876GB 905GB 29,0GB linux-swap(v1)
3 905GB 906GB 1075MB fat32 efi
4 906GB 907GB 1075MB fat32 boot
(parted) quit
Voici l’état du premier disque après partitionnement :
Disk /dev/disk/by-id/nvme-WD_BLACK_SN850X_1000GB_24127V4A3114_1: 931.51 GiB, 1000204886016 bytes, 1953525168 sectors
Disk model: WD_BLACK SN850X 1000GB
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 314506F1-D2ED-4DEF-9B83-7951ACB95CCA
Device Start End Sectors Size Type
/dev/disk/by-id/nvme-WD_BLACK_SN850X_1000GB_24127V4A3114_1-part1 2048 1711290367 1711288320 816G Linux filesystem
/dev/disk/by-id/nvme-WD_BLACK_SN850X_1000GB_24127V4A3114_1-part2 1711290368 1767972863 56682496 27G Linux filesystem
/dev/disk/by-id/nvme-WD_BLACK_SN850X_1000GB_24127V4A3114_1-part3 1767972864 1770072063 2099200 1G Microsoft basic data
/dev/disk/by-id/nvme-WD_BLACK_SN850X_1000GB_24127V4A3114_1-part4 1770072064 1772171263 2099200 1G Microsoft basic data
Plus qu’à formatter avant de pouvoir monter les deux nouvelles partitions :
1mkfs.vfat -F 32 /dev/disk/by-id/nvme-WD_BLACK_SN850X_1000GB_24127V4A3114_1-part3
2mkfs.vfat -F 32 /dev/disk/by-id/nvme-WD_BLACK_SN850X_1000GB_24127V4A3114_1-part4
On vérifie que rien n’a été cassé par une typo ou sur une erreur de choix de disque ou copier/coller (et on en profite pour vérifier l’état de ses backups) puis on peut passer à la suite.
Configuration sous NixOS
Pour activer les mirrored boots, il est nécessaire de monter les partitions et d’indiquer à GRUB où les trouver.
Préparation du premier disque
1{
2
3 boot = {
4 # Enable EFI
5 loader.efi = {
6 canTouchEfiVariables = true;
7 efiSysMountPoint = "/boot1/efi";
8 };
9 loader.grub = {
10 efiSupport = true;
11 copyKernels = true;
12 device = "nodev"; # Disable non-EFI (legacy) Grub install
13 };
14 # Enable ZFS support
15 initrd.supportedFilesystems = ["zfs"];
16 supportedFilesystems = [ "zfs" ];
17 };
18
19 # Legacy drive
20 fileSystems."/boot" = {
21 device = "/dev/disk/by-uuid/3882-4280";
22 fsType = "vfat";
23 };
24 fileSystems."/boot/efi" =
25 {
26 device = "/dev/disk/by-uuid/333A-D038";
27 fsType = "vfat";
28 };
29
30 # NVMe no1 boot & ESP partitions
31 fileSystems."/boot1" = {
32 device = "/dev/disk/by-id/nvme-WD_BLACK_SN850X_1000GB_24127V4A3114_1-part4";
33 fsType = "vfat";
34 };
35 fileSystems."/boot1/efi" = {
36 device = "/dev/disk/by-id/nvme-WD_BLACK_SN850X_1000GB_24127V4A3114_1-part3";
37 fsType = "vfat";
38 };
39
40 boot.loader.grub.mirroredBoots = [
41 {
42 devices = [ "nodev" ]; # Specifies to NOT install GRUB, only generate files (for UEFI)
43 path = "/boot";
44 }
45 {
46 devices = [ "nodev" ]; # Specifies to NOT install GRUB, only generate files (for UEFI)
47 path = "/boot1";
48 }
49 ];
50}
Un petit nixos-rebuild boot pour vérifier que la configuration soit valide et que les partitions soient correctement
peuplées.
Préparation du second disque
Le redémarrage s’est bien passé. Au démarrage, on a pu confirmer que boot.loader.efi.efiSysMountPoint faisait bien
effet puisque le disque de démarrage a bel et bien changé, et le label pointe désormais sur « NixOS-boot1-efi ».
Impeccable, c’est parti pour préparer le second NVMe : mêmes commandes parted que plus haut, et on vérifie avec
fdisk -l que les partitions sont bien alignées à la fin pour que ce soit propre.
On n’oublie pas de formatter les nouvelles partitions avec mkfs.vfat -F 32 (et surtout, on relis bien plusieurs
fois avant de lancer une commande destructrice qui peut bloquer l’accès aux disques !).
Configuration du second disque
Plus qu’à ajouter les lignes suivantes dans notre configuration anciennement créée :
1{ lib, ... }:
2
3{
4 # ...
5
6 # NVMe no2 boot & ESP partitions
7 fileSystems."/boot2" = {
8 device = "/dev/disk/by-id/nvme-WDS100T1X0E-00AFY0_2140JY444704-part4";
9 fsType = "vfat";
10 };
11 fileSystems."/boot2/efi" = {
12 device = "/dev/disk/by-id/nvme-WDS100T1X0E-00AFY0_2140JY444704-part3";
13 fsType = "vfat";
14 };
15
16 # MANDATORY HERE: use `lib.mkForce` if you do not use `/boot` anymore, as NixOS adds a mirroredBoots entry to `/boot` by default
17 # See: https://github.com/NixOS/nixpkgs/blob/c5e1866b3d1decee15e982376131cca7103fbdfe/nixos/modules/system/boot/loader/grub/grub.nix#L717
18 boot.loader.grub.mirroredBoots = lib.mkForce [
19 # ...
20 {
21 devices = [ "nodev" ]; # Specifies to NOT install GRUB, only generate files (for UEFI)
22 path = "/boot2";
23 }
24 ];
On rebuild et on vérifie que tout fonctionne bien.
Erreurs de build et démontage des partitions
Sur une erreur de référence de configuration, en rebuildant, je me suis retrouvé dans une situation où /boot et
/boot1 se sont démontés. Ça bloquait évidemment le passage à la nouvelle configuration :
/nix/store/vmvflds3p010s8kx6fgm2yc7vfip0bmw-grub-2.12-rc1/sbin/grub-install: nvlist_lookup_string ("path"): Cannot allocate memory
/nix/store/43fgp3a80y82qwd4kc76i3xs4vbv64kn-install-grub.pl: installation of GRUB EFI into /boot failed: Inappropriate ioctl for device
Pour résoudre le problème, il suffit de monter manuellement les 6 partitions :
# mount /dev/disk/by-uuid/3882-4280 /boot
# mount /dev/disk/by-uuid/333A-D038 /boot/efi
# mount /dev/disk/by-id/nvme-WD_BLACK_SN850X_1000GB_24127V4A3114_1-part4 /boot1
# ...
Ça devrait permettre de débloquer nixos-rebuild switch !
Décomissionnement du NVMe en fin de vie
Plus qu’à retirer les entrées fileSystems."/boot" et fileSystems."/boot/efi" du système, ainsi que l’entrée
boot.loader.grub.mirroredBoots qui référençait /boot, pour ne garder que /boot1 et /boot2.
On applique, on reboot, et on vérifie que ça fonctionne bien !
Limite d’entrées EFI
Une des problématiques de la configuration en mirrored boots de NixOS étant son incapacité à définir plusieurs entrées de démarrage au sein de l’UEFI :
1 # …
2 loader.efi = {
3 canTouchEfiVariables = true;
4 efiSysMountPoint = "/boot1/efi";
5 };
Techniquement, si le disque qui héberge /boot1 meurt, il faudra spécifier manuellement le chemin hardware du disque
qui héberge /boot2 pour pouvoir démarrer.
Depuis le BIOS/l’UEFI, tout dépend du constructeur, mais ça peut se faire facilement avec une clé USB GRUB et quelques
commandes depuis la CLI GRUB (touche C une fois sur le menu GRUB) :
set root (hdX,gpt3)
chainloader /NixOS-boot2/grubx64.efi
Idéalement, je chercherai une solution pour mettre à jour les variables EFI pour du multiboot directement via NixOS.
Grub tente de démarrer sur la partition / au lieu de /boot1
J’ai réalisé que Grub tentait de démarrer sur ma partition racine au lieu de démarrer sur /boot1, alors même que
/boot1 contient le kernel et l’initramfs. Ça a causé quelques soucis étant donné que ma partition racine est sous
ZFS, et les scripts d’installation Grub n’ayant pas été capable de configurer automatiquement le démarrage sur la
partition ZFS (search infructueux puisqu’il cherche le label de la pool, puis démarrage sur un chemin relatif depuis
la partition recherchée).
La raison est simple : le script d’invocation de grub-install
récupère toutes les entrées mirroredBoots. Or, dans certains cas, NixOS ajoute un failsafe qui s’assure de la
présence d’au moins un périphérique mirroredBoots qui pointe sur… /boot.
De mon côté, /boot n’étant pas un point de montage, il considère la partition parente / (la partition racine ZFS)
et tente de configurer le démarrage dessus.
Note : Grub supporte absolument le démarrage sur ZFS, mais n’étant pas l’architecture recherchée ici, je n’ai pas cherché plus loin les raisons pour lesquelles le démarrage sur ZFS ne fonctionnait pas.
1871 Mots
2024-05-18 16:51