Deux SSD en miroir sous NixOS

nixosbtrfsuefi

Deux SSD, tout en miroir, et une machine qui continue de démarrer quand un disque meurt. C’est l’objectif, et il est à moitié atteignable.

Je suis parti d’un guide écrit par un modèle de langage, près de deux mille cinq cents lignes, mieux rangé que la plupart des tutoriels humains. Je l’ai mesuré couche par couche, puis installé pour de bon dans une machine virtuelle. Sa disposition UEFI ne démarre sur aucune machine, et aucune de ses vérifications ne le dit.

Autant annoncer tout de suite la moitié qui ne marche pas : une machine à qui il manque un disque ne redémarre pas seule, et aucune option d’une ligne n’y change rien. Je l’ai cru, je l’avais écrit, l’installation réelle m’a détrompé.

Le piège : ne pas mettre l’ESP en RAID sous UEFI

C’est la décision structurante, et elle se prend avant la première commande.

L’idée séduisante consiste à réunir les deux partitions EFI dans un RAID 1 logiciel. Le noyau les voit comme un volume unique, les écritures sont dupliquées, et si un disque lâche l’autre contient déjà tout. Plus de /boot à synchroniser à la main.

Le problème est un problème d’ordre. Quand une machine UEFI démarre, le micrologiciel doit lire la partition EFI avant qu’aucun noyau n’existe. Il n’a pas de pilote md. Il ouvre la partition, lit son premier secteur, et s’attend à y trouver l’en-tête d’un système de fichiers FAT.

Or mdadm écrit ses propres métadonnées sur chaque membre de la grappe, et mdadm --create sans --metadata choisit la version 1.2, celle qui place son superbloc à 4 kio du début. Deux grappes identiques, une seule variable :

métadonnées 1.2 (le défaut)métadonnées 1.0
Premiers octets du membre00 00 00 00eb 58 90 6d
Octet 510, signature d’amorçage00 0055 aa
En-tête FAT32 trouvé ànulle partoctet 0
Superbloc mdoctet 4096, depuis le début8192 octets avant la fin
mdadm --examineData Offset : 2048 sectorsaucun décalage

eb 58 90 est l’instruction de saut qui ouvre un secteur d’amorçage FAT32, 55 aa sa signature de fermeture. En 1.2 il n’y a ni l’une ni l’autre : le superbloc md occupe la place, et les données commencent un mébioctet plus loin que là où le micrologiciel regarde.

Démarré sous EDK2, avec un vrai chargeur d’amorçage au chemin de repli \EFI\BOOT\BOOTX64.EFI, le résultat est sans ambiguïté :

BdsDxe: failed to load Boot0001 "UEFI Misc Device" from PciRoot(0x0)/Pci(0x1,0x0): Not Found
BdsDxe: failed to load Boot0002 "UEFI Misc Device 2" from PciRoot(0x0)/Pci(0x2,0x0): Not Found
BdsDxe: loading Boot0003 "EFI Internal Shell"

La table de correspondance de ce shell tranche la question : elle liste BLK0: à BLK5:, donc les disques et leurs partitions GPT, ESP comprise. Il n’y a aucune entrée FS0:. Le micrologiciel a parfaitement vu les partitions. Il n’a trouvé de système de fichiers sur aucune.

Passer en --metadata 1.0 déplace le superbloc à la fin et rend la lecture possible, je l’ai vérifié. Je ne le recommande pas : le micrologiciel peut alors écrire dans l’ESP sans passer par md, pour ses variables d’amorçage ou une mise à jour, et désynchroniser le miroir en silence. Raisonnement, pas mesure, mais il suffit à préférer autre chose.

La bonne réponse sous NixOS s’appelle mirroredBoots. Deux ESP indépendantes, chacune avec son propre FAT32, et NixOS écrit les deux à chaque nixos-rebuild boot. Le micrologiciel n’a plus rien à comprendre : il lit une partition FAT ordinaire, sur le disque qui lui reste.

