Terres dévastées

Vallée de montagne enneigée entre deux sommets rocheux, sous un ciel bleu

Étiquette : carnet d’admin

Billets, notes et écrits associés au mot-clé « carnet d’admin » sur le carnet de bord personnel.

Schéma : le répertoire de données de MySQL 8 (datadir)

Dans les fichiers d’une base de données (1/3) : InnoDB, de MySQL à MariaDB

Avec InnoDB comme avec les autres moteurs, une base de données relationnelle donne l’illusion de tables, de lignes et de colonnes. Sur le disque, il n’y a que des fichiers découpés en blocs de taille fixe. Comprendre comment ces blocs sont organisés explique beaucoup de comportements qui semblent mystérieux : pourquoi un fichier ne rétrécit pas après un DELETE, pourquoi une clé primaire en UUID ralentit les insertions, pourquoi un index secondaire coûte plus cher qu’on ne le pense, ou ce que protège vraiment le « doublewrite buffer ».

Cette série de trois billets ouvre le capot de trois moteurs : InnoDB (le moteur de stockage de MySQL et de MariaDB), PostgreSQL et Oracle. Le même fil conducteur les traverse : les fichiers, les pages, les lignes, et l’endroit où vivent les anciennes versions des données. Premier arrêt : InnoDB.

Le répertoire de données

Arborescence du datadir de MySQL 8
Les fichiers d’une instance MySQL 8 et leurs équivalents MariaDB.

Chaque fichier a un rôle précis :

  • ibdata1, le tablespace système. Historiquement, il contenait tout : tables, index, undo, dictionnaire. Aujourd’hui, il ne garde plus que le change buffer (sur MySQL) et, sur MariaDB, le doublewrite et parfois l’undo. Il ne rétrécit jamais : un ibdata1 de 50 Go gonflé par une vieille transaction le restera, et la seule façon de le réduire est un export complet suivi d’une réimportation ;
  • un fichier .ibd par table, grâce à innodb_file_per_table=ON (le défaut depuis MySQL 5.6). C’est ce qui permet de récupérer l’espace d’une table supprimée ou vidée (DROP, TRUNCATE), de déplacer une table seule, ou de restaurer un seul tablespace ;
  • les tablespaces d’undo (undo_001…) : les images d’avant des lignes modifiées, pour les ROLLBACK et pour la lecture cohérente des autres transactions ;
  • le journal redo : chaque modification de page y est décrite avant que la page elle-même soit écrite sur le disque. Après un crash, InnoDB rejoue ce journal ;
  • le doublewrite buffer, expliqué plus bas ;
  • les binlogs, qui n’appartiennent pas à InnoDB mais au serveur : ils décrivent les changements au niveau SQL, pour la réplication et la restauration à un instant donné.

Une base de données SQL (CREATE DATABASE shop) n’est rien d’autre qu’un répertoire. Les noms exacts des fichiers globaux varient : MySQL et MariaDB ont divergé sur le dictionnaire (MySQL 8 a remplacé les fichiers .frm par un dictionnaire transactionnel dans mysql.ibd, MariaDB a gardé les .frm), sur le redo et sur le doublewrite. Le format interne des pages, lui, est commun. Pour voir les tablespaces d’une instance :

-- MySQL 8
SELECT name, file_size, allocated_size FROM information_schema.INNODB_TABLESPACES;
-- MariaDB
SELECT name, file_size, allocated_size FROM information_schema.INNODB_SYS_TABLESPACES;

Pages, extents, segments

Hiérarchie tablespace, segments, extents, pages
Un tablespace est découpé en pages de 16 Ko, regroupées en extents de 1 Mo, attribués aux segments de chaque index.

L’unité de base est la page, de 16 Ko par défaut. Sa taille (innodb_page_size) est fixée à la création de l’instance et ne peut plus changer ensuite. Tout ce qu’InnoDB lit ou écrit, il le lit ou l’écrit par pages entières : lire une ligne de 100 octets, c’est charger 16 Ko en mémoire, dans le buffer pool.

