tuxvador@blog:~/blog/fr/proxmox-failing-nvme-bad-blocks$

~/blog/fr/proxmox-failing-nvme-bad-blocks

Mon SSD Proxmox est en train de mourir : secteurs défectueux, clones liés et volumes thin

6 min de lectureRead in English →

#proxmox#homelab#linux#storage#lxc

Ma vue d’ensemble du homelab listait « des sauvegardes avec un vrai test de restauration » parmi les choses à faire. Je m’y suis mis avec un petit conteneur : le sauvegarder, réduire son disque, le restaurer. La sauvegarde a échoué.

tar: ./usr/lib/x86_64-linux-gnu/libicudata.so.76.1: File shrank by 26752560 bytes; padding with zeros

Un fichier ne rétrécit pas pendant qu’on le lit depuis un snapshot figé. Le journal du noyau disait pourquoi :

nvme0n1: I/O Cmd(0x2) @ LBA 193218432, 128 blocks, I/O Error (sct 0x2 / sc 0x81)
critical medium error, dev nvme0n1, sector 193218432

Le code d’état 0x81 de type 0x2 est une erreur de lecture non récupérée : le disque a essayé et n’a pas pu rendre les données. C’est l’unique SSD de l’hôte Proxmox. Tous les conteneurs et le système de l’hôte vivent dessus.

SMART disait PASSED

smartctl -a /dev/nvme0
Attribut Valeur
Évaluation globale PASSED
Critical warning 0x00
Réserve disponible 100 %
Usure 26 %
Heures de fonctionnement 40 679
Arrêts non propres 988
Media and data integrity errors 2 860

Le résumé rassure, la dernière ligne non. Ce compteur cumule les erreurs non récupérées sur toute la vie du disque, et il augmentait à chaque fois que quelque chose lisait la zone défectueuse. Pour avoir une vraie réponse, il faut que le disque lise tout :

smartctl -t long /dev/nvme0        # ~25 minutes on this drive
smartctl -l selftest /dev/nvme0
Extended  Completed: failed segments  40679  193218544  seg 7  0x2 0x81

Le scan complet du disque lui-même s’est arrêté au même endroit que le noyau. J’ai réinséré le SSD au cas où ce serait un problème de connexion ; le lien PCIe est revenu à pleine vitesse et l’autotest a échoué à l’adresse identique. C’est de la mémoire flash abîmée, pas un disque mal enfiché. Le SSD doit être remplacé. La suite de ce billet, c’est ce qu’on fait en attendant.

Quels fichiers sont sur les secteurs défectueux ?

Le noyau donne un secteur, pas un fichier. La façon pragmatique de trouver les fichiers est de tous les lire :

# inside the container, or on its mounted volume
find / -xdev -type f -print0 | xargs -0 cat > /dev/null 2> unreadable.txt

Chaque Input/output error désigne une victime. Dans le premier conteneur, il y en avait une : libicudata.so.76.1, le fichier de données ICU du paquet Debian libicu76.

Pour un fichier qui vient d’un paquet, la correction consiste à faire réécrire le fichier par le paquet :

apt-get install --reinstall libicu76
dpkg --verify libicu76      # silence = every file matches its checksum

La nouvelle copie atterrit sur des blocs neufs, le fichier se lit, et la sauvegarde passe.

Les clones liés partagent leurs erreurs

J’ai ensuite lancé le même test de lecture sur chaque conteneur avant la première sauvegarde planifiée, et le même fichier était illisible dans trois autres conteneurs et dans le template dont ils étaient clonés.

C’est ainsi que fonctionnent les clones liés sur LVM-thin. Un clone est un snapshot thin du volume du template : tant qu’un conteneur ne réécrit pas un bloc, il lit la copie du template. Un secteur défectueux dans le template est un secteur défectueux dans chaque clone qui n’a jamais touché ce fichier. Le fichier de données d’ICU est rarement réécrit, donc tous pointaient encore vers l’original.

Les conteneurs étaient faciles : le même --reinstall dans chacun. Le template était plus difficile, parce que le volume de base d’un template Proxmox est en lecture seule et que pct refuse de le démarrer. Le réparer veut dire rendre le LV inscriptible pendant quelques minutes :

lvchange -ay -K pve/base-2000-disk-0      # activate it (-K: it is normally skipped)
lvchange -p rw pve/base-2000-disk-0
mount /dev/pve/base-2000-disk-0 /mnt/tpl
cp good-copy/libicudata.so.76.1 /mnt/tpl/usr/lib/x86_64-linux-gnu/libicudata.so.76.1.new
chown 100000:100000 /mnt/tpl/usr/lib/x86_64-linux-gnu/libicudata.so.76.1.new   # unprivileged uid mapping
chmod 644 /mnt/tpl/usr/lib/x86_64-linux-gnu/libicudata.so.76.1.new
mv /mnt/tpl/usr/lib/x86_64-linux-gnu/libicudata.so.76.1{.new,}   # rename: fresh blocks, atomic swap
md5sum /mnt/tpl/usr/lib/x86_64-linux-gnu/libicudata.so.76.1    # compare with the package's md5sums
umount /mnt/tpl
lvchange -p r pve/base-2000-disk-0
e2fsck -fn /dev/pve/base-2000-disk-0
lvchange -an pve/base-2000-disk-0