Chaque ESP est autonome : NixOS y recopie le chargeur et les noyaux, une fois par entrée mirroredBoots. Sur la machine installée, les deux en portaient deux chacune.

Mesuré sous EDK2, NVRAM vierge :

Disques attachésLe micrologiciel trouve et exécute le chargeur
les deuxoui
disque A seuloui
disque B seuloui

C’est l’étape exacte où la disposition du guide d’origine échoue.

Le mode BIOS, lui, est un autre monde, et le RAID sur /boot y fonctionne vraiment. Le micrologiciel y charge le core.img de GRUB depuis une petite partition dédiée, en secteurs bruts, sans aucun système de fichiers. Ce core.img embarque les modules mdraid1x et fat : GRUB assemble la grappe lui-même et lit /boot dessus. Démarré sous SeaBIOS, avec les métadonnées 1.2, la trace série contient les marqueurs placés dans un grub.cfg qui n’existe que sur la grappe :

MARKER-GRUB-READ-GRUB-CFG-FROM-MD-ARRAY
MARKER-ROOT-IS-md/0

GRUB a résolu sa racine en md/0 tout seul, et avec chaque disque seul également. L’erreur du guide d’origine n’a donc rien à voir avec mdadm. Elle porte sur la couche qui fait la lecture.

Partitionner

Les deux disques vont être effacés. Repérez-les par identifiant stable, pas par /dev/sda : l’ordre d’énumération peut changer d’un démarrage à l’autre, et ces noms vont finir écrits dans votre configuration.

ls -l /dev/disk/by-id/ | grep -v part
DISK_A=/dev/disk/by-id/nvme-SAMSUNG_XXXX_YYYY
DISK_B=/dev/disk/by-id/nvme-SAMSUNG_XXXX_ZZZZ

Le nettoyage demande deux passes, et l’ordre n’est pas négociable :

mdadm --stop --scan 2>/dev/null || true

# les superblocs md d'une ancienne installation vivent DANS les partitions
for p in "$DISK_A"-part* "$DISK_B"-part*; do
  [ -e "$p" ] || continue
  mdadm --zero-superblock "$p" 2>/dev/null || true
  wipefs --all --force "$p"
done

wipefs --all --force "$DISK_A"
wipefs --all --force "$DISK_B"

J’avais d’abord condensé cela en un wipefs par disque, en me disant que détruire la table de partitions suffisait. C’est faux : un superbloc md vit à l’intérieur de la région de l’ancienne partition, là où un effacement du disque entier ne va pas le chercher.

après un wipefs du disque entier seulement :
   mdadm --examine trouve un superbloc : oui
après un wipefs des partitions, puis du disque :
   mdadm --examine trouve un superbloc : non

Le guide d’origine effaçait bien les superblocs partition par partition. Sur ce point il avait raison et j’avais tort de simplifier.

En revanche il proposait aussi d’écraser la fin de chaque disque au dd, pour la table GPT de secours, et cette étape-là est réellement inutile : wipefs efface déjà les trois signatures.

/dev/loop0: 8 bytes were erased at offset 0x00000200 (gpt)
/dev/loop0: 8 bytes were erased at offset 0x3ffffe00 (gpt)
/dev/loop0: 2 bytes were erased at offset 0x000001fe (PMBR)

La deuxième ligne est l’en-tête de secours, en fin de disque. Sans wipefs, le dd de début seul laisse cette copie intacte et sgdisk régénère l’en-tête principal à partir d’elle.

En UEFI, une ESP par disque et le reste pour les données :

for d in "$DISK_A" "$DISK_B"; do
  parted -s "$d" mklabel gpt
  parted -s "$d" mkpart ESP fat32 1MiB 512MiB
  parted -s "$d" set 1 esp on
  parted -s "$d" mkpart primary 512MiB 100%
done

mkfs.fat -F 32 -n ESP-A "$DISK_A-part1"
mkfs.fat -F 32 -n ESP-B "$DISK_B-part1"