Les pages sont regroupées en extents de 64 pages, soit 1 Mo. Allouer par extents plutôt que page par page garde les pages d’un même index physiquement proches, ce qui rend les lectures séquentielles efficaces.

Chaque index possède deux segments : un pour ses pages feuilles, un pour ses pages intermédiaires. Les séparer garde les feuilles (là où sont les données) contiguës, pour les parcours par plage. Pour ne pas réserver 1 Mo à chaque petite table, un segment reçoit d’abord jusqu’à 32 pages isolées, et ne passe aux extents complets qu’ensuite. C’est pourquoi une table vide occupe une centaine de kilo-octets, et pas plusieurs mégaoctets.

Les premières pages d’un .ibd sont réservées à la gestion de l’espace : la page 0 (FSP_HDR) décrit l’état des extents, la page 1 le bitmap du change buffer, la page 2 les « inodes » des segments. Sur MySQL 8, la page 3 contient la description de la table en JSON (SDI) et la racine de l’index cluster est en page 4. Sur MariaDB, qui garde des .frm, la racine est en page 3.

Anatomie d’une page InnoDB

Structure d'une page d'index InnoDB de 16 Ko
Une page d’index : en-têtes, lignes, espace libre, répertoire et queue.

Chaque zone a une raison d’être :

  • Le FIL header est commun à toutes les pages, quel que soit leur type. Il contient le numéro de page, le type de page et le LSN (Log Sequence Number) de la dernière modification. Dans l’ancienne disposition, il contient aussi la somme de contrôle, qui détecte une corruption à la lecture ; sur MariaDB 10.11, la disposition par défaut (full_crc32) laisse les octets 0 à 3 à zéro et place la somme de contrôle tout à la fin de la page (voir les tests en fin de billet). Après un crash, InnoDB compare ce LSN à celui de chaque entrée du redo : il ne rejoue que ce que la page n’a pas encore reçu. Les pointeurs vers les pages précédente et suivante relient toutes les pages d’un même niveau de l’arbre ;
  • l’INDEX header donne le niveau de la page dans l’arbre (0 pour une feuille), le nombre de lignes et l’identifiant de l’index auquel elle appartient ;
  • infimum et supremum sont deux fausses lignes, la plus petite et la plus grande possibles. Elles servent de début et de fin à la liste chaînée des lignes, ce qui simplifie le code d’insertion ;
  • les lignes sont écrites dans l’ordre où elles arrivent, mais chacune pointe vers la suivante dans l’ordre de la clé. Insérer au milieu ne déplace rien : il suffit de modifier deux pointeurs ;
  • le page directory part de la fin de la page et grandit vers le haut. Il contient un « slot » pour 4 à 8 lignes. Pour trouver une clé, InnoDB fait une recherche dichotomique dans ces slots, puis parcourt au plus quelques lignes de la liste chaînée ;
  • le FIL trailer occupe les 8 derniers octets de la page. Dans l’ancienne disposition, il répète la somme de contrôle et les 32 bits de poids faible du LSN : une page dont le début et la fin ne correspondent pas n’a été écrite qu’à moitié. Sur MariaDB 10.11 (full_crc32), les 4 derniers octets sont la somme de contrôle elle-même, un CRC-32C du reste de la page, et les 4 octets qui les précèdent sont les 32 bits de poids faible du LSN.

Quand une page est pleine et qu’une ligne doit y entrer, InnoDB la coupe en deux (page split) et met à jour la page parente. Quand deux pages voisines descendent sous la moitié de leur remplissage, il peut les fusionner. Ces opérations sont coûteuses et laissent des pages à moitié vides : c’est la fragmentation.

Le doublewrite buffer : pourquoi écrire deux fois

Un disque garantit l’atomicité d’écriture de 512 octets ou 4 Ko, pas de 16 Ko. Si le serveur s’arrête au milieu de l’écriture d’une page, celle-ci peut se retrouver à moitié ancienne, à moitié nouvelle : une torn page. Le redo ne peut pas la réparer, car il décrit des modifications à appliquer sur une page cohérente. InnoDB écrit donc chaque page d’abord dans le doublewrite buffer, puis à son emplacement définitif. Après un crash, une page déchirée est remplacée par sa copie intacte, puis le redo s’applique. Sur un système de fichiers qui garantit lui-même des écritures atomiques de 16 Ko (ZFS, certains SSD), on peut le désactiver.

