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 -v dans 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)

  1. /boot/efi et /boot1/efi
  2. /boot/efi, /boot1/efi et /boot2/efi
  3. /boot1/efi et /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.