DATA_A="$DISK_A-part2"
DATA_B="$DISK_B-part2"

Les deux ESP reçoivent deux étiquettes distinctes, et c’est volontaire : ce sont deux systèmes de fichiers séparés, que NixOS doit pouvoir monter séparément.

En BIOS, trois partitions, dont une de deux mébioctets que GRUB utilise en interne et qui n’est ni formatée ni montée :

for d in "$DISK_A" "$DISK_B"; do
  parted -s "$d" mklabel gpt
  parted -s "$d" mkpart primary 1MiB 3MiB
  parted -s "$d" set 1 bios_grub on
  parted -s "$d" mkpart primary 3MiB 515MiB
  parted -s "$d" mkpart primary 515MiB 100%
done

mdadm --create /dev/md0 --level=1 --raid-devices=2 --name=boot \
  "$DISK_A-part2" "$DISK_B-part2"
mkfs.fat -F 32 -n BOOT /dev/md0

DATA_A="$DISK_A-part3"
DATA_B="$DISK_B-part3"

btrfs RAID 1 et les sous-volumes

Identique dans les deux modes, sauf que la partition de données change de numéro : la deuxième en UEFI, la troisième en BIOS. D’où les DATA_A et DATA_B des blocs précédents. L’inattention coûte cher ici : en BIOS, part2 est un membre du RAID de /boot, et un mkfs.btrfs dessus détruirait l’amorçage.

mkfs.btrfs -L nixos -d raid1 -m raid1 "$DATA_A" "$DATA_B"

mount /dev/disk/by-label/nixos /mnt
btrfs subvolume create /mnt/@
btrfs subvolume create /mnt/@home
btrfs subvolume create /mnt/@snapshots
umount /mnt

-d raid1 -m raid1 met données et métadonnées en miroir. Les sous-volumes séparent ce qui se sauvegarde de ce qui se remplace : @ la racine, @home les comptes, @snapshots les instantanés.

Monter

mount -o subvol=@,compress=zstd,noatime /dev/disk/by-label/nixos /mnt
mkdir -p /mnt/{home,.snapshots}
mount -o subvol=@home,compress=zstd,noatime /dev/disk/by-label/nixos /mnt/home
mount -o subvol=@snapshots,compress=zstd,noatime /dev/disk/by-label/nixos /mnt/.snapshots

Puis les partitions d’amorçage, selon le mode. En UEFI, les deux ESP :

mkdir -p /mnt/boot /mnt/boot-fallback
mount -o fmask=0077,dmask=0077 /dev/disk/by-label/ESP-A /mnt/boot
mount -o fmask=0077,dmask=0077 /dev/disk/by-label/ESP-B /mnt/boot-fallback

En BIOS, la grappe :

mkdir -p /mnt/boot
mount -o fmask=0077,dmask=0077 /dev/md0 /mnt/boot

fmask=0077 et dmask=0077 réservent la lecture à root. FAT32 ne porte pas de permissions Unix, ces masques sont donc le seul levier, et la partition contient le noyau et l’initrd. NixOS y fait aussi pointer la graine aléatoire de systemd, quel que soit le chargeur.

La configuration, sans écraser le matériel détecté

nixos-generate-config --root /mnt

Deux fichiers, et le plus gros piège du guide d’origine.

hardware-configuration.nix ne contient pas que du matériel : il contient votre disposition de systèmes de fichiers, relevée sur ce qui est monté à cet instant. D’où l’ordre des étapes précédentes. Voici ce qu’il a écrit lors de mon installation :

  boot.initrd.availableKernelModules = [ "virtio_pci" "virtio_scsi" "sr_mod" ];
  fileSystems."/" =
    { device = "/dev/disk/by-uuid/05a364d3-6496-4908-9a0d-6f0e3dc84948";
      fsType = "btrfs";
      options = [ "subvol=@" ];
    };
  fileSystems."/boot" =
    { device = "/dev/disk/by-uuid/C3C1-28D9";
      fsType = "vfat";
      options = [ "fmask=0077" "dmask=0077" ];
    };