Une ligne

Format d'une ligne InnoDB DYNAMIC
Une ligne de l’index cluster et le détail de son en-tête.
  • Les colonnes cachées : DB_TRX_ID est l’identifiant de la dernière transaction qui a modifié la ligne, DB_ROLL_PTR pointe vers l’image précédente dans l’undo. Une transaction qui ne doit pas voir la modification suit ce pointeur pour reconstruire l’ancienne version ;
  • sans clé primaire, InnoDB utilise la première clé unique non nulle, ou à défaut ajoute une colonne cachée DB_ROW_ID de 6 octets, alimentée par un compteur global partagé par toutes ces tables. Définissez toujours une clé primaire ;
  • le bit « deleted » : un DELETE ne fait que marquer la ligne. Elle est réellement retirée plus tard par le purge thread, une fois qu’aucune transaction ne peut plus la voir ;
  • les NULL ne coûtent qu’un bit dans le bitmap : aucune place dans les données ;
  • les colonnes longues (TEXT, BLOB, gros VARCHAR) : une ligne doit tenir dans environ la moitié d’une page, pour qu’une page contienne au moins deux lignes. Au format DYNAMIC, une valeur trop longue part entière dans des pages de débordement, et la ligne ne garde qu’un pointeur de 20 octets.

L’index cluster d’InnoDB : la table est un arbre

Index cluster et index secondaire InnoDB
Les feuilles de l’index PRIMARY contiennent les lignes ; un index secondaire contient la clé primaire.

C’est la décision de conception la plus importante d’InnoDB : la table est l’index de la clé primaire. Il n’existe pas de « tas » de lignes à côté : les lignes complètes sont stockées dans les feuilles d’un arbre B+ trié par clé primaire. Plusieurs conséquences pratiques en découlent :

  • une recherche par clé primaire descend l’arbre et trouve directement la ligne. Avec des pages de 16 Ko et une clé BIGINT, une page intermédiaire référence plus d’un millier de pages filles : trois niveaux suffisent pour des centaines de millions de lignes ;
  • une clé primaire croissante (AUTO_INCREMENT) ajoute toujours à droite de l’arbre : les pages se remplissent proprement, sans split au milieu. Une clé aléatoire (UUID v4) insère n’importe où, provoque des splits partout et laisse des pages à moitié pleines. Les UUID ordonnés dans le temps (UUID v7) évitent ce problème ;
  • un index secondaire ne contient pas l’adresse physique de la ligne mais sa clé primaire. Une recherche par index secondaire coûte donc deux descentes d’arbre. En contrepartie, déplacer une ligne lors d’un split ne demande pas de mettre à jour les index secondaires ;
  • une clé primaire longue est recopiée dans chaque index secondaire : une clé de 36 caractères multiplie la taille de tous les index ;
  • un index couvrant, qui contient toutes les colonnes dont la requête a besoin, évite la seconde descente : EXPLAIN affiche alors Using index.

Pourquoi le fichier ne rétrécit pas

Un DELETE massif libère de la place dans les pages, que les insertions suivantes réutiliseront, mais ne rend rien au système de fichiers. Pour récupérer l’espace, il faut reconstruire la table :

OPTIMIZE TABLE shop.orders;          -- équivaut, pour InnoDB, à ALTER TABLE … FORCE
-- data_free n'est qu'une estimation, voir les tests plus bas
SELECT table_name, data_free/1024/1024 AS free_mb
FROM information_schema.tables WHERE table_schema='shop' ORDER BY data_free DESC;

N’utilisez pas data_free pour décider si une reconstruction vaut le coup : dans mon test, il est resté à 7,0 Mo après la suppression de 90 % d’une table dont le fichier faisait 80 Mio, et la reconstruction a ramené le fichier à 9 Mio. C’est la taille du fichier sur disque que la reconstruction récupère.

