« Remounting filesystem read-only » : le site tombe en 500 et le disque n'est pas plein

par Luca Cavallini Cas d'assistance
« Remounting filesystem read-only » : le site tombe en 500 et le disque n'est pas plein

Le symptôme : un 500 instantané, et SSH qui ne répond plus

La boutique, une instance PrestaShop sur un serveur privé virtuel, est tombée d'un coup : page blanche, erreur 500, et la première chose qui frappe est la vitesse de la réponse, 0,02 seconde. Un « vrai » 500 PHP, causé par une erreur applicative, prend en général quelques dixièmes de seconde, parce qu'il doit d'abord charger le framework. Une réponse aussi rapide est presque toujours le signe que l'erreur se produit bien plus tôt, à un niveau qui n'a rien à voir avec le code de la boutique.

Deuxième indice : la connexion SSH au conteneur expirait. Pas un « connection refused », un vrai délai dépassé, comme si quelque chose s'était bloqué en cours de route. Dans le conteneur, un simple touch test renvoyait :

touch: cannot touch 'test': Read-only file system

Le système de fichiers s'était remonté en lecture seule. Le nœud physique qui héberge le conteneur, vérifié immédiatement, allait bien : charge normale, aucune erreur matérielle signalée.

Ce que cela semblait être, et n'était pas

Un système de fichiers qui bascule en lecture seule oriente, dans l'ordre, vers trois pistes. Aucune des trois n'était la bonne, mais c'est en les écartant une par une qu'on est arrivé à la vraie cause.

  • « Le disque est plein. » La première hypothèse, et aussi la plus facile à vérifier : df -h dans le conteneur indiquait 186 Go utilisés sur 1,5 To disponibles, soit 14 %. Ce n'était pas ça. Un disque réellement à court d'espace donne une erreur différente et bien plus claire (« No space left on device »), pas un remontage en lecture seule.
  • « Le disque physique est mort. » Un remontage en lecture seule est aussi le comportement typique d'ext4 quand il rencontre des erreurs d'E/S venant d'un disque défaillant. Mais la vérification sur le nœud physique ne montrait aucune erreur matérielle, aucun secteur défectueux, aucune alerte sur la grappe. Le disque était sain.
  • « MySQL s'est corrompu. » L'une des tables de configuration est bien revenue marquée comme endommagée une fois le site relancé. Mais c'était une conséquence du passage en lecture seule en pleine écriture, pas la cause de l'incident : MySQL ne peut pas avoir cassé un système de fichiers qu'il ne gère pas.

Le journal du noyau du nœud physique, consulté après avoir écarté les deux premières hypothèses, racontait une histoire précise :

Buffer I/O error on device dm-4
EXT4-fs warning: I/O error 3 writing to inode ...
Aborting journal on device dm-4-8
EXT4-fs error (device dm-4):
ext4_journal_check_start: comm mysqld:
Detected aborted journal
EXT4-fs (dm-4): Remounting filesystem read-only

La vraie cause : c'est le pool sous le disque qui était plein, pas le disque

Le volume du conteneur n'est pas un disque physique dédié : c'est un volume LVM en thin provisioning, posé sur un pool partagé avec d'autres machines virtuelles du même nœud (le mécanisme est identique sur tout hyperviseur qui utilise un stockage LVM thin, Proxmox dans ce cas, mais il n'est pas propre à ce produit). Avec le thin provisioning, le volume « voit » sa taille déclarée, 1,5 To, mais les blocs physiques ne sont alloués depuis le pool qu'au moment où ils sont réellement nécessaires. Si le pool sous-jacent se remplit, les écritures échouent au niveau des blocs, même si le volume du conteneur affiche encore de l'espace libre vu de l'intérieur : les 14 % indiqués par df étaient parfaitement vrais, et parfaitement hors sujet.

La vérification du pool l'a confirmé : Data% à 100,00 %, avec l'attribut d'état qui affichait la lettre D, plus d'espace de données. Pool plein, écritures refusées au niveau des blocs, ext4 qui voit des erreurs d'E/S, interrompt son journal pour protéger la cohérence des données et se remonte en lecture seule. À partir de là, PHP et MySQL ne peuvent plus rien écrire, et la boutique répond 500 à chaque requête.