Il désigne les volumes par UUID : les étiquettes n’étaient qu’une commodité pour taper les montages. Et il a gardé subvol= et les masques FAT, mais laissé tomber compress=zstd et noatime.

J’ai installé une machine ainsi, sans rien ajouter. findmnt, sur le système démarré :

/              rw,relatime,discard=async,space_cache=v2,subvolid=256,subvol=/@
/home          rw,relatime,discard=async,space_cache=v2,subvolid=257,subvol=/@home
/boot          rw,relatime,fmask=0077,dmask=0077,codepage=437,...

Pas de compression, relatime au lieu de noatime. Les masques FAT ont survécu. Qui s’arrête là installe un système sans compression, et rien ne le lui dira.

Le guide d’origine remplaçait configuration.nix par un cat > ..., écrasant la ligne imports = [ ./hardware-configuration.nix ] écrite quelques secondes plus tôt. L’extrait ci-dessus montre le prix : le profil de virtualisation et les modules virtio_* de l’initrd, et avec eux la racine, introuvable dans une machine virtuelle. Sur ses configurations j’ai mesuré la même perte en trois endroits : initrd réduit à ahci nvme sd_mod md_mod raid1, microcode plus mis à jour, micrologiciels redistribuables plus installés.

Garder l’import et redéclarer les fileSystems ne marche pas davantage. Je m’y suis cassé le nez à ma première tentative :