La reconstruction copie toutes les lignes dans un nouveau fichier, compact, puis remplace l’ancien. Elle se fait en ligne sur les versions récentes, mais demande temporairement l’espace d’une seconde copie de la table.

Prochain billet : PostgreSQL, qui a fait le choix inverse sur presque tous ces points : une table en tas, des pages de 8 Ko, et des anciennes versions gardées dans la table elle-même.

Ce que j’ai mesuré (mise à jour du 2026-11-04)

J’ai d’abord écrit ce billet à partir de la documentation. Je l’ai ensuite testé sur MariaDB 10.11.18, Rocky Linux 10.2, WSL2, sur une instance jetable avec un buffer pool de 256 Mo. Ces tests ont corrigé deux affirmations plus haut (la position de la somme de contrôle et data_free) et confirmé la disposition des pages, les colonnes cachées, le comportement d’OPTIMIZE TABLE et l’emplacement du doublewrite buffer. Je n’ai pas testé MySQL 8 : les numéros de page 3 (SDI) et 4 (racine) pour MySQL viennent de la documentation.

Les deux vues de l’information schema, et les fichiers d’une base d’une seule table :

-rw-rw---- 1 mysql mysql  1055 Nov  4 17:21 orders.frm
-rw-rw---- 1 mysql mysql 65536 Nov  4 17:21 orders.ibd
| name             | file_size | allocated_size |
| innodb_system    |  12582912 |       12587008 |
| shop/orders      |     65536 |          65536 |
| innodb_temporary |  12582912 |       12582912 |
ERROR 1109 (42S02) at line 1: Unknown table 'INNODB_TABLESPACES' in information_schema

Les premières pages de orders.ibd (4 pages de 16 Kio), après un arrêt propre. La page 0 est l’en-tête de l’espace, la 1 le bitmap du change buffer, la 2 les inodes de segments, et la page 3 est la racine de l’index clustered :

page 0: type=8 (FSP_HDR) page_no=0 space_id=5 lsn=46034 prev=0xffffffff next=0xffffffff
page 1: type=5 (IBUF_BITMAP) page_no=1 space_id=5 lsn=45760 prev=0xffffffff next=0xffffffff
page 2: type=3 (INODE) page_no=2 space_id=5 lsn=46034 prev=0xffffffff next=0xffffffff
page 3: type=17855 (INDEX) page_no=3 space_id=5 lsn=47160 prev=0xffffffff next=0xffffffff

Où se trouve la somme de contrôle. Avec innodb_checksum_algorithm = full_crc32, les octets 0 à 3 sont à zéro, et les 4 derniers octets égalent le CRC-32C des 16380 premiers octets de la page, pour chacune des 4 pages :

innodb_checksum_algorithm = full_crc32
page 0: bytes 0-3=0x00000000  last 4 bytes=0x34c1c1a7  crc32c(page without last 4 bytes)=0x34c1c1a7  match=True
page 1: bytes 0-3=0x00000000  last 4 bytes=0x248d385f  crc32c(page without last 4 bytes)=0x248d385f  match=True
page 2: bytes 0-3=0x00000000  last 4 bytes=0x85d98b4a  crc32c(page without last 4 bytes)=0x85d98b4a  match=True
page 3: bytes 0-3=0x00000000  last 4 bytes=0x3e60cc66  crc32c(page without last 4 bytes)=0x3e60cc66  match=True

Les colonnes cachées, lues directement dans la page 3. Après la clé primaire sur 4 octets (80000001, bit de signe inversé) viennent DB_TRX_ID sur 6 octets et DB_ROLL_PTR sur 7 octets, puis les colonnes utilisateur :

id=1 at page offset 128: pk=80000001 DB_TRX_ID=000000000013 DB_ROLL_PTR=84000001340110 columns: 616c6963656669727374060300000018
id=2 at page offset 163: pk=80000002 DB_TRX_ID=000000000013 DB_ROLL_PTR=8400000134011c columns: 626f627365636f6e64050500000020ff
id=3 at page offset 197: pk=80000003 DB_TRX_ID=000000000013 DB_ROLL_PTR=84000001340128 columns: 6361726f6c7468697264000000000000