Le pool ne s'était pas rempli à cause d'un seul événement, mais de trois problèmes qui se sont additionnés au fil du temps :

  1. De vieux snapshots jamais nettoyés. Une tâche automatique de snapshots avait été désactivée par le passé, mais sans supprimer les snapshots déjà créés. Ils étaient là depuis des semaines, et retenaient des blocs qui auraient sinon été libérés.
  2. Le seuil d'extension automatique du pool était réglé à 100 %. Un pool qui ne s'étend automatiquement qu'une fois déjà plein ne s'étend en pratique jamais à temps : il n'y avait aucune marge pour réagir avant la saturation, et aucune alerte préalable.
  3. Aucun discard/TRIM périodique. Quand un fichier est supprimé dans le système de fichiers du conteneur, les blocs qu'il occupait restent alloués dans le pool jusqu'à ce qu'un TRIM les restitue. Le calcul, fait après l'incident, était sans appel : 1,40 To alloués dans le pool pour 186 Go réellement utilisés dans le conteneur, plus d'un téraoctet d'espace « fantôme » jamais rendu.

Le déclencheur final a été une sauvegarde de routine : son snapshot a été créé sur un pool déjà à la limite, et en quelques minutes le pool a complètement saturé.

Le correctif

L'intervention s'est faite dans cet ordre, du plus urgent au plus durable :

  1. Extension d'urgence du pool (non destructive, tant qu'il reste de la marge dans le volume group sous-jacent) : le pool redescend sous les 100 % et les écritures fonctionnent à nouveau.
  2. Suppression du snapshot de la sauvegarde échouée, sans risque puisque le système de sauvegarde le recrée de toute façon au passage suivant.
  3. Suppression des vieux snapshots inutilisés, avec l'accord explicite du client avant d'y toucher : à eux seuls, ils représentaient la plus grande partie de l'espace récupéré. Le volume actif du conteneur n'a jamais été touché, ce qui a été vérifié avant et après.
  4. e2fsck avec le conteneur arrêté, pour rejouer le journal et réparer les inodes laissés orphelins par l'écriture interrompue.
  5. Vérification de l'intégrité de la base de données : la réparation automatique de MySQL s'était déjà déclenchée d'elle-même sur la table signalée comme endommagée ; un contrôle explicite de toutes les tables à risque a confirmé que les données étaient intactes et identiques à celles d'avant l'incident.
  6. Abaissement du seuil d'extension automatique du pool, pour que la prochaine fois le système s'étende avec de la marge, au lieu d'attendre d'être déjà plein.

Récupérer l'espace « fantôme » avec un TRIM est un chantier à part : un conteneur non privilégié ne peut pas le lancer de l'intérieur, et avec le conteneur en marche, souvent pas non plus de l'extérieur. Il faut une courte fenêtre de maintenance avec le conteneur arrêté, consacrée uniquement à cela.

Comment vérifier que le correctif tient

  • Suivez le nombre d'erreurs 5xx dans le temps, et pas seulement le fait que le site répond à nouveau : dans ce cas, toutes les erreurs étaient concentrées dans la seule minute du redémarrage (démarrage du conteneur, MySQL en pleine réparation), puis plus aucune erreur sur des milliers de requêtes suivantes. Une erreur isolée au redémarrage est normale ; une erreur qui se répète ne l'est pas.
  • Surveillez le pourcentage de remplissage du pool dans le temps, pas seulement juste après le correctif : un pool qui recommence à se remplir au même rythme qu'avant signifie que les causes de fond (snapshots qui s'accumulent, réglage d'extension automatique trop permissif) ont été repoussées, pas réellement éliminées.
  • Faites un contrôle explicite des tables de la base de données après tout incident de ce type, même si le site semble déjà fonctionner : un remontage en lecture seule en pleine écriture peut laisser une table dans un état incohérent sans qu'aucune erreur n'apparaisse tout de suite sur la page.
  • Planifiez une fenêtre de maintenance pour le TRIM si le volume a accumulé beaucoup d'espace fantôme : sans cela, la marge réelle reste plus faible que ce que suggèrent les chiffres du pool.

La boutique concernée reste volontairement anonyme : ce qui compte ici, c'est le mécanisme. Si un site tombe avec un 500 instantané, que SSH expire et que le système de fichiers se révèle en lecture seule, la première chose à vérifier n'est pas « combien de disque reste-t-il dans le conteneur » : c'est combien d'espace reste réellement dans le pool en dessous, parce que ce sont deux chiffres différents, et un seul des deux, quand il tombe à zéro, fait tomber la boutique.

Laisser un commentaire

Les commentaires sont modérés avant publication.

← Blog

Chargement...
Retour en haut