La bonne copie venait d’un conteneur qui avait déjà réinstallé le paquet, vérifiée contre le md5 que dpkg conserve pour elle. Les clones n’ont pas été touchés, parce que ce sont des snapshots indépendants qui avaient chacun réécrit leur propre copie entre-temps.

Une seconde zone défectueuse est apparue dans le conteneur de supervision : un téléchargement oublié, illisible à mi-chemin. Je l’ai supprimé. Aucune troisième zone n’est apparue.

Puis ext4 est retombé sur le secteur défectueux

L’après-midi suivant, l’hôte a planté. À son retour, le journal du conteneur de supervision était plein de ceci :

EXT4-fs warning: failed to convert unwritten extents to written extents -- potential data loss!
nvme0n1: I/O Cmd(0x2) @ LBA 193218432 ... I/O Error

Même adresse. La réinstallation avait libéré les blocs défectueux dans le système de fichiers du conteneur, et ext4 les avait réutilisés pour quelque chose de neuf : le fichier de journal de systemd. Les écritures dans la zone abîmée ont échoué, et ce fichier de journal a été perdu.

L’étape manquante était un trim. Sur un volume thin, les blocs qu’un système de fichiers libère restent alloués à ce volume jusqu’à ce qu’il soit trimé. Proxmox ne trime pas les conteneurs automatiquement, donc les blocs libérés étaient toujours là, associés à la même zone physique, prêts à être réutilisés.

pct fstrim 101 --ignore-mountpoints     # and every linked clone of the same template

--ignore-mountpoints l’empêche de toucher aux montages bind (dans mon cas, un disque USB en NTFS). Trimer les quatre conteneurs qui avaient détenu une copie a rendu 71 Go au pool thin, qui est passé de 56 % à 45 % d’occupation. Un effet secondaire agréable d’une étape que j’aurais dû faire de toute façon.

La procédure est désormais : après avoir réécrit un fichier qui se trouvait sur un secteur défectueux, trimer ce conteneur et chaque clone lié de son template.

La tâche de sauvegarde qui en est sortie

Tout cela a été découvert par une sauvegarde, les sauvegardes sont donc devenues la priorité :

  • Un stockage dédié sur le disque USB, avec is_mountpoint activé. Si le disque n’est pas monté, le stockage passe hors ligne et la tâche échoue, au lieu d’écrire discrètement 20 Go dans le système de fichiers racine du SSD mourant.
  • Une tâche hebdomadaire, le dimanche à 2 h, tous les conteneurs et le template, en mode snapshot (les conteneurs continuent de tourner), zstd.
  • Rétention keep-last=3,keep-weekly=4.

Cette ligne de rétention a une histoire. J’ai d’abord mis keep-weekly=4 seul, qui garde la sauvegarde la plus récente par semaine. Ma toute première sauvegarde manuelle de test a donc supprimé la sauvegarde précédente de la même semaine, qui était la copie « avant » d’un conteneur que je venais de redimensionner. Ajoutez keep-last pour que les exécutions manuelles n’évincent pas les planifiées.

Le disque USB est le goulot d’étranglement, à environ 15 Mo/s soutenus une fois le cache de pages plein. Un client torrent qui écrivait sur le même disque a fait ramper la sauvegarde d’un conteneur. Pour l’exécution manuelle, j’ai d’abord arrêté le client ; un script hook vzdump qui le met en pause automatiquement est sur la liste. Et vzdump --stop ne range pas derrière lui : il a laissé un conteneur verrouillé en snapshot-delete avec un volume de snapshot résiduel, nettoyés avec :

pct unlock 104 && pct delsnapshot 104 vzdump

Une exécution manuelle complète a pris un peu plus d’une heure, surtout pour les deux plus gros conteneurs.

La suite

Le SSD de remplacement sera le test de restauration à l’origine de tout ça : chaque conteneur restauré depuis ces sauvegardes sur du matériel neuf. D’ici là, les compteurs SMART du disque partent vers Prometheus, via le même paquet de métriques que j’ai installé pour les mises à jour, et rien n’est cloné depuis le template sans test de lecture préalable.

Pièges

  • SMART « PASSED » n’est pas un bilan de santé. Lisez le compteur d’erreurs média et lancez l’autotest étendu.
  • Une sauvegarde est un test de lecture complet que vous alliez faire de toute façon. Un « File shrank » de tar sur un snapshot signifie une erreur de lecture ; regardez journalctl -k.
  • Les clones liés héritent des secteurs défectueux du template. Testez le template et chaque clone, pas seulement le conteneur qui a échoué.
  • Le volume de base d’un template est en lecture seule. lvchange -ay -K, puis -p rw, et remettez-le avec -p r.
  • Réécrire un fichier ne retire pas le secteur défectueux. Trimez, sinon le système de fichiers le réutilisera.
  • keep-weekly seul supprime vos sauvegardes manuelles. Associez-le à keep-last.
  • Activez is_mountpoint sur un stockage de sauvegarde USB, sinon un disque débranché remplit la racine de l’hôte.
  • Une tâche de sauvegarde sans notification est un espoir. La mienne ne rapporte encore qu’au journal des tâches ; la brancher sur le canal push est la prochaine étape.

cd ~/blog