Un DELETE ne réduit pas le fichier, data_free ne voit pas l’espace libre, et OPTIMIZE TABLE reconstruit la table. ALTER TABLE ... FORCE donne la même taille finale (9437184 octets). ibdata1 est passé de 12 à 76 Mio pendant le chargement (les undo logs sont ici dans le tablespace système, innodb_undo_tablespaces = 0) et n’a pas diminué ensuite. Dans les lignes de tableau ci-dessous, les colonnes sont table_rows, data_mb, index_mb et data_free_mb :

-- after loading 300000 rows
   big.ibd: 83886080 bytes
   ibdata1: 79691776 bytes
|     299999 |     0.0 |      0.0 |          7.0 |
-- after DELETE of 90% of the rows
   big.ibd: 83886080 bytes
   ibdata1: 79691776 bytes
|      29999 |     0.0 |      0.0 |          7.0 |
| shop.big | optimize | note     | Table does not support optimize, doing recreate + analyze instead |
-- after OPTIMIZE TABLE
   big.ibd: 9437184 bytes
   ibdata1: 79691776 bytes
|      29614 |     8.5 |      0.0 |          0.0 |
-- before ALTER TABLE FORCE
   big.ibd: 146800640 bytes
   ibdata1: 79691776 bytes
|      29614 |     8.5 |      0.0 |          7.0 |
-- after ALTER TABLE FORCE
   big.ibd: 9437184 bytes
   ibdata1: 79691776 bytes
|      29614 |     8.5 |      0.0 |          0.0 |

Les clés aléatoires. 500 000 lignes, buffer pool de 256 Mo, tout en mémoire. L’UUID() horodaté (version 1) et la clé aléatoire de type version 4 donnent des fichiers plus gros qu’un BIGINT séquentiel. Les largeurs de clé diffèrent (8 et 36 octets) : comparez plutôt les deux variantes d’UUID entre elles, la clé aléatoire est 40 % plus grosse, ce qui est cohérent avec des pages moins remplies (je n’ai pas mesuré le remplissage). Je n’ai pas pu montrer de ralentissement des insertions avec tout en mémoire, et la dernière ligne inclut le coût de RANDOM_BYTES() :

insert 500000 rows, sequential bigint : 0.8 s
insert 500000 rows, random UUID (v1)  : 1.1 s
| shop/pk_seq  |    32.0 |
| shop/pk_uuid |    48.0 |
insert 500000 rows, random v4-like UUID : 2.3 s
| shop/pk_rand |    68.0 |

Où se trouve le doublewrite buffer. J’ai démarré MariaDB 10.11.18 deux fois, avec la valeur par défaut innodb_doublewrite=ON puis avec OFF, chargé 20 000 lignes dans une table et tracé les écritures de pages avec strace. Les deux essais utilisaient innodb_use_native_aio=OFF, sinon les écritures ne passent pas par pwrite64 et strace ne les voit pas. Avec le doublewrite activé, ibdata1 reçoit deux écritures groupées qui commencent à la page 64 (160 Kio et 64 Kio), et Innodb_dblwr_pages_written passe de 0 à 39. Désactivé, rien n’est écrit à la page 64 d’ibdata1 et le compteur reste à 0. Sur MariaDB 10.11, le doublewrite buffer se trouve donc dans le tablespace système. Je n’ai pas regardé sa disposition exacte au-delà de la page de départ.

################ innodb_doublewrite = ON (default), native AIO off
| innodb_doublewrite | ON    |
| Innodb_dblwr_pages_written | 0     |
| Innodb_dblwr_pages_written | 39    |
  ibdata1             4 writes,     262144 bytes, pages 0..316
first alternations (file, consecutive writes): [['dw.ibd', 10], ['ibdata1', 1], ['dw.ibd', 39], ['ibdata1', 1], ['dw.ibd', 1], ['ibdata1', 2]]
ibdata1 writes as (first page, KiB): [(316, 16), (64, 160), (64, 64), (0, 16)]
################ innodb_doublewrite = OFF, native AIO off
| innodb_doublewrite | OFF   |
| Innodb_dblwr_pages_written | 0     |
| Innodb_dblwr_pages_written | 0     |
  ibdata1             2 writes,      32768 bytes, pages 0..49