error: The option `fileSystems."/".device' has conflicting definition values:
- In `/mnt/etc/nixos/configuration.nix': "/dev/disk/by-label/nixos"
- In `/mnt/etc/nixos/hardware-configuration.nix': "/dev/disk/by-uuid/05a364d3-..."

Le guide affirme que « la version de configuration.nix prendra le dessus ». Elle entre en conflit, et l’évaluation s’arrête net.

La sortie tient à une différence de type entre deux options voisines. device est une chaîne : deux définitions se contredisent. options est une liste : deux définitions fusionnent. On ajoute donc ce qui manque, sans jamais redéclarer un périphérique :

  fileSystems."/".options = [ "compress=zstd" "noatime" ];
  fileSystems."/home".options = [ "compress=zstd" "noatime" ];
  fileSystems."/.snapshots".options = [ "compress=zstd" "noatime" ];

Ce qui donne, à l’évaluation, la liste complète, le périphérique restant celui qu’a écrit la détection :

[ "x-initrd.mount" "compress=zstd" "noatime" "subvol=@" ]

configuration.nix ne redéclare donc aucun système de fichiers. Le voici en entier.

En UEFI :

{ config, pkgs, lib, ... }:
{
  imports = [ ./hardware-configuration.nix ];

  boot.loader.grub = {
    enable = true;
    efiSupport = true;
    efiInstallAsRemovable = true;
    mirroredBoots = [
      { path = "/boot"; devices = [ "/dev/disk/by-id/nvme-SAMSUNG_XXXX_YYYY" ]; }
      { path = "/boot-fallback"; devices = [ "/dev/disk/by-id/nvme-SAMSUNG_XXXX_ZZZZ" ]; }
    ];
  };
  boot.loader.efi.canTouchEfiVariables = false;

  boot.supportedFilesystems = [ "btrfs" ];
  boot.initrd.supportedFilesystems = [ "btrfs" ];

  fileSystems."/".options = [ "compress=zstd" "noatime" ];
  fileSystems."/home".options = [ "compress=zstd" "noatime" ];
  fileSystems."/.snapshots".options = [ "compress=zstd" "noatime" ];

  nix.package = pkgs.lix;

  services.btrfs.autoScrub = {
    enable = true;
    interval = "weekly";
    fileSystems = [ "/" ];
  };
  services.smartd = {
    enable = true;
    notifications.wall.enable = true;
  };

  networking.hostName = "nixos";
  networking.networkmanager.enable = true;
  time.timeZone = "Europe/Paris";
  i18n.defaultLocale = "fr_FR.UTF-8";

  users.users.someone = {
    isNormalUser = true;
    extraGroups = [ "wheel" "networkmanager" ];
  };

  environment.systemPackages = with pkgs; [
    vim git btrfs-progs compsize mdadm smartmontools
  ];

  services.openssh.enable = true;

  system.stateVersion = "26.05";
}

Trois lignes méritent un mot.

efiInstallAsRemovable = true fait écrire le chargeur au chemin de repli que le micrologiciel cherche quand aucune entrée NVRAM ne le guide. C’est ce drapeau qui le décide, pas GRUB par défaut. Deux installations sur une ESP neuve, le drapeau pour seule différence :

avec --removable :   <ESP>/EFI/BOOT/BOOTAA64.EFI
sans --removable :   <ESP>/EFI/NixOS/grubaa64.efi

Une entrée NVRAM désigne une partition précise par son identifiant et meurt avec le disque qui la porte, donc le second chemin n’est atteignable que par elle. Le chemin de repli se trouve tout seul sur n’importe quel disque restant, ce que le micrologiciel a fait dans le test à un disque.

En contrepartie, nixpkgs l’interdit avec les variables EFI, explicitement :

If you wish to to use boot.loader.grub.efiInstallAsRemovable,
then turn off boot.loader.efi.canTouchEfiVariables

nix.package = pkgs.lix installe Lix, en 2.95.4 au moment où j’écris. Le guide le promettait dans son nom de fichier et ne l’utilisait nulle part.

system.stateVersion porte la version depuis laquelle vous installez et ne change jamais ensuite. Ne reprenez pas la mienne sans vérifier la vôtre.

En BIOS, seul le bloc d’amorçage change, et /boot-fallback disparaît :

  boot.loader.grub = {
    enable = true;
    devices = [
      "/dev/disk/by-id/nvme-SAMSUNG_XXXX_YYYY"
      "/dev/disk/by-id/nvme-SAMSUNG_XXXX_ZZZZ"
    ];
  };

  boot.swraid = {
    enable = true;
    mdadmConf = ''
      ARRAY /dev/md0 metadata=1.2 name=boot
      MAILADDR root
    '';
  };

  fileSystems."/boot" = {
    device = "/dev/md0";
    fsType = "vfat";
    options = [ "fmask=0077" "dmask=0077" ];
  };

La ligne MAILADDR n’est pas décorative. Sans elle, le module mdadm prévient :

mdadm: Neither MAILADDR nor PROGRAM has been set. This will cause the `mdmon` service to crash.

Avec elle, les deux configurations ci-dessus s’évaluent sans un seul avertissement.

Si vous ajoutez GNOME, attention aux options renommées :

  services.xserver.enable = true;
  services.displayManager.gdm.enable = true;
  services.desktopManager.gnome.enable = true;
  environment.systemPackages = with pkgs; [ gnome-tweaks gnome-terminal ];

Les anciens chemins sous services.xserver. fonctionnent encore mais préviennent, et les paquets ne sont plus sous gnome., le préfixe ayant été aplati.

Installer

nixos-install

Puis, avant de redémarrer :

cat /proc/mdstat                      # en BIOS uniquement
ls -la /mnt/boot/EFI/BOOT/            # en UEFI, sur les deux ESP
ls -la /mnt/boot-fallback/EFI/BOOT/

Ces vérifications sont utiles mais faibles, et c’est le point que je voudrais faire passer. grub-install écrit dans un système de fichiers monté, et Linux monte parfaitement ce qu’aucun micrologiciel ne lira, puisqu’il a le pilote md. Sur l’ESP en RAID de la première section :

exit code: 0
Installation finished. No error reported.

Les fichiers sont là, et ce sont de vrais fichiers. Rien dans le chemin d’installation n’est un micrologiciel, donc rien n’a de raison de se demander si un micrologiciel atteindrait ces octets. L’installateur vérifie que l’écriture a eu lieu. La question portait sur qui saura la relire.

La seule vérification qui compte est un démarrage avec un disque débranché. Faites-la le jour de l’installation, pendant qu’il n’y a rien à perdre.

Ce qui se passe vraiment quand un disque meurt

La redondance échoue à deux étages indépendants. Le premier, micrologiciel et chargeur, est réglé par ce qui précède. Le second est btrfs, et il est plus désagréable.

OpérationRésultat
mount ordinaire d’un raid1 à deux disques, un disque absentéchoue
mount -o degradedréussit, données lisibles

Un btrfs RAID 1 à deux disques refuse un montage ordinaire quand un disque manque. Les options de racine du guide, subvol=@,compress=zstd,noatime, ne portent pas degraded. Le mot apparaît trois fois dans ses pages, chaque fois comme un état à constater, jamais comme l’option qui permet de démarrer.

Et le secours est moins accessible qu’avant. L’initrd systemd est actif par défaut, et l’accès d’urgence y est fermé par défaut :

emergencyAccess
  Set to true for unauthenticated emergency access, and false or
  null for no emergency access.
  default = false

Vérifié sur la machine installée, démarrée avec un seul disque. Le micrologiciel a lancé le chargeur, l’étage d’amorçage a tenu. Puis :

Dependency failed for /sysroot.
Dependency failed for Initrd Root File System.
[  OK  ] Started Emergency Shell.
[  OK  ] Reached target Emergency Mode.
You are in emergency mode. After logging in, type "journalctl -xb" to view
Cannot open access to console, the root account is locked.

La dernière ligne est tout le problème. Le shell de secours existe, et il est hors d’atteinte.

degraded dans les options ne suffit pas

C’est ici que j’ai dû corriger ce que j’allais publier. Puisque les options fusionnent, ajouter degraded tient sur une ligne, et c’est ce que tout le monde propose :

  fileSystems."/".options = [ "degraded" ];

Je l’ai appliquée à la machine, rebâtie avec nixos-rebuild boot, puis redémarrée avec un seul disque. Elle n’a pas démarré. Voici pourquoi :

         Expecting device /dev/disk/by-uuid…64d3-6496-4908-9a0d-6f0e3dc84948...
[ TIME ] Timed out waiting for device /dev/…a364d3-6496-4908-9a0d-6f0e3dc84948.
[DEPEND] Dependency failed for /sysroot.

Les points de suspension sont ceux de systemd, qui raccourcit les noms d’unités trop longs. Le montage n’est jamais tenté. udev ne publie le lien by-uuid d’un btrfs que lorsque tous ses membres sont présents : systemd attend un nœud qui n’apparaîtra jamais, et l’option qu’il aurait utilisée ne joue aucun rôle. Elle est correcte et arrive trop tard.

J’ai essayé l’échappatoire évidente, désigner la racine par une partition plutôt que par l’UUID, avec le lib.mkForce que le message d’erreur suggérait lui-même. Elle échoue aussi, pour une raison indépendante : l’initrd ne crée pas les liens by-id.

         Expecting device /dev/disk/by-id/virtio-DISKA-part2...
[ TIME ] Timed out waiting for device /dev/disk/by-id/virtio-DISKA-part2.

Et ce disque-là était présent.

Aucune option d’une ligne ne fait redémarrer cette machine seule sur un disque. Le choix n’est pas entre « sans intervention » et « à la main », mais entre « à la main, préparé » et « à la main, enfermé dehors ».

Ce qui marche, et que j’ai mesuré

Une seule ligne compte vraiment, et c’est celle qui vous laisse entrer :

  boot.initrd.systemd.emergencyAccess = "$y$j9T$...";

Un mot de passe haché, ou true pour un shell sans authentification si la machine est physiquement hors d’atteinte. Le même démarrage à un disque donne alors un shell, et la récupération tient en deux commandes :

# btrfs device scan
Scanning for Btrfs filesystems
registered: /dev/vda2

# mount -o degraded,subvol=@ /dev/vda2 /m
RC=0

# ls /m
bin   boot  boot-fallback  dev  etc  home  lib  nix
proc  root  run  srv  sys   tmp  usr  var

# btrfs filesystem show /m
Label: 'nixos'  uuid: 05a364d3-6496-4908-9a0d-6f0e3dc84948
    Total devices 2 FS bytes used 1.24GiB
    devid    1 size 15.50GiB used 3.26GiB path /dev/vda2
    devid    2 size 0 used 0 path  MISSING

Le volume se monte, les données sont là, btrfs nomme le disque absent.

Donc : « la machine démarre même si un disque meurt » est faux sur cette disposition. L’amorçage survit, les données survivent, la remise en route demande un clavier. C’est déjà beaucoup, et très différent de ce qu’annonce le guide.

Une légende à ne pas répéter, et j’étais sur le point de la reprendre : monter un btrfs RAID 1 dégradé en écriture ne condamne pas le volume. Les groupes single apparaissent, mais le second montage dégradé réussit toujours sur un noyau 7.1.3. Les noyaux actuels évaluent la tolérance groupe de blocs par groupe de blocs.

Remplacer le disque

Partitionnez le disque neuf à l’identique, redéfinissez DATA_B selon votre mode, puis traitez les deux étages séparément.

L’amorçage, en UEFI : formatez la nouvelle ESP avec la bonne étiquette, montez-la, laissez NixOS la peupler.

mkfs.fat -F 32 -n ESP-B /dev/disk/by-id/<neuf>-part1
mount -o fmask=0077,dmask=0077 /dev/disk/by-label/ESP-B /boot-fallback
nixos-rebuild boot

En BIOS, c’est mdadm qui reconstruit :

mdadm --manage /dev/md0 --add /dev/disk/by-id/<neuf>-part2
watch cat /proc/mdstat
nixos-rebuild boot

Les données ensuite. Le guide proposait btrfs device remove puis device add. La première commande ne peut pas réussir sur une grappe à deux disques, dans aucun des deux états :

CommandeRésultat
device remove, les deux disques vivantsERROR: unable to go below two devices on raid1
device remove, contre le disque mortERROR: Invalid argument
btrfs replace startréussit, données intactes

La commande qui existe s’appelle btrfs replace, et le guide ne la mentionne jamais.

btrfs filesystem show /            # relever le devid du disque manquant
btrfs replace start <devid> "$DATA_B" /
btrfs replace status /

Le devid, pas le chemin : la documentation précise que ce format devient nécessaire dès que le disque source est déconnecté, ce qui est exactement le cas d’un disque mort. Le disque cible doit être de taille égale ou supérieure.

Son balance final, lui, est nécessaire, simplement pas là où il le mettait : les groupes single survivent à un remplacement.

profils juste après le replace :
   Data, single: total=963.81MiB, used=250.78MiB
   Data, RAID1:  total=1.52GiB,   used=549.22MiB

après balance -dconvert=raid1 -mconvert=raid1 :
   Data, RAID1:     total=980.00MiB, used=800.00MiB
   Metadata, RAID1: total=256.00MiB, used=1.16MiB
   System, RAID1:   total=32.00MiB,  used=16.00KiB

Donc, pour finir :

btrfs balance start -dconvert=raid1 -mconvert=raid1 /
btrfs filesystem usage /

Bon réflexe, mauvaise séquence.

Surveiller

Trois choses suffisent, déjà dans la configuration plus haut. Le autoScrub hebdomadaire vérifie les sommes de contrôle et répare depuis la copie saine. smartd surveille l’état des disques. En BIOS, MAILADDR fait exister le service qui prévient quand la grappe se dégrade.

En UEFI il n’y a pas d’équivalent, faute de grappe md : un disque btrfs manquant ne se signale que par btrfs device stats ou par le scrub. La surveillance compte donc plus ici qu’ailleurs, puisque la perte d’un disque reste invisible jusqu’au prochain démarrage.

Aucun de ces outils ne vous dira si la machine démarrerait. Mettez-vous un rappel pour débrancher un disque une fois par an.

Ce que j’ai vérifié, et comment

Sur un Mac sans constructeur Linux, en quatre paliers de coût croissant, plus la lecture des sources de nixpkgs là où l’affirmation porte sur ce que NixOS fait et non sur ce que le matériel répond.

Le quatrième palier est une installation réelle : ISO officielle NixOS 26.05 pour aarch64, machine QEMU avec deux disques vierges et firmware EDK2, ce guide déroulé commande par commande jusqu’à nixos-install, puis des redémarrages en débranchant des disques. C’est lui qui a trouvé les trois corrections les plus importantes de cet article, dont deux portaient sur ce que j’avais écrit. La méthode a son propre article.

AffirmationÉtablie par
Position du superbloc selon la version de métadonnéesmdadm réel, dans un conteneur privilégié
Un micrologiciel UEFI ne lit pas une ESP en RAID 1.2QEMU et EDK2, sans système d’exploitation
Le micrologiciel exécute le chargeur depuis chaque disque seul, deux ESP indépendantesQEMU et EDK2, un disque attaché à la fois
GRUB lit /boot sur une grappe md en mode BIOSQEMU et SeaBIOS, marqueurs dans grub.cfg
grub-install réussit sur une ESP illisiblegrub-install réel, code de retour et fichiers écrits
Seul --removable écrit au chemin de repligrub-install réel, deux installations comparées
Un btrfs RAID 1 dégradé exige -o degradedbtrfs réel, noyau 7.1.3
device remove échoue, replace réussitidem
wipefs efface la GPT de secoursidem, sur fichier en loop
Un wipefs du disque seul laisse un superbloc mdidem, ancienne installation reconstituée
Les configurations ci-dessus s’évaluent sans avertissementévaluation Nix, nixpkgs 26.11pre
Les options GNOME ont été renomméesidem, avertissements de renommage capturés
Lix 2.95.4 s’installe par nix.packageidem
La configuration publiée s’évalue contre un hardware-configuration.nix réelévaluation Nix du bloc extrait de cet article
Le déroulé complet installe un NixOS qui démarreinstallation réelle, QEMU et EDK2, NVRAM vierge
Les deux ESP reçoivent chargeur et noyauxinstallation réelle, find sur chacune
nixos-generate-config perd compress=zstd et noatimeinstallation réelle, findmnt après démarrage
Redéclarer fileSystems entre en conflit avec le fichier généréinstallation réelle, l’évaluation s’arrête
degraded seul ne fait pas démarrer sur un disqueinstallation réelle, un seul disque attaché
L’accès d’urgence ouvert permet de monter et de lireinstallation réelle, shell de secours
Chaque mirroredBoots reçoit noyaux et initrdlecture de grub.nix et install-grub.pl
L’accès d’urgence de l’initrd est fermé par défautlecture de la définition de l’option
--removable est incompatible avec les variables EFIassertion nixpkgs, déclenchée volontairement

La limite honnête est l’architecture. L’installation réelle et les mesures grub-install ont tourné en aarch64-linux, seul natif sur cette machine, et le chargeur s’y nomme BOOTAA64.EFI au lieu de BOOTX64.EFI. Rien de ce qui est en jeu ici n’en dépend : mirroredBoots, la fusion des options de montage, l’attente d’un nœud by-uuid par systemd et le comportement de btrfs sont le même code des deux côtés. Le décalage du superbloc et le mode BIOS, eux, ont bien été mesurés sur x86.

Reste non testé l’exercice sur du matériel physique, avec un micrologiciel de carte mère plutôt qu’EDK2, et un disque qui meurt vraiment plutôt qu’un câble virtuel débranché.

Sources

Le comportement des micrologiciels décrit ici n’est pas cité : il est mesuré, sous EDK2 et SeaBIOS.


← Retour au blog