first alternations (file, consecutive writes): [['dw.ibd', 5], ['ibdata1', 1], ['dw.ibd', 20], ['ibdata1', 1]]
ibdata1 writes as (first page, KiB): [(49, 16), (0, 16)]

Les compteurs sont ceux de SHOW GLOBAL STATUS LIKE 'Innodb_dblwr%', avant et après le chargement.


Ce billet fait partie d’une série de notes d’administration système (tag « carnet d’admin »).
Série « Dans les fichiers d’une base de données », partie 1/3. · Billet suivant → · English version

Articles liés

Chronologie du reboot (19:49 à 20:20) et les preuves d'un arrêt propre : Wazuh, rsyslog, wtmp, montages XFS, puis blocage matériel dû à la carte mère

Le serveur qui ne revenait pas après un reboot : lire les traces d’un arrêt, la carte mère coupable

Un mercredi soir, mise à jour classique du serveur de la maison : dnf update -y, une centaine de paquets dont un nouveau noyau et de nouveaux firmwares, puis reboot. D’habitude, la machine revient en moins d’une minute. Cette fois, rien. Pas de ping, pas de SSH, et surtout aucune réponse ARP sur le réseau local : vu du switch, la carte réseau n’existait plus. Cet article reprend l’enquête dans l’ordre : le symptôme, les traces qu’un arrêt laisse derrière lui, puis le coupable : la carte mère.

Chronologie du reboot (19:49 à 20:20) et les preuves d'un arrêt propre : Wazuh, rsyslog, wtmp, montages XFS, puis blocage matériel dû à la carte mère
Ce que les traces d’un reboot prouvent, et où elles s’arrêtent.

En un coup d’œil

IndiceCe que j’ai observéCe que ça prouve
RéseauNi ping, ni SSH, aucune réponse ARPLe noyau ne tourne pas, ou la carte réseau n’est pas initialisée
WazuhDernier événement « Wazuh agent stopped » à l’heure du rebootArrêt propre, pas un crash
rsyslogLe journal s’arrête à « System is rebooting »Silence normal : rsyslog s’arrête parmi les premiers services
wtmpEntrée shutdown à 19:50:04systemd est allé au bout de la séquence d’arrêt
Montages XFS« Ending clean mount » sur les quatre systèmes de fichiersTous les disques ont été démontés proprement
ConclusionTrente minutes sans aucune activitéBlocage après l’appel de redémarrage du noyau : carte mère ou firmware

Le symptôme : une machine muette

Le détail de l’ARP compte. Une machine qui démarre mais dont un service plante répond toujours à l’ARP, car c’est le noyau qui répond, pas une application. L’absence d’ARP signifie que le noyau ne tourne pas, ou que la carte réseau n’est pas initialisée. Le problème est donc bas niveau : démarrage du noyau, initramfs, firmware de la carte mère, ou machine tout simplement éteinte.

Ce qu’on peut vérifier à distance

Avant de descendre devant la machine, j’ai rassemblé ce qui était visible depuis le reste du réseau :

arping -c 3 -I eth0 192.168.10.17     # aucune réponse : la carte réseau est muette
ip neigh show 192.168.10.17           # entrée FAILED dans la table ARP du voisin
  • Le SIEM (Wazuh) avait reçu un dernier événement de l’agent de la machine : « Wazuh agent stopped », à l’heure exacte du reboot. C’est le signe d’un arrêt propre, pas d’un crash. Un crash n’aurait laissé aucun message.
  • Les autres machines du réseau fonctionnaient normalement : le problème ne venait ni du switch ni du routeur.
  • La machine virtuelle hébergée sur ce serveur était tombée avec lui, ce qui est cohérent avec un hôte éteint.

Ce qu’il fallait voir à l’écran

Sans carte de gestion à distance (pas d’IPMI sur une carte mère grand public), la seule façon de savoir est de regarder l’écran. Voici la liste que je me suis préparée, de la cause la plus probable à la moins probable, pour ne pas improviser devant la console :

  • Bloqué dans GRUB ? Choisir le noyau précédent dans le menu. S’il démarre, le nouveau noyau ou son initramfs sont en cause. On le fixe ensuite avec grubby --set-default.
  • Shell d’urgence (dracut, emergency mode) ? journalctl -xb -p err montre pourquoi : volume LVM introuvable, entrée fstab invalide, module manquant dans l’initramfs.
  • Réétiquetage SELinux ou fsck en cours ? C’est long sur un gros disque. Il faut attendre, surtout ne pas couper.
  • Démarré mais sans réseau ? ip -br a et nmcli dev : une carte renommée ou « unmanaged » après une mise à jour arrive plus souvent qu’on ne croit.
  • Écran noir, ventilateurs qui tournent, rien ne bouge ? C’est le cas le plus déroutant. C’était le mien.

La machine a fini par repartir après une demi-heure, avec un appui long sur le bouton d’alimentation. Le système a démarré normalement sur le nouveau noyau, sans aucune erreur. Restait à comprendre ce qui s’était passé, alors que le journal système ne gardait que le démarrage en cours. C’est l’objet de la section suivante.

Lire les traces d’un arrêt

La machine est revenue, mais journalctl ne montre que le démarrage en cours. Sur ce serveur, le journal n’est pas persistant (Storage=auto sans répertoire /var/log/journal). Les traces du démarrage précédent sont donc perdues de ce côté-là. Heureusement, d’autres sources survivent à un redémarrage.

Source 1 : rsyslog et /var/log/messages

grep -E "System is rebooting|Command line:" /var/log/messages | tail -4
# 19:49:42 systemd-logind: System is rebooting.
# 20:20:22 kernel: Command line: BOOT_IMAGE=(hd0,gpt2)/vmlinuz-6.12.0-211.67.1…

Le journal s’arrête net à « System is rebooting », puis reprend trente minutes plus tard avec la ligne de commande du nouveau noyau. Ce silence n’est pas suspect : rsyslog fait partie des premiers services arrêtés pendant un reboot, il n’écrit donc jamais la fin de la séquence d’arrêt. Il faut chercher la suite ailleurs.

Source 2 : wtmp, le registre des connexions et des arrêts

last -x -F | head -6
# reboot   system boot  6.12.0-211.67.1  Wed Oct 21 20:20:26 2026   still running
# shutdown system down  6.12.0-211.54.1  Wed Oct 21 19:50:04 2026 - 20:20:26 (00:30)

last -x affiche aussi les entrées système, pas seulement les connexions. La ligne shutdown system down est écrite par systemd-update-utmp à la toute fin de la séquence d’arrêt, quand presque tous les services sont déjà arrêtés. Sa présence prouve que systemd est allé au bout. Elle est horodatée à 19:50:04, soit 22 secondes après le début du reboot. La colonne « (00:30) » est la durée jusqu’au démarrage suivant : trente minutes pendant lesquelles la machine n’a rien fait.

Source 3 : le montage des systèmes de fichiers au démarrage suivant

grep -E "XFS .*(Mounting|recovery|clean mount)" /var/log/messages | grep "20:20"
# XFS (dm-0): Mounting V5 Filesystem …
# XFS (dm-0): Ending clean mount

Quand un système de fichiers XFS n’a pas été démonté proprement, le montage suivant rejoue son journal et affiche « Starting recovery » puis « Ending recovery ». Ici, les quatre systèmes de fichiers indiquent « Ending clean mount » : ils avaient tous été démontés correctement. Le système d’exploitation avait donc terminé son arrêt avant que la machine ne se fige.

Chronologie du redémarrage : les journaux couvrent l'arrêt propre, puis le silence jusqu'à la coupure
Où s’arrêtent les traces : tout est journalisé jusqu’à l’appel de redémarrage du noyau, le blocage est côté matériel.

Ce que ça permet de conclure

  • l’arrêt systemd est allé au bout (entrée shutdown dans wtmp) ;
  • les disques ont été démontés proprement (montage « clean » ensuite) ;
  • la machine n’a ensuite plus rien fait pendant trente minutes, jusqu’à ce qu’on la relance à la main.

Le blocage s’est donc produit après que le noyau a demandé à la carte mère de redémarrer, quelque part entre cet appel et le BIOS. Ce n’est ni un paquet, ni un service, ni un système de fichiers.

Pour la prochaine fois

Rendre le journal persistant simplifie beaucoup ce genre d’enquête. C’est une ligne :

mkdir -p /var/log/journal && systemd-tmpfiles --create --prefix /var/log/journal
journalctl --list-boots      # les démarrages précédents apparaissent désormais

Même persistant, le journal ne verra jamais ce qui se passe après l’arrêt du noyau. Les indices de wtmp et des montages restent les plus fiables pour savoir si un arrêt s’est terminé proprement.

Le coupable : la carte mère

Récapitulons les preuves : l’arrêt systemd s’est terminé, les systèmes de fichiers ont été démontés proprement, puis la machine n’a plus rien fait pendant trente minutes. Le coupable se trouve donc entre l’appel de redémarrage du noyau et le premier écran du BIOS. Autrement dit, la carte mère n’a jamais effectué sa réinitialisation.

Comment Linux redémarre une machine

À la fin de l’arrêt, le noyau appelle reboot(), et celui-ci doit déclencher un vrai reset matériel. Sur x86, le noyau essaie plusieurs méthodes, dans un ordre par défaut qui dépend de la machine :

  • acpi : écrire dans le registre de reset déclaré par le firmware (table ACPI FADT) ;
  • kbd : la méthode historique, une impulsion via le contrôleur clavier ;
  • pci : écrire dans le port 0xCF9, le registre de reset du chipset ;
  • efi : demander au firmware UEFI de redémarrer ;
  • triple : provoquer une « triple faute » du processeur, en dernier recours.

Si la méthode choisie ne fonctionne pas sur une carte précise, le noyau a tout arrêté et attend un reset qui ne vient jamais. C’est exactement le tableau observé : écran noir, ventilateurs qui tournent, plus aucune activité réseau.

Pourquoi cette fois-là, et pas les autres ?

Le redémarrage suivant, sur le nouveau noyau, a pris 34 secondes, sans aucun souci. Plusieurs facteurs ont pu jouer le soir du blocage : l’ancien noyau avec 35 jours d’uptime, et une mise à jour des firmwares (linux-firmware) dans la même transaction. Ces blocages au reset sont connus pour être intermittents sur certaines cartes mères grand public. Je n’ai pas pu reproduire le problème, et je n’ai donc rien changé dans l’immédiat.

Si ça se reproduit

La piste est de forcer une autre méthode de reset avec le paramètre noyau reboot=, puis de tester plusieurs redémarrages :

grubby --update-kernel=ALL --args="reboot=pci"     # ou reboot=efi, reboot=acpi
grubby --info=DEFAULT | grep args                  # vérifier
reboot                                             # tester, plusieurs fois
grubby --update-kernel=ALL --remove-args="reboot"  # pour revenir en arrière

reboot=pci (le registre 0xCF9 du chipset) est souvent la plus fiable sur le matériel Intel grand public, et reboot=efi sur les firmwares UEFI récents. Le paramètre s’applique à tous les noyaux installés, y compris les prochains.

Ce que j’en retiens

  • avant d’accuser une mise à jour, vérifiez si l’arrêt s’est terminé : wtmp et les montages « clean » le disent sans ambiguïté ;
  • un blocage après un arrêt propre relève du matériel ou du firmware, pas du système : aucun retour arrière de paquets ne l’aurait corrigé ;
  • rendez le journal persistant (/var/log/journal) pour que les démarrages précédents restent consultables ;
  • si la machine héberge un service dont tout dépend, notez la procédure de reprise. Ici, le coffre à secrets doit être descellé à la main après chaque redémarrage : c’est le sujet d’un autre article.

Articles liés

Page 2 of 2

Propulsé par WordPress & Thème par Anders Norén