Ubuntu 24.04 : DKMS cassé après mise à jour du noyau — récupérer les pilotes sans interruption

Vous déployez un correctif sur une flotte. Le noyau est mis à jour. Quelques minutes plus tard, votre supervision s’affole : GPU absents, pools ZFS qui se plaignent, offloads NIC perdus, peut‑être un tissu InfiniBand qui vacille. Les services tournent encore… pour l’instant. Puis vous voyez le problème : DKMS n’a pas construit les modules pour le nouveau noyau, donc le prochain redémarrage sera un piège.

C’est la réalité d’exécuter Ubuntu 24.04 en production. Les mises à jour du noyau sont routinières ; les échecs DKMS sont le prix à payer pour les pilotes hors‑arbre. L’objectif ici n’est pas de « réparer après casse ». L’objectif est : réparer pendant que le système reste en ligne, et rendre la prochaine mise à jour du noyau ennuyeuse.

Comment DKMS échoue réellement après une mise à jour du noyau

DKMS (Dynamic Kernel Module Support) existe parce que les fournisseurs continuent de livrer des modules noyau qui ne sont pas dans le noyau principal. NVIDIA, ZFS‑on‑Linux, certains pilotes NIC et RAID, VirtualBox, certains agents de sécurité — tout ce qui compile contre des en‑têtes du noyau est concerné.

Lorsque Ubuntu installe un nouveau noyau, les scripts du paquet tentent de reconstruire les modules DKMS pour ce noyau. Si cette reconstruction échoue, vous pouvez ne pas le remarquer immédiatement parce que le noyau en cours d’exécution a encore des modules fonctionnels chargés. La panne apparaît lorsque :

  • Vous redémarrez et le nouveau noyau démarre sans le module.
  • l’initramfs a été généré sans le module, provoquant des échecs en début de démarrage (stockage, root‑on‑ZFS, chiffrement, etc.).
  • Secure Boot bloque le module non signé, et vous obtenez le cas particulier « construit mais non chargeable ».
  • les en‑têtes du nouveau noyau n’étaient pas installés, donc DKMS n’avait rien contre quoi compiler.

La plupart des incidents « DKMS cassé » sont l’un de ces quatre cas. La solution n’est que rarement mystérieuse ; le problème est le plus souvent chronophage et opérationnellement stressant. L’astuce est de réduire le risque : diagnostiquer précisément, construire pour le noyau dans lequel vous comptez démarrer, valider la possibilité de chargement, et seulement alors autoriser les redémarrages.

Vérité sèche : DKMS n’est pas « dynamique » comme l’imagine la direction. C’est « dynamique » comme un formulaire papier : on peut le remplir à nouveau chaque fois que le noyau change.

Playbook de diagnostic rapide

Quand vous cherchez à éviter une interruption, la vitesse compte. Le chemin le plus rapide est : identifier le noyau cible, confirmer si le module existe pour lui, confirmer s’il peut se charger, puis valider les artefacts de démarrage (initramfs). Tout le reste est de la garniture.

Première étape : quel noyau exécutez‑vous et quels noyaux sont installés ?

  • Si vous exécutez encore l’ancien noyau, vous pouvez reconstruire calmement avant de redémarrer.
  • Si vous êtes déjà sur le nouveau noyau et que des modules manquent, vous devez rétablir la fonctionnalité sur le noyau en cours (parfois possible, parfois non).

Deuxième étape : DKMS indique‑t‑il « built » pour le noyau cible ?

  • Si ce n’est pas construit : vous êtes en mode « reconstruire et résoudre les dépendances de build ».
  • Si c’est construit : vérifiez s’il a été installé dans /lib/modules/<kernel> et si modprobe réussit.

Troisième étape : Secure Boot bloque‑t‑il le module ?

  • Secure Boot activé + module non signé = il se compilera mais échouera au chargement avec des erreurs de signature.
  • C’est la boucle numéro un « je l’ai reconstruit trois fois et rien n’a changé ».

Quatrième étape : l’initramfs contient‑il ce dont vous avez besoin ?

  • Si le module est nécessaire au démarrage précoce (root‑on‑ZFS, stockage, crypto), « construit » ne suffit pas.
  • Regénérez l’initramfs pour le noyau cible et vérifiez qu’il contient le module.

Cinquième étape : bloquez les changements risqués pendant que vous réparez

  • Mettre en attente les paquets noyau si les mises à jour automatiques continuent de tirer de nouveaux noyaux pendant votre récupération.
  • Épingler un noyau connu bon comme option de retour arrière.

Faits et contexte intéressants (pourquoi cela se répète)

  • DKMS est né dans l’écosystème Dell au milieu des années 2000 pour maintenir les pilotes fournisseurs compilables lors des montées de noyau, surtout sur les flottes d’entreprise.
  • Ubuntu intègre DKMS depuis des années, mais cela repose toujours sur les scripts de packaging et la présence des en‑têtes — pas d’en‑têtes, pas de module.
  • La rigidité de Secure Boot a transformé les “échecs de build” en “échecs de chargement”. Le module peut compiler parfaitement et être rejeté par le noyau.
  • ZFS on Linux a longtemps vécu hors‑arbre pour des raisons de licence ; cette histoire explique pourquoi beaucoup d’installations Ubuntu s’appuient encore sur DKMS pour ZFS.
  • La stabilité de l’ABI du noyau n’est pas garantie pour les modules hors‑arbre. Des montées mineures du noyau peuvent casser les builds si le module utilise des API internes.
  • Le rythme HWE et SRU d’Ubuntu peut vous surprendre : une mise à jour du noyau peut arriver via unattended upgrades même si vous « n’avez rien changé ».
  • l’initramfs est souvent le véritable domaine de défaillance. Le système démarre un noyau ; puis l’espace utilisateur précoce ne trouve pas le module de stockage dont il a besoin.
  • Les builds DKMS peuvent être affectés par des changements d’outillage (gcc, make, binutils). « Noyau mis à jour » signifie parfois aussi « votre compilateur a bougé ».
  • Certains fournisseurs livrent des modules précompilés pour des versions de noyau spécifiques, mais les versions de noyau d’Ubuntu dérivent ; DKMS devient le repli — jusqu’à ce qu’il ne le soit plus.

Une citation qui a survécu à plus de postmortems que quiconque ne le mérite : « L’espoir n’est pas une stratégie. » — Gene Kranz. Elle s’applique aussi aux reconstructions DKMS.

Tâches pratiques : commandes, sorties et décisions (12+)

Ceci n’est pas « exécutez tout ». C’est une boîte à outils. Chaque tâche inclut : la commande, une sortie d’exemple, ce que cela signifie, et la décision que vous prenez à partir de là.

Task 1: Confirm the running kernel

cr0x@server:~$ uname -r
6.8.0-51-generic

Ce que cela signifie : C’est le noyau en cours d’exécution. Si DKMS a cassé lors de l’installation d’un noyau plus récent, vous pouvez généralement le réparer sans interruption immédiate, car vous n’utilisez pas encore ce nouveau noyau.

Décision : Si le noyau en cours est toujours le noyau connu bon, effectuez votre reconstruction DKMS pour le nouveau noyau maintenant, puis planifiez un redémarrage contrôlé plus tard.

Task 2: List installed kernels and see what the next reboot will likely use

cr0x@server:~$ dpkg -l 'linux-image-*generic' | awk '/^ii/{print $2,$3}'
linux-image-6.8.0-51-generic 6.8.0-51.52
linux-image-6.8.0-52-generic 6.8.0-52.53

Ce que cela signifie : Vous avez au moins deux noyaux installés ; la version la plus élevée est généralement sélectionnée au démarrage.

Décision : Identifiez le noyau « cible » pour lequel vous devez avoir des modules DKMS (ici : 6.8.0-52-generic).

Task 3: Check DKMS status across kernels

cr0x@server:~$ dkms status
zfs/2.2.2, 6.8.0-51-generic, x86_64: installed
zfs/2.2.2, 6.8.0-52-generic, x86_64: built
nvidia/550.90.07, 6.8.0-51-generic, x86_64: installed
nvidia/550.90.07, 6.8.0-52-generic, x86_64: added

Ce que cela signifie : « installed » signifie que le module est compilé et copié dans /lib/modules/<kernel>. « built » est compilé mais peut ne pas être installé. « added » signifie que DKMS connaît la source mais ne l’a pas construite pour ce noyau.

Décision : Pour le noyau cible, tout ce qui n’est pas « installed » est un risque. Construisez + installez maintenant.

Task 4: Verify kernel headers exist for the target kernel

cr0x@server:~$ dpkg -l | awk '/linux-headers-6.8.0-52-generic/{print $1,$2,$3}'
ii linux-headers-6.8.0-52-generic 6.8.0-52.53

Ce que cela signifie : DKMS a besoin des en‑têtes. Si cela manque, DKMS échouera avec des erreurs du type « Kernel headers for target not found. »

Décision : Si les en‑têtes manquent, installez‑les avant de reconstruire les modules DKMS.

Task 5: Install the missing headers (if needed)

cr0x@server:~$ sudo apt-get update
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Reading package lists... Done
cr0x@server:~$ sudo apt-get install -y linux-headers-6.8.0-52-generic
Reading package lists... Done
Building dependency tree... Done
The following NEW packages will be installed:
  linux-headers-6.8.0-52-generic
Setting up linux-headers-6.8.0-52-generic (6.8.0-52.53) ...

Ce que cela signifie : Les en‑têtes sont maintenant présentes ; DKMS a une chance raisonnable.

Décision : Reconstruisez les modules DKMS pour le noyau cible.

Task 6: Trigger DKMS autoinstall for the target kernel

cr0x@server:~$ sudo dkms autoinstall -k 6.8.0-52-generic
Sign command: /lib/modules/6.8.0-52-generic/build/scripts/sign-file
Signing key: /var/lib/shim-signed/mok/MOK.priv
Public certificate (MOK): /var/lib/shim-signed/mok/MOK.der
Building module:
Cleaning build area... done.
Building module(s).... done.
Installing /lib/modules/6.8.0-52-generic/updates/dkms/zfs.ko
Installing /lib/modules/6.8.0-52-generic/updates/dkms/nvidia.ko
depmod... done.

Ce que cela signifie : DKMS a construit et installé les modules pour ce noyau spécifique, et depmod a mis à jour les cartes de dépendances des modules.

Décision : Si cela a réussi, passez à la validation : le module peut‑il se charger (ou au moins est‑il présent et signé) ?

Task 7: If it fails, read the DKMS build log like you mean it

cr0x@server:~$ sudo tail -n 40 /var/lib/dkms/nvidia/550.90.07/build/make.log
CONFTEST: drm_prime_pages_to_sg_has_drm_device_arg
CONFTEST: drm_gem_object_put_unlocked
error: implicit declaration of function ‘drm_gem_object_put_unlocked’
make[2]: *** [scripts/Makefile.build:243: /var/lib/dkms/nvidia/550.90.07/build/nvidia-drm/nvidia-drm-gem.o] Error 1
make[1]: *** [Makefile:1926: /var/lib/dkms/nvidia/550.90.07/build] Error 2
make: *** [Makefile:234: __sub-make] Error 2

Ce que cela signifie : C’est un décalage d’API au moment de la compilation. Ce n’est pas un paquet manquant ; le code source du module ne prend pas en charge cette API du noyau.

Décision : Arrêtez d’essayer des reconstructions aléatoires. Vous avez besoin d’une version du pilote/module compatible avec ce noyau (par ex. mettre à jour le paquet NVIDIA), ou vous devez démarrer sur l’ancien noyau jusqu’à résolution.

Task 8: Validate module presence for the target kernel without rebooting

cr0x@server:~$ ls -l /lib/modules/6.8.0-52-generic/updates/dkms/ | egrep 'zfs|nvidia' | head
-rw-r--r-- 1 root root  8532480 Dec 29 10:12 nvidia.ko
-rw-r--r-- 1 root root 17362944 Dec 29 10:12 zfs.ko

Ce que cela signifie : Les fichiers existent à l’endroit où DKMS les place pour ce noyau.

Décision : Validez ensuite la possibilité de chargement et l’état de signature (surtout si Secure Boot est activé).

Task 9: Check Secure Boot state (the “built but blocked” detector)

cr0x@server:~$ mokutil --sb-state
SecureBoot enabled

Ce que cela signifie : Le noyau appliquera la vérification de signature des modules. Les modules DKMS non signés échoueront au chargement.

Décision : Si Secure Boot est activé, assurez‑vous que les modules DKMS sont signés avec une clé enregistrée, ou prévoyez un flux contrôlé d’enregistrement MOK.

Task 10: Attempt to load the module on the running kernel (only when safe)

cr0x@server:~$ sudo modprobe -v zfs
insmod /lib/modules/6.8.0-51-generic/updates/dkms/spl.ko
insmod /lib/modules/6.8.0-51-generic/updates/dkms/zfs.ko

Ce que cela signifie : Sur le noyau en cours, le chargement du module réussit. C’est un contrôle de cohérence montrant que votre installation DKMS n’est pas globalement cassée.

Décision : Si modprobe échoue avec « Required key not available », vous êtes en plein problème de signature Secure Boot. Si il échoue avec « Unknown symbol », vous avez un décalage noyau/module.

Task 11: Inspect kernel logs for signature or symbol errors

cr0x@server:~$ sudo dmesg -T | tail -n 20
[Mon Dec 29 10:19:02 2025] Lockdown: modprobe: unsigned module loading is restricted; see man kernel_lockdown.7
[Mon Dec 29 10:19:02 2025] nvidia: module verification failed: signature and/or required key missing - tainting kernel

Ce que cela signifie : Secure Boot ou la politique lockdown bloque ou marque le noyau. Certains environnements tolèrent la marque (taint) ; d’autres la considèrent comme non conforme.

Décision : Si votre politique exige des modules signés, corrigez la signature et l’enregistrement maintenant, avant de redémarrer sur un noyau qui refusera complètement le module.

Task 12: Verify initramfs was rebuilt for the target kernel

cr0x@server:~$ ls -lh /boot/initrd.img-6.8.0-52-generic
-rw-r--r-- 1 root root 98M Dec 29 10:14 /boot/initrd.img-6.8.0-52-generic

Ce que cela signifie : L’initramfs existe et a été mis à jour récemment, mais cela ne garantit pas qu’il contient votre module.

Décision : Si le module est nécessaire au démarrage (ZFS root, HBA de stockage, NIC spéciale), vous devez vérifier sa présence à l’intérieur de l’initramfs.

Task 13: Confirm the module is inside initramfs (the “trust but verify” step)

cr0x@server:~$ lsinitramfs /boot/initrd.img-6.8.0-52-generic | egrep '/zfs\.ko|/nvidia\.ko' | head
usr/lib/modules/6.8.0-52-generic/updates/dkms/zfs.ko

Ce que cela signifie : ZFS est inclus dans l’espace utilisateur précoce pour ce noyau. Pour les GPU, il n’est généralement pas nécessaire dans l’initramfs ; pour les scénarios de boot stockage/réseau, il peut l’être.

Décision : Si manquant, regénérez l’initramfs après avoir réparé l’installation DKMS.

Task 14: Rebuild initramfs for a specific kernel (targeted, not shotgun)

cr0x@server:~$ sudo update-initramfs -u -k 6.8.0-52-generic
update-initramfs: Generating /boot/initrd.img-6.8.0-52-generic

Ce que cela signifie : Vous avez forcé la régénération de l’initramfs pour le noyau qui vous importe.

Décision : Relancez les contrôles lsinitramfs ; seulement après cela envisagez le redémarrage.

Task 15: Ensure the module dependency map is correct for the target kernel

cr0x@server:~$ sudo depmod -a 6.8.0-52-generic

Ce que cela signifie : modules.dep et assimilés sont mis à jour. Certains scripts postinst le font ; certains échecs l’omettent. Lancer manuellement est peu coûteux.

Décision : Si modprobe se plaint ensuite de dépendances introuvables, vous avez probablement manqué depmod ou les modules ont été placés dans un chemin non standard.

Task 16: Hold kernel updates while you stabilize (optional but often wise)

cr0x@server:~$ sudo apt-mark hold linux-image-generic linux-headers-generic
linux-image-generic set on hold.
linux-headers-generic set on hold.

Ce que cela signifie : Vous empêchez les méta‑paquets de noyau d’installer automatiquement de nouveaux noyaux.

Décision : Utilisez cela pendant la réponse à incident. Retirez la mise en attente une fois que vous avez une chaîne DKMS reproductible et une porte de validation.

Task 17: Confirm what will be the default boot entry (so you don’t reboot into the trap)

cr0x@server:~$ grep -E 'GRUB_DEFAULT|GRUB_TIMEOUT|GRUB_SAVEDEFAULT' /etc/default/grub
GRUB_DEFAULT=0
GRUB_TIMEOUT=5

Ce que cela signifie : Par défaut, l’entrée numéro un du menu est choisie, typiquement le noyau le plus récent.

Décision : Si le noyau le plus récent n’a pas de modules fonctionnels, soit corrigez DKMS pour celui‑ci, soit paramétrez temporairement GRUB pour démarrer sur le noyau connu bon.

Task 18: Spot “half-configured” packages after a messy update

cr0x@server:~$ sudo dpkg --audit
The following packages are in a mess due to serious problems during installation. They must be reinstalled for them to work properly:
 linux-image-6.8.0-52-generic

Ce que cela signifie : L’installation du paquet noyau ne s’est pas terminée proprement, ce qui peut sauter les déclencheurs DKMS et la génération d’initramfs.

Décision : Corrigez l’état du packaging avant de déboguer DKMS indéfiniment.

Task 19: Repair package state and re-run postinst triggers

cr0x@server:~$ sudo apt-get -f install
Reading package lists... Done
Building dependency tree... Done
Correcting dependencies... Done
Setting up linux-image-6.8.0-52-generic (6.8.0-52.53) ...
update-initramfs: Generating /boot/initrd.img-6.8.0-52-generic

Ce que cela signifie : Les hooks post‑install du noyau ont été exécutés. Cela inclut souvent les déclencheurs de reconstruction DKMS.

Décision : Revérifiez dkms status pour le noyau cible ; validez de nouveau la présence des modules et le contenu de l’initramfs.

Blague 1 : DKMS, c’est comme un abonnement à la salle de sport : vous ne remarquez qu’il ne fonctionne pas quand vous essayez de vous en servir.

Récupérer les pilotes sans interruption : stratégie efficace

« Sans interruption » ne signifie pas magie. Cela signifie éviter de redémarrer sur un noyau incapable de charger des modules critiques, et éviter de réinitialiser du matériel en plein trafic. Pour la plupart des incidents DKMS, le système fonctionne encore sur le noyau précédent et tout va bien — jusqu’au redémarrage. C’est votre fenêtre.

Étape 1 : Décidez ce que signifie « pilote critique » sur cet hôte

Ne traitez pas tous les modules DKMS de la même façon. Un module VirtualBox manquant sur un serveur est ennuyeux ; un module de stockage manquant sur un nœud root‑on‑ZFS est catastrophique. Classez l’hôte :

  • Critique stockage : root ZFS, pools ZFS, pilotes HBA, dépendances dm‑crypt.
  • Critique réseau : pilotes NIC hors‑arbre (rare sur Ubuntu, mais existant), modules DPDK, piles SR‑IOV, offloads fournisseurs.
  • Critique calcul : nœuds GPU NVIDIA, clusters ML, transcodeurs vidéo.
  • « Agréable à avoir » : pilotes sur postes développeurs et modules non essentiels.

Critique signifie : pas de redémarrage tant que le noyau cible n’a pas un module validé, chargeable et un initramfs sain.

Étape 2 : Construisez pour le noyau dans lequel vous démarrerez, pas pour celui que vous exécutez

Les paramètres par défaut de DKMS peuvent vous induire en erreur. Si vous lancez simplement dkms autoinstall sans -k, il cible souvent le noyau en cours. Ce n’est pas ce dont vous avez besoin en récupération. Vous avez besoin du noyau qui sera utilisé au prochain démarrage.

Construisez explicitement pour la version de noyau cible. Toujours.

Étape 3 : Préférez les paquets fournisseurs qui suivent votre ligne de noyau

Quand un module DKMS ne se compile pas à cause d’un décalage d’API, vous avez deux choix réalistes :

  • Mettre à jour le paquet pilote/module vers une version compatible avec le nouveau noyau.
  • Retarder le redémarrage et épingler la version du noyau jusqu’à ce qu’un pilote compatible existe.

Tenter de patcher la source du module sur une machine de production à 2h du matin est un hobby, pas une pratique SRE.

Étape 4 : Validez par « peut‑il se charger » et « est‑il dans l’initramfs »

La présence sur disque ne suffit pas. Il vous faut au minimum :

  • Un test de chargement sur le noyau cible (difficile sans redémarrer).
  • La validation de la signature (si Secure Boot est activé).
  • La vérification de l’inclusion dans l’initramfs pour les modules critiques au démarrage.

Un compromis pratique : validez le chemin d’artefact DKMS, lancez des vérifications modinfo, contrôlez l’état de signature du module, et validez l’initramfs. Ensuite redémarrez dans une fenêtre de maintenance contrôlée avec un noyau de retour disponible.

Étape 5 : Ne cassez pas votre propre réseau en réparant un pilote

La plupart des récupérations DKMS sollicitent fortement CPU et disque mais ne perturbent pas le trafic. La zone dangereuse, c’est décharger/recharger des modules sur un système en production. Sauf si vous avez de la redondance (bonding, multipath, clustering), évitez de recharger des modules réseau/stockage sur un hôte critique en simple exemplaire pendant les heures d’activité.

Reconstruire et installer est sûr. Décharger/recharger est un changement.

Étape 6 : Créez une « porte de redémarrage »

En production, le contrôle le plus simple pour éviter l’interruption est une politique : n’autorisez pas le redémarrage si les modules DKMS ne sont pas installés pour le noyau le plus récemment installé. Vous pouvez faire cela avec un script local qui vérifie :

  • dkms status pour le noyau cible affiche « installed » pour les modules critiques
  • lsinitramfs contient les modules nécessaires au démarrage précoce
  • mokutil --sb-state et l’état des signatures sont alignés

Puis intégrez‑le dans votre processus de changement. Ennuyeux. Efficace.

Secure Boot et signature des modules (MOK) : le casseur silencieux

Si vous exécutez Ubuntu 24.04 sur du matériel avec Secure Boot activé — et beaucoup d’organisations le font, pour des raisons de conformité — DKMS peut « réussir » et vous pouvez quand même perdre le fonctionnement. Voici pourquoi :

  • DKMS compile un module.
  • Le noyau refuse de le charger s’il n’est pas signé (ou signé par une clé non enregistrée).
  • Vous ne le découvrez que lorsque le pilote est nécessaire pour la première fois, souvent après un redémarrage.

Comment reconnaître rapidement les échecs de signature Secure Boot

Signes typiques :

  • modprobe: ERROR: could not insert '...': Required key not available
  • dmesg affiche « module verification failed » ou des restrictions lockdown
  • dkms status indique « installed » mais la fonctionnalité est absente

Que faire à ce sujet (options pragmatiques)

  1. Signer les modules DKMS et enregistrer la clé (MOK). C’est l’option propre quand Secure Boot doit rester activé.
  2. Désactiver Secure Boot dans le firmware. C’est opérationnellement le plus simple mais peut violer la politique.
  3. Utiliser des pilotes intégrés signés quand c’est possible. Meilleur à long terme, pas toujours disponible.

Vérifier si DKMS signe les modules

cr0x@server:~$ sudo grep -R "sign-file" -n /etc/dkms /etc/modprobe.d 2>/dev/null | head

Ce que cela signifie : Il peut ne pas y avoir de configuration explicite. Sur Ubuntu, la signature des modules DKMS est souvent liée à l’outillage shim/MOK et aux scripts de packaging.

Décision : Si Secure Boot est activé et que vous voyez des échecs de signature, ne faites pas d’hypothèses. Vérifiez la signature du module avec modinfo.

Inspecter les métadonnées de signature d’un module

cr0x@server:~$ modinfo -F signer /lib/modules/6.8.0-52-generic/updates/dkms/zfs.ko
Canonical Ltd. Secure Boot Signing

Ce que cela signifie : Le module porte une chaîne signer. Si elle est vide, il peut être non signé (ou dépourvu de métadonnées).

Décision : Si le champ signer manque et que Secure Boot est activé, vous devez signer et enregistrer la clé, ou accepter que le module ne se chargera pas.

Vérifier les clés MOK enregistrées

cr0x@server:~$ sudo mokutil --list-enrolled | head
[key 1]
SHA1 Fingerprint: 12:34:56:78:90:...
Subject: CN=Canonical Ltd. Secure Boot Signing

Ce que cela signifie : Le système fait confiance à un ensemble de clés. Si votre signature DKMS utilise une clé différente, le noyau la rejettera.

Décision : Alignez votre clé de signature avec les clés enregistrées, ou enregistrez la bonne clé via MOK (ce qui requiert typiquement un redémarrage vers le gestionnaire MOK).

Blague 2 : Secure Boot est le videur du club du noyau : votre module peut être parfaitement habillé et ne pas être sur la liste.

initramfs, démarrage précoce, et pourquoi « ça a été construit » ne suffit pas

initramfs est l’image compressée d’espace utilisateur précoce que le noyau charge pour passer de « noyau démarré » à « système racine monté ». Si votre module critique n’est pas dans l’initramfs, la présence du module sur le disque est sans objet parce que le disque peut ne pas être encore accessible.

Cela importe pour :

  • Systèmes root‑on‑ZFS
  • Root chiffré nécessitant des modules spécifiques tôt
  • Certaines configurations exotiques de stockage ou de boot réseau

Mode d’échec : modules DKMS installés, mais initramfs généré avant l’installation

Cela arrive lors de mises à jour interrompues, d’opérations de paquets parallèles, ou quand DKMS s’exécute tard et que l’initramfs a déjà été généré. Vous redémarrez et découvrez que l’espace utilisateur précoce ne trouve pas ZFS/SPL, ou que votre pilote de stockage est absent.

Correction : regénérez l’initramfs après l’installation DKMS pour le noyau cible, et vérifiez le contenu avec lsinitramfs.

Mode d’échec : plusieurs noyaux, initramfs obsolète

Vous pouvez avoir un initramfs correct pour le noyau en cours, mais pas pour le noyau le plus récemment installé. C’est ainsi que le piège de redémarrage se pose. Validez toujours l’initramfs correspondant au noyau dans lequel vous allez redémarrer.

Trois mini‑histoires d’entreprise (anonymisées, réalistes)

Mini‑histoire 1 : l’incident causé par une mauvaise hypothèse

Ils exploitaient un petit cluster GPU pour de l’inférence batch. Rien d’exotique : hôtes Ubuntu, pilote NVIDIA DKMS, ordonnanceur de jobs et une fenêtre de changements chaque mardi. Le rythme des mises à jour était « noyau automatiquement, pilotes quand quelqu’un se plaint ». Ça a bien marché… jusqu’à ce que non.

Une mise à jour du noyau est arrivée vendredi soir via unattended upgrades. Personne n’a remarqué parce que les nœuds tournaient encore sur l’ancien noyau et les GPU étaient toujours disponibles. Lundi matin, ils ont drainé un nœud pour maintenance non liée et l’ont redémarré. Il est revenu sans que les modules NVIDIA ne se chargent.

L’hypothèse erronée était subtile : « Si le pilote est installé, il est installé. » Ils n’avaient jamais vérifié si le pilote avait été construit pour le noyau nouvellement installé. Le nœud a redémarré sur le noyau le plus récent (comme prévu), et DKMS avait silencieusement échoué quelques jours plus tôt.

Ils ont essayé la correction classique : réinstaller le paquet pilote. Ça ne s’est toujours pas chargé. Finalement, quelqu’un a regardé dmesg et trouvé un message d’application de signature Secure Boot. Secure Boot avait été activé dans le firmware après un renouvellement de matériel récent, mais personne n’avait mis à jour le runbook.

La réparation a été simple — signer et enregistrer correctement la clé — mais cela a exigé des redémarrages vers le gestionnaire MOK. Ils ont perdu une journée à coordonner les redémarrages entre nœuds, alors que cela aurait pu être évité avec une porte de pré‑redémarrage et une vérification « DKMS installé pour le noyau le plus récent ».

Mini‑histoire 2 : l’optimisation qui s’est retournée contre eux

Une société financière en avait assez des déploiements de correctifs lents. Ils ont décidé d’« optimiser » en retirant les outils de build des serveurs de production : plus de gcc, plus de make, en‑têtes absents, paquets minimaux seulement. La sécurité appréciait. L’image était plus légère, les scans plus propres, et les serveurs avaient un air d’appliance.

Puis une mise à jour du noyau s’est déployée. DKMS a tenté de reconstruire un module NIC hors‑arbre dont ils dépendaient pour certaines fonctionnalités d’une carte particulière. Pas de compilateur, pas d’en‑têtes, pas de build. DKMS a échoué, mais le noyau courant a continué de tourner. L’échec est resté invisible.

La vague de redémarrages suivante a eu lieu pendant une maintenance électrique du datacenter. Les redémarrages étaient obligatoires. Plusieurs hôtes sont revenus sur le nouveau noyau sans le module NIC. Le pilote intégré suffisait à démarrer, mais il manquait les offloads qu’ils avaient réglés pour la latence. Le symptôme n’était pas « pas de réseau ». C’était pire : effondrement de performance intermittent et timeouts sous charge.

Ils sont revenus sur la décision de l’image minimale pour cette flotte et ont déplacé les builds DKMS dans une pipeline contrôlée : précompiler les modules pour le noyau cible dans un environnement de build, livrer les artefacts et vérifier avant le redémarrage. L’optimisation n’était pas mauvaise en soi. Elle l’était sans remplacer la construction implicite par DKMS par une chaîne d’approvisionnement explicite.

La leçon : si vous retirez les compilateurs des hôtes, vous prenez en charge le processus de build des modules de bout en bout. Sinon, vous ne faites que repousser l’échec au moment du redémarrage.

Mini‑histoire 3 : la pratique ennuyeuse mais correcte qui a sauvé la mise

Une société média exploitait beaucoup de serveurs orientés stockage, certains avec pools ZFS. Leur pratique était douloureusement ennuyeuse : après chaque mise à jour du noyau, un contrôle automatisé de « readiness reboot » était lancé. Il vérifiait l’état DKMS pour ZFS par rapport au noyau le plus récemment installé, confirmait que l’initramfs contenait ZFS, et s’assurait qu’un noyau connu bon restait installé comme rollback.

Un matin, le contrôle a signalé une erreur sur un sous‑ensemble d’hôtes. DKMS indiquait ZFS « built » mais pas « installed » pour le noyau le plus récent. Les hôtes tournaient encore normalement, donc ils n’ont pas paniqué. Ils ont bloqué les redémarrages via leur orchestrateur et ouvert un ticket.

La cause racine était une course entre paquets lors d’une précédente unattended upgrade : la génération d’initramfs est arrivée, puis l’installation DKMS a échoué et a été relancée, laissant un état incohérent. Le contrôle ennuyeux l’a détecté avant tout redémarrage. Ils ont exécuté dkms autoinstall -k, regénéré l’initramfs pour le noyau cible, et levé la porte.

Zéro interruption, pas de drame, pas de week‑end perdu. Voilà à quoi ressemble l’excellence opérationnelle quand on retire le PowerPoint.

Erreurs courantes : symptôme → cause → correctif

1) Symptom: dkms status shows “added” for the new kernel

Cause racine : Le module DKMS est enregistré mais pas construit pour ce noyau ; en‑têtes manquants ou build échoué précédemment.

Correctif : Installez les en‑têtes pour le noyau cible, puis lancez sudo dkms autoinstall -k <kernel>. Validez que les fichiers existent sous /lib/modules/<kernel>/updates/dkms.

2) Symptom: DKMS build fails with “Kernel headers not found”

Cause racine : Paquet linux-headers-<kernel> manquant, ou le lien symbolique /lib/modules/<kernel>/build cassé.

Correctif : Installez les en‑têtes correspondants ; vérifiez que ls -l /lib/modules/<kernel>/build pointe vers les en‑têtes.

3) Symptom: Module builds, but modprobe fails with “Required key not available”

Cause racine : Secure Boot activé ; module non signé ou signé avec une clé non enregistrée.

Correctif : Assurez‑vous que les modules DKMS sont signés avec une clé de confiance et enregistrez‑la via MOK, ou désactivez Secure Boot si la politique le permet.

4) Symptom: Boot into new kernel loses ZFS/root storage

Cause racine : initramfs du nouveau noyau manque les modules requis, souvent à cause d’un timing DKMS ou d’échecs de triggers postinst.

Correctif : Après l’installation DKMS, lancez update-initramfs -u -k <kernel>, puis confirmez avec lsinitramfs.

5) Symptom: DKMS compile errors about missing symbols / implicit declarations

Cause racine : Changement d’API du noyau ; la version du pilote n’est pas compatible avec le nouveau noyau.

Correctif : Mettez à jour le paquet pilote/source du module (par ex. version NVIDIA/ZFS plus récente), ou retenez le noyau et redémarrez sur l’ancien noyau jusqu’à disponibilité des paquets compatibles.

6) Symptom: Everything looks installed, but hardware still doesn’t work after reboot

Cause racine : Vous avez construit pour la mauvaise version de noyau (noyau en cours, pas celui installé le plus récent), ou vous avez démarré un noyau différent de celui attendu.

Correctif : Confirmez les noyaux installés, confirmez la sélection de boot par défaut, reconstruisez explicitement pour le noyau de démarrage avec dkms autoinstall -k.

7) Symptom: Package upgrades hang or leave “half-configured” state

Cause racine : Mise à jour interrompue, contention de verrou dpkg, système de fichiers plein, ou échecs de scripts postinst (souvent DKMS).

Correctif : Réparez l’état dpkg : apt-get -f install, vérifiez l’espace disque, et relancez les builds DKMS après que la couche de packaging soit saine.

Listes de vérification / plan étape par étape

Checklist A: No-downtime recovery on a host still running the old kernel

  1. Identifier le noyau cible (le plus récemment installé) : utilisez dpkg -l pour les images installées.
  2. Vérifier l’état DKMS pour les modules critiques par rapport à ce noyau : dkms status.
  3. Installer les en‑têtes pour le noyau cible si elles manquent : apt-get install linux-headers-<kernel>.
  4. Reconstruire les modules pour le noyau cible : dkms autoinstall -k <kernel>.
  5. Valider la présence des artefacts dans /lib/modules/<kernel>/updates/dkms.
  6. Vérification Secure Boot : mokutil --sb-state et confirmer le signer du module via modinfo.
  7. Regénérer l’initramfs pour le noyau cible (hôtes stockage critique) : update-initramfs -u -k <kernel>.
  8. Vérifier le contenu initramfs : lsinitramfs inclut les modules requis.
  9. Conserver un rollback disponible : confirmer qu’un noyau plus ancien connu bon reste installé.
  10. Planifier le redémarrage avec plan de retour arrière (accès console, sélection GRUB, intervention distante si nécessaire).

Checklist B: If you already rebooted into the broken kernel

  1. Confirmer ce qui manque : lsmod, modprobe, et dmesg.
  2. Vérifier Secure Boot immédiatement ; ne perdez pas de temps à reconstruire des modules non signés si Secure Boot va les bloquer.
  3. Installer les prérequis de build (temporairement) : en‑têtes, toolchain compilateur si DKMS en a besoin.
  4. Reconstruire DKMS pour le noyau en cours : dkms autoinstall -k $(uname -r).
  5. Si le build échoue à cause d’un décalage d’API : arrêtez et choisissez une version de pilote compatible ou redémarrez sur le noyau précédent via GRUB.
  6. Corriger l’initramfs si des modules critiques au démarrage sont impliqués, puis tester le redémarrage.

Checklist C: Prevent it next time (production hygiene)

  1. Créer une « porte de redémarrage » qui vérifie que DKMS est installé pour le noyau le plus récent et valide l’initramfs si nécessaire.
  2. Stager les mises à jour du noyau sur des hôtes canaris avec du matériel représentatif.
  3. Traquer la politique Secure Boot comme une contrainte de première classe, pas une note de BIOS.
  4. Garder au moins un noyau de rollback installé et bootable en permanence.
  5. Contrôler les unattended upgrades pour que les noyaux ne changent pas sans validation.

FAQ

1) Pourquoi DKMS « s’est cassé » seulement après la mise à jour du noyau ?

Parce que les modules DKMS sont compilés contre les en‑têtes d’une version précise du noyau. Quand le noyau change, le module doit être reconstruit. Si cette reconstruction échoue, vous ne le remarquerez pas tant que vous ne redémarrerez pas sur le nouveau noyau ou que vous n’essayerez pas de charger le module pour celui‑ci.

2) Puis‑je réparer DKMS sans redémarrer ?

Vous pouvez reconstruire et installer des modules pour le noyau suivant sans redémarrer, oui. Vous ne pouvez généralement pas tester leur chargement dans ce noyau suivant sans réellement le démarrer. C’est pourquoi vous validez les artefacts, les signatures et le contenu de l’initramfs avant le redémarrage.

3) Que signifient « added » vs « built » vs « installed » dans dkms status ?

added : DKMS connaît la source du module mais ne l’a pas construite pour ce noyau. built : compilé mais pas nécessairement installé dans l’arbre de modules du noyau. installed : placé dans /lib/modules/<kernel> et depmod a été (ou devrait être) exécuté.

4) Ai‑je vraiment besoin des en‑têtes correspondants au noyau ?

Oui. DKMS construit contre les en‑têtes pour la version de noyau ciblée. Il n’existe pas de « assez proche » ; installez linux-headers-<version-exacte>.

5) Pourquoi Secure Boot rend‑il cela tellement pire ?

Parce qu’il transforme un problème de compilation en un problème d’application à l’exécution. Vous pouvez construire le module avec succès et quand même être incapable de le charger. Le noyau rejettera les modules non signés par une clé de confiance quand Secure Boot et les politiques lockdown l’exigent.

6) Si Secure Boot est activé, dois‑je le désactiver ?

Uniquement si votre politique le permet. Désactiver Secure Boot peut être la solution opérationnellement la plus simple, mais la bonne correction dans des environnements régulés est de signer les modules DKMS avec une clé que vous contrôlez et de l’enregistrer via MOK.

7) Pourquoi mon système a‑t‑il démarré mais le stockage ou le réseau était cassé ensuite ?

Souvent parce que le pilote est chargé plus tard que vous ne le pensez, ou qu’un pilote intégré de secours existe mais n’a pas les fonctionnalités. Une autre cause fréquente : l’initramfs manque un module nécessaire tôt, donc le démarrage réussit partiellement, puis les périphériques apparaissent tard ou incorrectement.

8) Quel est le rollback le plus sûr si je ne peux pas faire construire DKMS pour le nouveau noyau ?

Démarrez sur le noyau précédent connu bon et mettez temporairement en attente les méta‑paquets noyau. Ensuite mettez à jour le paquet pilote/module vers une version qui prend en charge le nouveau noyau avant de retenter le redémarrage.

9) Dois‑je garder les compilateurs hors des serveurs de production ?

Ça dépend. Si vous vous appuyez sur des builds DKMS sur la machine hôte, vous avez besoin de la toolchain de build et des en‑têtes. Si vous les retirez, vous devez remplacer la construction locale DKMS par une pipeline qui produit et distribue des modules compatibles pour chaque noyau déployé.

10) Comment éviter que des noyaux « piège de reboot » s’accumulent ?

Mettez en place une étape de validation après l’installation du noyau qui vérifie l’état DKMS pour les modules critiques sur le noyau le plus récent installé. Si elle échoue, bloquez l’automatisation des redémarrages et alertez. C’est moins coûteux que la réponse à incident.

Prochaines étapes à effectuer aujourd’hui

Si vous exécutez Ubuntu 24.04 avec des pilotes gérés par DKMS, cessez de traiter les mises à jour du noyau comme « juste des patches de sécurité ». Ce sont aussi des événements de reconstruction de pilotes. Le chemin pratique vers zéro interruption est court :

  1. Choisissez vos modules DKMS critiques par rôle d’hôte (stockage, réseau, GPU).
  2. Après chaque installation de noyau, reconstruisez les modules pour le noyau le plus récemment installé (dkms autoinstall -k).
  3. Validez les signatures si Secure Boot est activé ; ne supposez pas qu’un build réussi équivaut à un chargement réussi.
  4. Regénérez et vérifiez l’initramfs pour les modules nécessaires au démarrage précoce.
  5. Only then reboot. Keep a rollback kernel installed and bootable.

Faites cela, et « DKMS cassé après mise à jour du noyau » cessera d’être un incident. Ce deviendra un élément de checklist terminé avant que quelqu’un ne le remarque.

Ubuntu 24.04 : disque « plein » mais df indique de l’espace — explication et correction de l’épuisement des inodes

Vous êtes connecté par SSH à un serveur Ubuntu 24.04 qui « a plein d’espace » selon df -h.
Pourtant, chaque déploiement échoue, les logs ne tournent plus, apt ne peut pas dépaqueter, et le noyau continue de lancer
No space left on device comme s’il était payé à l’erreur.

C’est l’un de ces incidents qui fait douter les personnes les plus compétentes. Le disque n’est pas plein.
Quelque chose d’autre l’est. La plupart du temps : les inodes. Et une fois que vous comprenez ce que cela signifie, la correction est presque ennuyeuse.
Presque.

Ce que vous voyez : « disque plein » avec des Go libres

Sous Ubuntu, « disque plein » signifie souvent une des trois choses suivantes :

  • Épuisement de l’espace en blocs : le cas classique ; vous avez manqué d’octets.
  • Épuisement des inodes : vous avez manqué d’entrées de métadonnées pour les fichiers ; des octets peuvent rester disponibles.
  • Blocs réservés, quotas ou corruption du système de fichiers : vous avez de l’espace mais vous ne pouvez pas l’utiliser.

L’épuisement des inodes est sournois parce qu’il n’apparaît pas dans la première commande que tout le monde lance.
df sans options ne rapporte que l’utilisation des blocs. Vous pouvez avoir 200 Go libres et ne pas pouvoir
créer un fichier de 0 octet. Le système de fichiers ne peut pas allouer un nouvel inode, il ne peut donc pas créer un nouveau fichier.

Vous remarquerez des effets secondaires étranges :

  • De nouveaux fichiers de journal ne peuvent pas être créés, donc les services plantent ou arrêtent d’écrire les logs au pire moment.
  • apt échoue en cours d’installation parce qu’il ne peut pas créer de fichiers temporaires ou dépaqueter des archives.
  • Les builds Docker commencent à échouer sur des opérations « writing layer » même si les volumes semblent corrects.
  • Certaines applications signalent « disque plein » tandis que d’autres continuent de fonctionner (parce qu’elles ne créent pas de fichiers).

Il y a un test simple : essayez de créer un fichier sur le système de fichiers affecté. S’il échoue avec « No space left »
alors que df -h montre de l’espace libre, arrêtez de discuter avec df et vérifiez les inodes.

Les inodes expliqués comme si vous gériez la production

Un inode est l’enregistrement du système de fichiers pour un fichier ou un répertoire. C’est de la métadonnée : propriétaire, permissions, horodatages,
taille, pointeurs vers les blocs de données. Dans de nombreux systèmes de fichiers, le nom de fichier n’est pas stocké dans l’inode ; il vit
dans les entrées de répertoire qui associent les noms aux numéros d’inode.

La vérité opérationnelle importante : la plupart des systèmes Linux ont deux « budgets » séparés :
les blocs (octets) et les inodes (nombre de fichiers). Si vous épuisez l’un ou l’autre, c’est fini.

Pourquoi les inodes s’épuisent dans les systèmes réels

L’épuisement des inodes n’est généralement pas dû à « beaucoup de gros fichiers ». C’est dû à « des millions de petits fichiers ».
Pensez caches, spools de mail, artefacts de build, couches de conteneurs, espaces de travail CI, téléchargements temporaires,
et tampons de métriques que quelqu’un a oublié d’expirer.

Un fichier de 1 Ko coûte toujours un inode. Un fichier de 0 octet coûte toujours un inode. Un répertoire coûte aussi un inode.
Quand vous avez 12 millions de petits fichiers, le disque peut être largement vide en octets, mais la table d’inodes est cuite.

Quels systèmes de fichiers sont les plus susceptibles de vous mordre

  • ext4 : courant sur Ubuntu ; les inodes sont généralement créés au formatage selon un ratio d’inodes. Si vous vous trompez, vous pouvez tomber en panne.
  • XFS : les inodes sont plus dynamiques ; l’épuisement des inodes est moins courant mais pas mythique.
  • btrfs : l’allocation des métadonnées est différente ; vous pouvez toujours rencontrer des problèmes d’espace métadonnées, mais ce n’est pas la même histoire d’« nombre d’inodes fixe ».
  • overlayfs (Docker) : ce n’est pas un type de système de fichiers à part entière, mais il amplifie le comportement « beaucoup de fichiers » sur des hôtes riches en conteneurs.

Une citation à garder dans votre runbook mental :

« L’espoir n’est pas une stratégie. » — General Gordon R. Sullivan

Playbook de diagnostic rapide (premier/deuxième/troisième)

Quand l’alerte dit « No space left on device » mais que les graphiques indiquent que vous êtes OK, ne vous dispersez pas.
Suivez le playbook.

Premier : confirmez ce que « espace » signifie réellement

  1. Vérifiez les blocs (df -h) et les inodes (df -i) pour le point de montage affecté.
  2. Essayez de créer un fichier sur ce point de montage ; confirmez le chemin d’erreur.
  3. Vérifiez un remontage en lecture seule ou des erreurs de système de fichiers dans dmesg.

Deuxième : trouvez le montage et les plus gros consommateurs d’inodes

  1. Identifiez quel chemin du système de fichiers échoue (logs, tmp, répertoire de données).
  2. Trouvez les répertoires avec un grand nombre de fichiers en utilisant find et quelques comptes ciblés.
  3. Si c’est Docker/Kubernetes, vérifiez overlay2, images, containers et logs.

Troisième : libérez des inodes en toute sécurité

  1. Commencez par des nettoyages sûrs et évidents : anciens logs, caches, fichiers temporaires, vacuum du journal, garbage des conteneurs.
  2. Supprimez des fichiers, pas des répertoires, si l’application attend une structure de répertoire.
  3. Confirmez que l’utilisation des inodes baisse (df -i) et que les services se rétablissent.

Vous n’avez pas besoin d’actions héroïques. Vous avez besoin d’un plan de suppression contrôlé et d’un postmortem qui empêche que cela se reproduise.

Tâches pratiques : commandes, sens des sorties, décisions

Ci‑dessous des tâches réelles que vous pouvez exécuter sur Ubuntu 24.04. Chacune inclut quoi surveiller et quelle décision prendre.
Faites-les dans l’ordre si vous êtes en astreinte ; piochez si vous connaissez déjà le montage.

Tâche 1 : Confirmer l’utilisation des blocs (la vérification évidente)

cr0x@server:~$ df -hT
Filesystem     Type   Size  Used Avail Use% Mounted on
/dev/sda2      ext4   200G   62G  128G  33% /
tmpfs          tmpfs  3.1G  1.2M  3.1G   1% /run
/dev/sdb1      ext4   1.8T  1.1T  648G  63% /data

Sens : l’espace en blocs est correct sur / et /data.
Décision : Si vous recevez toujours « No space left », passez aux vérifications d’inodes. Ne perdez pas de temps à chasser des gros fichiers pour l’instant.

Tâche 2 : Vérifier l’utilisation des inodes (la moitié des cas)

cr0x@server:~$ df -iT
Filesystem     Type    Inodes  IUsed   IFree IUse% Mounted on
/dev/sda2      ext4   13107200 13107190     10  100% /
tmpfs          tmpfs    790000     420 789580    1% /run
/dev/sdb1      ext4  122142720  982134 121160586   1% /data

Sens : le système de fichiers racine a IUse% 100%. Il ne reste que 10 inodes libres. C’est terminé pour la création de fichiers.
Décision : Vous devez supprimer des fichiers sur / (ou les déplacer) pour libérer des inodes. Supprimer un gros fichier ne suffit pas si vous avez besoin de libérer de nombreux inodes.

Tâche 3 : Reproduire l’échec de façon contrôlée

cr0x@server:~$ touch /tmp/inode-test-file
touch: cannot touch '/tmp/inode-test-file': No space left on device

Sens : le système de fichiers ne peut pas allouer un inode pour un petit fichier.
Décision : Traitez cela comme un incident de disponibilité. Tout ce qui doit écrire (logs, sockets, fichiers PID, temporaires) peut échouer ensuite.

Tâche 4 : Identifier quel chemin est sur le système de fichiers impacté

cr0x@server:~$ findmnt -T /var/log
TARGET SOURCE    FSTYPE OPTIONS
/      /dev/sda2 ext4   rw,relatime,errors=remount-ro

Sens : /var/log se trouve sur /. Si les logs ont explosé en millions de fichiers, voilà votre champ de bataille.
Décision : Concentrez votre recherche sous /var, /tmp, /var/lib et tous les répertoires d’applications sur /.

Tâche 5 : Repérer les suspects habituels par taille (octets)

cr0x@server:~$ sudo du -xh --max-depth=1 /var | sort -h
12M	/var/cache
180M	/var/log
2.1G	/var/lib
2.4G	/var

Sens : l’utilisation en octets n’est pas extrême. C’est votre premier indice que le nombre de fichiers, pas la taille, est le problème.
Décision : Arrêtez d’optimiser pour les Go. Commencez à optimiser pour le nombre de fichiers.

Tâche 6 : Trouver les répertoires avec un énorme nombre de fichiers (balayage de haut niveau)

cr0x@server:~$ sudo bash -lc 'for d in /var/* /tmp /home; do [ -d "$d" ] && printf "%s\t" "$d" && find "$d" -xdev -type f 2>/dev/null | wc -l; done | sort -n -k2 | tail -n 10'
/var/cache	1320
/var/log	5402
/var/lib	12877190
/tmp	120
/home	88

Sens : /var/lib contient ~12,8 millions de fichiers. Ce n’est pas « un peu désordonné » ; c’est vos inodes.
Décision : Zoomez dans /var/lib. Si c’est un hôte conteneur, attendez-vous à /var/lib/docker ou /var/lib/containerd.

Tâche 7 : Réduire rapidement à l’intérieur de /var/lib

cr0x@server:~$ sudo bash -lc 'for d in /var/lib/*; do [ -d "$d" ] && printf "%s\t" "$d" && find "$d" -xdev -type f 2>/dev/null | wc -l; done | sort -n -k2 | tail -n 10'
/var/lib/systemd	2200
/var/lib/dpkg	9800
/var/lib/docker	12866012

Sens : Docker dévore votre budget d’inodes via les couches, le cache de build et les logs de conteneurs.
Décision : Décidez si vous pouvez exécuter un prune en toute sécurité maintenant. Si cet hôte est dédié à une seule tâche, le prune est généralement correct. Si c’est un serveur « pet » avec des charges inconnues, soyez plus prudent.

Tâche 8 : Vérifier la vue de Docker (octets, pas inodes, mais utile)

cr0x@server:~$ sudo docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          48        12        23.4GB    18.1GB (77%)
Containers      65        18        4.2GB     2.9GB (69%)
Local Volumes   26        20        11.8GB    1.1GB (9%)
Build Cache     152       0         6.4GB     6.4GB

Sens : Il y a beaucoup à supprimer, mais cette sortie ne montre pas le nombre de fichiers.
Décision : Procédez aux actions de prune si vous comprenez l’impact : conteneurs arrêtés, images pendantes, cache de build.

Tâche 9 : Pruner Docker en toute sécurité (commencez conservateur)

cr0x@server:~$ sudo docker image prune -a -f
Deleted Images:
deleted: sha256:1a2b3c...
deleted: sha256:4d5e6f...
Total reclaimed space: 17.8GB

Sens : Les images inutilisées ont été supprimées ; le nombre d’inodes devrait aussi diminuer car les couches contiennent de nombreux petits fichiers.
Décision : Revérifiez df -i. Si l’utilisation des inodes ne baisse pas assez, supprimez ensuite les conteneurs et le cache de build.

Tâche 10 : Pruner le build cache (souvent une usine silencieuse d’inodes)

cr0x@server:~$ sudo docker builder prune -a -f
Deleted build cache objects:
k2r1m3n4o5p6...
Total reclaimed space: 6.4GB

Sens : Les hôtes CI/build laissent souvent des fragments de cache derrière eux.
Décision : Si vous avez besoin de builds reproductibles et de rebuilds rapides, planifiez ce nettoyage plutôt que de l’exécuter constamment. Pour l’instant, vous stabilisez le nœud.

Tâche 11 : Confirmer la récupération des inodes (c’est votre métrique de succès)

cr0x@server:~$ df -i /
Filesystem     Inodes   IUsed    IFree IUse% Mounted on
/dev/sda2     13107200 8123400  4983800   63% /

Sens : Vous êtes revenu de la falaise. Les services peuvent créer des fichiers à nouveau.
Décision : Redémarrez les services qui ont planté à cause des écritures échouées, et mettez en place des garde-fous pour éviter une récidive.

Tâche 12 : Si les logs sont en cause, nettoyez et limitez (systemd-journald)

cr0x@server:~$ sudo journalctl --disk-usage
Archived and active journals take up 1.7G in the file system.

Sens : Les journaux ne sont pas nécessairement lourds en inodes (ce sont généralement des fichiers moins nombreux et plus volumineux), mais ils peuvent quand même peser.
Décision : Si les octets importent aussi, vacuum. Si ce sont les inodes qui importent, concentrez-vous sur les applications qui créent de nombreux fichiers de logs séparés, pas sur le journal lui‑même.

cr0x@server:~$ sudo journalctl --vacuum-time=7d
Vacuuming done, freed 1.2G of archived journals from /var/log/journal.

Sens : Octets libérés. Inodes libérés de façon modeste seulement.
Décision : Définissez des limites persistantes dans la config de journald si cet hôte est sujet à des logs bruyants.

Tâche 13 : Si apt échoue, nettoyez le cache des paquets

cr0x@server:~$ sudo apt-get clean

Sens : Supprime les archives de paquets téléchargés sous /var/cache/apt/archives.
Décision : Bonne hygiène, mais rarement une solution miracle pour les inodes. Aide lorsque les caches contiennent beaucoup de petits fichiers partiels.

Tâche 14 : Trouver les répertoires à grand nombre de fichiers avec du (style inode)

cr0x@server:~$ sudo du -x --inodes --max-depth=2 /var/lib | sort -n | tail -n 10
1200	/var/lib/systemd
9800	/var/lib/dpkg
12866012	/var/lib/docker
12877190	/var/lib

Sens : C’est la vue qui compte : consommation d’inodes par répertoire.
Décision : Ciblez le plus gros consommateur. Ne faites pas « un peu de nettoyage partout ». Vous perdrez du temps et serez toujours à 100%.

Tâche 15 : En cas de doute, inspectez la fan-out pathologique

cr0x@server:~$ sudo find /var/lib/docker -xdev -type f -printf '%h\n' 2>/dev/null | sort | uniq -c | sort -n | tail -n 5
  42000 /var/lib/docker/containers/8a7b.../mounts
  78000 /var/lib/docker/overlay2/3f2d.../diff/usr/lib
 120000 /var/lib/docker/overlay2/9c1e.../diff/var/cache
 250000 /var/lib/docker/overlay2/b7aa.../diff/usr/share
 980000 /var/lib/docker/overlay2/2d9b.../diff/node_modules

Sens : Une couche de conteneur avec node_modules peut générer des nombres de fichiers absurdes.
Décision : Corrigez le build (build multi‑étapes, suppression des dépendances dev, .dockerignore) et/ou déplacez le data root de Docker vers un système de fichiers conçu pour cette charge.

Tâche 16 : Confirmer le type de système de fichiers et les détails de provisioning des inodes

cr0x@server:~$ sudo tune2fs -l /dev/sda2 | egrep -i 'Filesystem features|Inode count|Inode size|Block count|Reserved block count'
Filesystem features:      has_journal ext_attr resize_inode dir_index filetype extent 64bit flex_bg sparse_super large_file huge_file dir_nlink extra_isize metadata_csum
Inode count:              13107200
Inode size:               256
Block count:              52428800
Reserved block count:     2621440

Sens : ext4 a un nombre d’inodes fixe ici. Vous ne pouvez pas « ajouter plus d’inodes » sans reconstruire le système de fichiers.
Décision : Si la mission de cet hôte est « millions de petits fichiers », planifiez une migration : nouveau système de fichiers avec un ratio d’inodes différent, ou un agencement de stockage différent.

Blague #1 : Les inodes sont comme des salles de réunion : vous pouvez avoir un immeuble vide et être « plein » si chaque salle est occupée par un petit post‑it.

Trois mini-récits d’entreprise (anonymisés, douloureusement réels)

1) Incident causé par une mauvaise hypothèse : « df dit que tout va bien »

Une entreprise SaaS de taille moyenne gérait une flotte de serveurs Ubuntu traitant l’ingestion de webhooks. Les ingénieurs surveillaient l’utilisation du disque en pourcentage,
déclenchaient des alertes à 80%, et étaient fiers : « Nous ne remplissons plus les disques ». Un mardi après‑midi, le pipeline d’ingestion a commencé à renvoyer
des 500 intermittents. Les retries s’accumulaient, les files d’attente se remplissaient, les dashboards s’enflammaient.

L’astreignant a lancé la routine standard : df -h semblait sain. Le CPU allait bien. La mémoire n’était pas excellente mais tenable.
Ils ont redémarré un service et il est mort immédiatement parce qu’il ne pouvait pas créer un fichier PID. Ce message d’erreur s’est finalement montré :
No space left on device.

Quelqu’un a suggéré « peut‑être que le disque a menti », ce qui est une façon humaine et charmante de dire « nous ne mesurions pas la bonne chose ».
Ils ont exécuté df -i et ont trouvé le système de fichiers racine à 100% d’utilisation des inodes. Le coupable n’était pas une base de données.
C’était un « magasin de retries temporaire » implémenté comme un fichier JSON par événement webhook sous /var/lib/app/retry/.
Chaque fichier était minuscule. Il y en avait des millions.

Le correctif immédiat a été simple : supprimer les fichiers plus anciens qu’un seuil et redémarrer. Le vrai correctif a pris un sprint :
migrer le magasin de retries vers une file conçue pour cela, cesser d’utiliser le système de fichiers comme une base de données à bas coût, et ajouter des alertes sur les inodes.
Le titre du postmortem était poli. Le chat interne, moins.

2) Optimisation qui a échoué : « tout mettre en cache sur le disque local »

Une équipe de data engineering a accéléré des jobs ETL en mettant en cache des artefacts intermédiaires sur le SSD local.
Ils sont passés de « un artefact par batch » à « un artefact par partition », pour la parallélisation.
Les performances ont augmenté. Les coûts semblaient meilleurs. Tout le monde a continué sa vie.

Des semaines plus tard, des nœuds ont commencé à tomber de façon échelonnée. Pas tous en même temps, ce qui rendait le diagnostic plus difficile.
Certains jobs réussissaient, d’autres échouaient aléatoirement en essayant d’écrire la sortie. Les erreurs étaient inconsistantes :
exceptions Python, erreurs Java IO, ou parfois « filesystem en lecture seule » après que le noyau ait remonté un disque en difficulté.

La cause racine était mécaniquement embarrassante : le cache a créé des dizaines de millions de petits fichiers.
Le système ext4 avait été formaté avec un ratio d’inodes par défaut adapté à un usage général, pas à des « millions de shards ».
Les nœuds n’ont pas manqué d’octets ; ils ont manqué d’identités de fichiers. L’« optimisation » était en fait un test de stress des inodes.

Ils ont « corrigé » en augmentant la taille du disque. Ça n’a pas aidé, car le nombre d’inodes restait fixé.
Ils ont ensuite reformatté avec une densité d’inodes appropriée et changé la stratégie de cache pour empaqueter des partitions dans des bundles de type tar.
Les performances ont légèrement régressé. La fiabilité s’est améliorée considérablement. C’est le compromis à accepter.

3) Pratique ennuyeuse mais correcte qui a sauvé la journée : fichiers séparés et garde-fous

Une autre organisation exécutait des charges mixtes sur des nœuds Kubernetes : services système, Docker/containerd, et un espace de travail local.
Ils avaient une règle : tout ce qui peut exploser en nombre de fichiers obtient son propre système de fichiers. Docker vivait sur /var/lib/docker
monté depuis un volume dédié. L’espace scratch était sur un montage séparé avec des politiques de nettoyage agressives.

Ils avaient aussi deux moniteurs ennuyeux : « utilisation des blocs » et « utilisation des inodes ». Pas de ML sophistiqué. Juste deux séries temporelles et des alertes
qui envoyaient une page avant la falaise. Ils testaient les alertes trimestriellement en créant une tempête d’inodes en staging (oui, ça existe).

Un jour, une nouvelle pipeline de build a commencé à produire des couches pathologiques avec d’énormes arbres de dépendances.
Les inodes du volume Docker ont monté rapidement. L’alerte a tiré tôt. L’astreignant n’a pas eu à apprendre quoi que ce soit de nouveau sous pression.
Ils ont pruné, rollbacké la pipeline et relevé les limites. Le reste du nœud est resté sain parce que le racine n’était pas impliqué.

Le rapport d’incident était court. Le correctif était ennuyeux. Tout le monde a dormi.
C’est tout l’intérêt du SRE.

Faits intéressants et un peu d’histoire (parce que ça explique les modes de panne)

  • Les inodes datent de l’Unix primitif : le concept remonte à la conception du système de fichiers Unix original, où métadonnées et blocs de données étaient des structures séparées.
  • Les ext traditionnels préallouent les inodes : ext2/ext3/ext4 décident typiquement du nombre d’inodes au moment de mkfs en fonction d’un ratio, pas dynamiquement selon la charge.
  • Les ratios d’inodes par défaut sont un compromis : ils visent des charges générales ; ils ne sont pas adaptés aux couches de conteneurs, caches CI ou explosions de maildir.
  • Les répertoires coûtent aussi des inodes : « nous avons seulement créé des répertoires » n’est pas une défense ; chaque répertoire consomme aussi un inode.
  • « No space left on device » couvre plusieurs raisons : la même chaîne d’erreur peut signifier manque de blocs, manque d’inodes, quota dépassé, ou même système de fichiers passé en lecture seule après des erreurs.
  • Les blocs réservés existent pour une raison : ext4 réserve généralement un pourcentage des blocs pour root, destiné à garder le système utilisable sous pression ; il ne réserve pas les inodes de la même façon.
  • Les charges de petits fichiers sont plus difficiles qu’elles en ont l’air : les opérations de métadonnées dominent ; l’efficacité des inodes et des recherches de répertoire peut compter plus que le débit.
  • Les images de conteneurs amplifient les schémas de petits fichiers : les écosystèmes de langage avec de grands arbres de dépendances (Node, Python, Ruby) peuvent créer des couches avec un nombre massif de fichiers.
  • Certaines filesystems ont évolué vers des métadonnées dynamiques : XFS et btrfs gèrent différemment les métadonnées, ce qui change la nature des pannes « full », mais ne les élimine pas.

Corrections : du nettoyage rapide à la prévention permanente

Stabilisation immédiate (minutes) : libérer des inodes sans empirer la situation

Votre travail pendant un incident n’est pas « rendre propre ». C’est « rendre le système réinscriptible » sans supprimer la mauvaise chose.
Voici ce qui tend à être sûr et efficace, par ordre décroissant de raison :

  • Supprimez les caches éphémères connus (cache de build, cache de paquets, fichiers temporaires) avec les commandes adaptées.
  • Prunez les ordures des conteneurs si l’hôte est centré conteneurs et que vous pouvez tolérer la suppression d’artefacts inutilisés.
  • Expirez les fichiers générés par l’application selon l’âge, pas au doigt mouillé. Préférez des politiques « plus vieux que N jours ».
  • Déplacez des répertoires hors du système de fichiers si la suppression est risquée : archivez vers un autre montage, puis supprimez localement.

Quand c’est lié aux logs : corrigez le problème de nombre de fichiers, pas seulement la rotation

Logrotate résout « un fichier qui grossit indéfiniment ». Il ne résout pas automatiquement « nous créons un fichier par requête ».
Si votre application crée des fichiers de log uniques par unité de travail (ID de requête, ID de job, ID de client), vous êtes en train d’attaquer votre propre table d’inodes en DDOS.

Préférez :

  • un flux de logs unique avec champs structurés (JSON suffit, gardez‑le raisonnable)
  • l’intégration à journald quand c’est approprié
  • du spool local borné avec rétention explicite

Quand c’est Docker : choisissez un data root adapté à la charge

Docker sur ext4 peut très bien fonctionner, jusqu’à ce que non. Si vous savez qu’un nœud va construire des images, exécuter de nombreux conteneurs,
et chasser des couches, traitez /var/lib/docker comme un datastore à fort churn et donnez‑lui son propre système de fichiers.

Options pratiques :

  • Montage séparé pour /var/lib/docker avec une densité d’inodes adaptée au nombre de fichiers attendu.
  • Nettoyage régulier du cache de build, programmé, pas manuel en urgence.
  • Corriger les builds d’images pour réduire la fan‑out des fichiers : builds multi‑étapes, réduire les dépendances, utiliser .dockerignore.

Correctif permanent : concevoir le système de fichiers pour la charge

Si l’épuisement des inodes revient, vous n’avez pas un « problème de nettoyage ». Vous avez un problème de capacity planning.
Sur ext4, le nombre d’inodes est fixé à la création. La seule vraie solution est de migrer vers un système de fichiers avec plus d’inodes
(ou un agencement différent), ce qui implique :

  • créer un nouveau système de fichiers avec une densité d’inodes plus élevée
  • déplacer les données
  • mettre à jour les mounts et les services
  • ajouter surveillance et politiques de rétention

Comment créer un ext4 avec plus d’inodes (migration planifiée)

Le nombre d’inodes sur ext4 est influencé par -i (octets par inode) et -N (nombre d’inodes explicite).
Baisser les octets par inode signifie plus d’inodes. Plus d’inodes signifie plus de surcharge en métadonnées. C’est un compromis, pas du gratuit.

cr0x@server:~$ sudo mkfs.ext4 -i 16384 /dev/sdc1
mke2fs 1.47.0 (5-Feb-2023)
Creating filesystem with 976754176 4k blocks and 61079552 inodes
Filesystem UUID: 9f1f4a1c-8b1d-4c1b-9d88-8d1aa14d4e1e
Superblock backups stored on blocks:
	32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632
Allocating group tables: done
Writing inode tables: done
Creating journal (262144 blocks): done
Writing superblocks and filesystem accounting information: done

Sens : Cet exemple crée bien plus d’inodes qu’un ratio par défaut le ferait sur le même volume.
Décision : N’utilisez ceci que lorsque vous savez que vous avez besoin de beaucoup de fichiers. Pour de grosses données séquentielles, ne gaspillez pas de métadonnées.

Blague #2 : Si vous traitez le système de fichiers comme une base de données, il finira par vous facturer en inodes.

Erreurs courantes : symptôme → cause racine → correctif

1) « df montre 30% utilisé mais je ne peux pas écrire » → épuisement d’inodes → vérifiez et supprimez les points chauds de petits fichiers

  • Symptôme : les écritures échouent, touch échoue, apt échoue, les services plantent en créant des fichiers temporaires.
  • Cause racine : df -i montre 100% d’inodes utilisés sur le montage.
  • Fix : identifiez les répertoires à fort nombre de fichiers avec du --inodes / find ... | wc -l, supprimez les fichiers éphémères sûrs, puis empêchez la récidive.

2) « J’ai supprimé un gros fichier, c’est toujours cassé » → vous avez libéré des blocs, pas des inodes → supprimez beaucoup de fichiers

  • Symptôme : les Go libres augmentent, mais « No space left » continue.
  • Cause racine : l’utilisation des inodes n’a pas changé.
  • Fix : libérez des inodes en supprimant des nombres de fichiers, pas des tailles de fichiers. Ciblez caches, spools, artefacts de build.

3) « Seul root peut écrire ; les utilisateurs ne peuvent pas » → blocs réservés ou quotas → vérifiez avec tune2fs et les outils de quota

  • Symptôme : root peut créer des fichiers, non-root ne peut pas.
  • Cause racine : pourcentage de blocs réservés sur ext4, ou quotas utilisateurs atteints.
  • Fix : vérifiez les blocs réservés avec tune2fs ; vérifiez les quotas ; ajustez avec précaution. Ne mettez pas aveuglément les blocs réservés à 0% sur les partitions système.

4) « Il est passé en lecture seule et maintenant tout échoue » → erreurs système de fichiers → inspectez dmesg, exécutez fsck (hors ligne)

  • Symptôme : le noyau a remonté errors=remount-ro, les écritures échouent avec des erreurs en lecture seule.
  • Cause racine : erreurs d’E/S ou corruption du système de fichiers, pas la capacité.
  • Fix : inspectez dmesg ; planifiez un reboot en recovery et lancez fsck. Le nettoyage de capacité ne réglera pas la corruption.

5) « Le nœud Kubernetes a DiskPressure mais df semble ok » → pression d’inodes du runtime des conteneurs → prune et montages séparés

  • Symptôme : pods évincés ; kubelet se plaint ; nœud instable.
  • Cause racine : répertoires du runtime remplissent le budget d’inodes (overlay2, logs).
  • Fix : prune du stockage runtime, appliquez la garbage collection d’images, mettez le runtime sur un volume dédié avec surveillance.

6) « Nous avons nettoyé /tmp ; ça a tenu une heure » → l’application recrée la tempête → corrigez la rétention à la source

  • Symptôme : incidents d’inodes répétés après nettoyage.
  • Cause racine : bug applicatif, mauvais design (un fichier par événement), ou TTL/rotation manquants.
  • Fix : ajoutez une politique de rétention, redesign du stockage (base de données/file d’attente/object store), imposer des limites et des alertes.

Listes de contrôle / plan étape par étape

Checklist d’astreinte (stabiliser en 15–30 minutes)

  1. Exécutez df -hT et df -iT pour le montage défaillant.
  2. Confirmez avec touch dans le chemin affecté.
  3. Trouvez le mapping du montage avec findmnt -T.
  4. Identifiez les gros consommateurs d’inodes avec du -x --inodes --max-depth=2 et des find ... | wc -l ciblés.
  5. Choisissez une action de nettoyage sûre et à fort impact (Docker prune, nettoyage de cache, suppression basée sur l’âge).
  6. Revérifiez df -i jusqu’à être sous ~90% sur le montage critique.
  7. Redémarrez les services impactés (seulement après que les écritures réussissent).
  8. Capturez des preuves : commandes exécutées, comptages d’inodes avant/après, répertoires responsables.

Checklist ingénierie (prévenir la récidive)

  1. Ajoutez la surveillance et les alertes d’inodes par système de fichiers (pas seulement l’utilisation disque en %).
  2. Mettez les répertoires à fort churn sur des montages dédiés : /var/lib/docker, spool d’app, cache de build.
  3. Implémentez la rétention côté producteur : TTL, limites, compactage périodique, ou backend de stockage différent.
  4. Revoyez les builds et images : réduisez le nombre de fichiers par couche ; évitez d’emballer d’énormes arbres de dépendances dans les images runtime.
  5. Si ext4 est utilisé pour des charges de petits fichiers, planifiez la densité d’inodes lors du formatage et documentez le choix.
  6. Faites un game day en staging : simulez la pression d’inodes et validez les alertes et les étapes de reprise.

Checklist de migration (quand le nombre d’inodes ext4 est fondamentalement mauvais)

  1. Mesurez le nombre actuel de fichiers et le taux de croissance (nouveaux fichiers quotidiens, comportement de rétention).
  2. Choisissez la cible : ext4 avec densité d’inodes plus élevée, XFS, ou une architecture différente (stockage objet, base de données, file d’attente).
  3. Provisionnez un nouveau volume et système de fichiers ; montez‑le à l’emplacement prévu.
  4. Arrêtez la charge, copiez les données (préservant propriétaires/permissions), validez, puis basculez.
  5. Réactivez la charge avec des valeurs par défaut de rétention activées dès le départ.
  6. Laissez des garde-fous : alertes, timers de nettoyage et politique de cap stricte.

FAQ

1) Qu’est‑ce qu’un inode en une phrase ?

Un inode est un enregistrement de métadonnées du système de fichiers qui représente un fichier ou un répertoire ; il faut un inode libre pour créer un nouveau fichier.

2) Pourquoi Ubuntu affiche « No space left on device » alors que df -h montre de l’espace libre ?

Parce que « espace » peut signifier des octets (blocs) ou des métadonnées de fichiers (inodes). df -h montre les blocs ; df -i montre les inodes.

3) Comment confirmer rapidement un épuisement d’inodes ?

Exécutez df -i sur le montage et vérifiez la présence de IUse% 100%, puis essayez touch pour confirmer que la création de fichiers échoue.

4) Puis‑je augmenter le nombre d’inodes sur un ext4 existant ?

Pas de manière pratique en ligne. Le nombre d’inodes ext4 est effectivement défini à la création du système de fichiers. La vraie solution est de migrer vers un nouveau système de fichiers avec plus d’inodes.

5) Pourquoi les conteneurs rendent-ils les problèmes d’inodes plus probables ?

Les couches d’images et les arbres de dépendances peuvent contenir d’énormes nombres de petits fichiers. Le stockage en overlay multiplie les opérations de métadonnées, et les caches de build s’accumulent silencieusement.

6) Supprimer un gros répertoire est‑il sûr ?

Parfois. Il est plus sûr de supprimer des fichiers basés sur l’âge dans un chemin éphémère connu que de supprimer un répertoire attendu par un service. Préférez des suppressions ciblées et vérifiez les configurations des services.

7) Que dois‑je surveiller pour détecter cela tôt ?

Surveillez l’utilisation des inodes par système de fichiers (df -i) et alertez sur la croissance soutenue et les seuils hauts (par exemple 85% et 95%), pas seulement le pourcentage d’utilisation du disque.

8) Si je n’ai plus d’inodes, dois‑je redémarrer ?

Redémarrer ne crée pas d’inodes. Cela peut temporairement supprimer quelques fichiers temporaires, mais ce n’est pas une solution et peut compliquer l’investigation. Libérez des inodes de manière délibérée à la place.

9) Pourquoi cela affecte‑t‑il parfois une seule application ?

Parce que seules les applications qui doivent créer de nouveaux fichiers sont bloquées. Les services en lecture seule peuvent continuer à fonctionner tant qu’ils n’essaient pas d’écrire des logs, sockets ou états.

10) Les logs journald sont‑ils susceptibles de causer un épuisement d’inodes ?

Moins fréquemment que les logs applicatifs qui créent un fichier par événement. journald a tendance à stocker les données dans quelques fichiers plus volumineux, ce qui est plus consommateur d’octets que d’inodes.

Prochaines étapes concrètes

Si vous ne prenez qu’une habitude : quand vous voyez « No space left on device », exécutez df -i aussi automatiquement que df -h.
Le système de fichiers a deux limites, et la production se moque de celle que vous avez oublié de surveiller.

Étapes pratiques :

  1. Ajoutez des alertes d’inodes sur chaque montage persistant (surtout / et les stockages des runtimes de conteneurs).
  2. Déplacez les chemins à fort churn sur des systèmes de fichiers dédiés pour qu’une charge défectueuse ne handicape pas tout le nœud.
  3. Corrigez le comportement des producteurs : rétention, TTL, moins de fichiers, meilleur empaquetage des artefacts.
  4. Pour ext4, planifiez la densité d’inodes en amont pour les charges de petits fichiers, et documentez votre choix.
  5. Organisez un game day : créez une tempête d’inodes contrôlée en staging, vérifiez vos alertes et la procédure de nettoyage.

Vous n’avez pas besoin de plus de disque. Vous avez besoin de moins de fichiers, de meilleurs contrôles de cycle de vie, et d’un agencement de système de fichiers qui reflète la réalité au lieu des hypothèses.

Sécurité CPU à venir : les surprises de type Spectre sont-elles terminées ?

Votre ticket d’incident indique « CPU 40% plus lent après le patch ». Votre ticket sécurité dit « les atténuations doivent rester activées ». Votre plan de capacité dit « lol ». Entre ces trois éléments se trouve la réalité de la sécurité CPU moderne : la prochaine surprise ne ressemblera pas exactement à Spectre, mais elle rima.

Si vous exploitez des systèmes en production — en particulier multi-tenant, hautes performances ou réglementés — votre travail n’est pas de gagner un débat sur la spéculation d’exécution. Votre travail est de garder la flotte assez rapide, assez sûre, et traçable quand ces deux objectifs entrent en conflit.

La réponse d’emblée (et quoi en faire)

Non, les surprises de type Spectre ne sont pas terminées. Nous avons dépassé la phase « tout brûle » de 2018, mais la leçon sous-jacente reste : les fonctions de performance créent des effets secondaires mesurables, et les attaquants adorent les effets secondaires mesurables. Les CPU continuent d’optimiser agressivement. Les logiciels continuent de construire des abstractions sur ces optimisations. La chaîne d’approvisionnement reste complexe (firmware, microcode, hyperviseurs, noyaux, bibliothèques, compilateurs). La surface d’attaque est toujours une cible mouvante.

La bonne nouvelle : nous ne sommes plus impuissants. Le matériel intègre désormais plus de réglages d’atténuation, de meilleurs paramètres par défaut et des contrats plus clairs. Les noyaux ont appris de nouvelles astuces. Les fournisseurs de cloud ont des pratiques opérationnelles qui n’impliquent pas des patchs paniqués à 3 h du matin. La mauvaise nouvelle : les atténuations ne sont pas « régler et oublier ». Ce sont de la configuration, de la gestion du cycle de vie et de l’ingénierie des performances.

Ce que vous devriez faire (opinion)

  • Cessez de traiter les atténuations comme un binaire. Élaborez une politique par charge de travail : multi-tenant vs mono-tenant, exposition navigateur/JS vs serveur uniquement, crypto sensible vs cache sans état.
  • Possédez votre inventaire CPU/firmware. « Nous sommes patchés » n’a pas de sens sans versions de microcode, versions de noyau et atténuations activées vérifiées pour chaque classe d’hôte.
  • Mesurez les performances avec les atténuations activées. Pas une fois. En continu. Lieez cela aux déploiements de noyau et de microcode.
  • Privilégiez l’isolation ennuyeuse aux réglages ingénieux. Hôtes dédiés, frontières VM solides et désactivation du SMT quand nécessaire valent mieux que d’espérer qu’un drapeau microcode vous sauve.
  • Instrumentez le coût. Si vous ne pouvez pas expliquer où sont passés les cycles (appels système, changements de contexte, mispredictions de branche, E/S), vous ne pouvez pas choisir les atténuations en toute sécurité.

Une idée paraphrasée de Gene Kim (fiabilité/ops) : Des changements rapides et fréquents sont plus sûrs quand vous avez de fortes boucles de rétroaction et pouvez détecter et récupérer vite. C’est ainsi que vous survivez aux surprises de sécurité : faites du changement la routine, pas l’héroïsme.

Ce qui a changé depuis 2018 : puces, noyaux et culture

Faits intéressants et contexte historique (bref et concret)

  1. 2018 a forcé l’industrie à parler d’architecture microarchitecturale comme si cela comptait. Avant, beaucoup d’équipes ops considéraient les internes CPU comme de la « magie du fournisseur » et se concentraient sur le réglage OS/app.
  2. Les premières atténuations étaient des instruments grossiers. Les réponses initiales du noyau échangeaient souvent latence contre sécurité parce que l’alternative était « ne rien expédier ».
  3. Retpoline était une stratégie de compilateur, pas une fonctionnalité matérielle. Elle a réduit certains risques d’injection de cible de branche sans dépendre uniquement du comportement du microcode.
  4. Le hyper-threading (SMT) est passé de « performance gratuite » à « bouton de risque ». Certains chemins de fuite sont pires quand des threads siblings partagent les ressources du cœur.
  5. Le microcode est devenu une dépendance opérationnelle. Mettre à jour le BIOS/firmware était autrefois rare dans les fermes ; maintenant c’est un élément de maintenance récurrent, parfois livré via des paquets OS.
  6. Les fournisseurs cloud ont discrètement modifié leurs politiques d’ordonnancement. Niveaux d’isolation, hôtes dédiés et contrôles de « voisin bruyant » ont soudainement eu un angle sécurité, pas seulement performance.
  7. La recherche en attaque s’est tournée vers de nouveaux canaux secondaires. Le timing du cache n’était que le début ; prédicteurs, buffers et effets d’exécution transitoire sont devenus des sujets courants.
  8. La posture de sécurité a commencé à inclure « régressions de performance comme risque ». Une atténuation qui divise le débit par deux peut forcer des raccourcis dangereux ou des reports de patch — les deux sont des échecs de sécurité.

Le matériel s’est amélioré pour être explicite

Les CPU modernes incluent davantage de réglages et de sémantiques pour le contrôle de la spéculation. Cela ne signifie pas « corrigé », cela signifie « le contrat est moins implicite ». Certaines atténuations sont désormais des fonctionnalités architecturées plutôt que des bricolages : barrières plus claires, meilleures sémantiques de séparation des privilèges et moyens plus prévisibles de vider ou segmenter l’état.

Cependant le progrès matériel est inégal. Différentes générations de CPU, fournisseurs et SKU varient largement. Vous ne pouvez pas traiter « Intel » ou « AMD » comme un comportement unique. Même au sein d’une famille modèle, des révisions microcode peuvent changer le comportement d’atténuation et la performance.

Les noyaux ont appris à négocier

Linux (et d’autres OS) sait désormais détecter les capacités CPU, appliquer des atténuations conditionnellement et exposer l’état de manière vérifiable par les opérateurs. C’est important. En 2018, beaucoup d’équipes se contentaient de basculer des flags de démarrage et d’espérer. Aujourd’hui vous pouvez interroger : « IBRS est-il actif ? » « KPTI est-il activé ? » « SMT est-il jugé dangereux ici ? » — et vous pouvez le faire à grande échelle.

De plus, les compilateurs et runtimes ont changé. Certaines atténuations vivent dans les choix de génération de code, pas seulement dans les commutateurs du noyau. C’est une leçon de fiabilité : votre « plateforme » inclut les toolchains.

Blague #1 : L’exécution spéculative est comme un stagiaire qui commence trois tâches à la fois « pour être efficace », puis renverse du café en production. Rapide, et étonnamment créatif.

Pourquoi « Spectre » est une classe, pas un bug

Quand on demande si Spectre est « terminé », on veut souvent dire : « Avons-nous fini avec les vulnérabilités liées à l’exécution spéculative ? » C’est comme demander si vous avez fini avec « les bugs dans les systèmes distribués ». Vous pouvez clore un ticket. Vous n’avez pas clos la catégorie.

Le schéma de base

Les problèmes de type Spectre exploitent un décalage entre le comportement architecturel (ce que le CPU promet de faire) et le comportement microarchitectural (ce qui se passe réellement en interne pour aller vite). L’exécution transitoire peut toucher des données qui devraient être inaccessibles, puis divulguer un indice à leur sujet via le timing ou d’autres canaux secondaires. Le CPU « annule » ensuite l’état architectural, mais il ne peut pas annuler la physique. Les caches ont été chauffés. Les prédicteurs ont été entraînés. Les buffers se sont remplis. Un attaquant malin peut mesurer les résidus.

Pourquoi les atténuations sont compliquées

Atténuer est difficile parce que :

  • Vous vous battez contre la mesure. Si l’attaquant peut mesurer quelques nanosecondes de façon consistante, vous avez un problème — même si rien de « mal » ne s’est produit au niveau architectural.
  • Les atténuations vivent à plusieurs couches. Fonctionnalités matérielles, microcode, noyau, hyperviseur, compilateur, bibliothèques et parfois l’application elle-même.
  • Les charges réagissent différemment. Une charge lourde en appels système peut souffrir de certaines atténuations du noyau ; une charge liée au calcul peut à peine s’en apercevoir.
  • Les modèles de menace diffèrent. Le bac à sable du navigateur est différent d’une machine HPC mono-tenant, et différent encore des nœuds Kubernetes partagés.

« Nous l’avons patché » n’est pas un état, c’est une affirmation

Opérationnellement, traitez la sécurité de type Spectre comme la durabilité des données en stockage : vous ne la déclarez pas, vous la vérifiez continuellement. La vérification doit être bon marché, automatisable et liée au contrôle des changements.

D’où viendront les prochaines surprises

La prochaine vague ne s’appellera pas nécessairement « Spectre vNext », mais elle exploitera toujours le même méta-problème : les fonctions de performance CPU créent un état partagé, et l’état partagé fuit.

1) Prédicteurs, buffers et structures partagées « invisibles »

Les caches sont le canal secondaire célèbre. Les attaquants réels s’intéressent aussi aux prédicteurs de branche, prédicteurs de retour, store buffers, line fill buffers, TLB et autres états microarchitecturaux qui peuvent être influencés et mesurés à travers des frontières de sécurité.

À mesure que les puces ajoutent plus d’ingéniosité (prédicteurs plus grands, pipelines plus profonds, issue plus large), le nombre d’endroits où l’« état résiduel » peut se cacher augmente. Même si les fournisseurs ajoutent du partitionnement, vous avez toujours des transitions : user→kernel, VM→hypervisor, container→container sur le même hôte, process→process.

2) Calcul hétérogène et accélérateurs

Les CPU partagent désormais du travail avec des GPU, NPU, DPU et « enclaves de sécurité ». Cela change la surface des canaux secondaires. Certains de ces composants ont leurs propres caches et ordonnanceurs. Si vous pensez que l’exécution spéculative est compliquée, attendez de devoir raisonner sur la mémoire GPU partagée et les noyaux multi-tenant.

3) Chaîne d’approvisionnement du firmware et dérive de configuration

Les atténuations dépendent souvent du microcode et des paramètres firmware. Les flottes dérivent. Quelqu’un remplace une carte mère, une mise à jour du BIOS annule un réglage, ou un fournisseur expédie un défaut « performance » qui réactive un comportement risqué. Votre modèle de menace peut être parfait et échouer parce que votre inventaire est de la fiction.

4) Pression cross-tenant dans le cloud

La réalité business : la multi-location paie les factures. C’est précisément là que les canaux secondaires importent. Si vous exploitez des nœuds partagés, vous devez supposer des voisins curieux. Si vous exploitez du matériel mono-tenant, vous devez quand même vous préoccuper d’évasions de bac à sable, d’exposition navigateur ou de charges malveillantes que vous exécutez vous-mêmes (bonjour, CI/CD).

5) La « taxe d’atténuation » déclenche des comportements dangereux

Ceci est le mode d’échec sous-discuté : des atténuations qui nuisent à la performance poussent les équipes à les désactiver, retarder les patchs ou surcommettre des nœuds pour respecter les SLO. C’est ainsi que vous accumulez une dette de sécurité avec intérêt. La prochaine surprise pourrait être organisationnelle, pas microarchitecturale.

Blague #2 : Rien ne motive un formulaire d’« acceptation du risque » comme une régression de performance de 20% et une fin de trimestre.

Modèles de risque qui correspondent vraiment à la production

Commencez par les frontières, pas par les noms de CVE

Les problèmes de type Spectre concernent les fuites à travers des frontières. Cartographiez donc votre environnement par frontières :

  • Utilisateur ↔ noyau (utilisateurs locaux non fiables, processus sandboxés, chemins d’évasion de conteneur)
  • VM ↔ hyperviseur (virtualisation multi-tenant)
  • Processus ↔ processus (hôte partagé avec domaines de confiance différents)
  • Thread ↔ thread (siblings SMT)
  • Hôte ↔ hôte (moins direct, mais pensez caches partagés dans certains designs, offloads NIC ou canaux secondaires de stockage partagé)

Trois postures communes en production

Posture A : « Nous exécutons du code non fiable » (atténuations les plus strictes)

Exemples : cloud public, runners CI pour contributeurs externes, fermes de rendu orientées navigateur, hôtes de plugins, PaaS multi-tenant. Ici, pas de compromis. Activez les atténuations par défaut. Envisagez de désactiver le SMT sur les nœuds partagés. Envisagez des hôtes dédiés pour les locataires sensibles. Vous réduisez la probabilité de divulgation de données cross-tenant.

Posture B : « Nous exécutons du code semi-fiable » (équilibré)

Exemples : Kubernetes interne avec de nombreuses équipes, clusters d’analytics partagés, bases de données multi-tenant. Vous craignez les mouvements latéraux et les expositions accidentelles. Les atténuations doivent rester activées, mais vous pouvez utiliser des niveaux d’isolation : workloads sensibles sur nœuds plus stricts, workloads généraux ailleurs. Les décisions SMT doivent être spécifiques à la charge.

Posture C : « Nous exécutons du code de confiance sur du matériel dédié » (ce n’est pas gratuit)

Exemples : boîtes DB dédiées, appliances mono-usage, HPC. Vous pouvez accepter un certain risque pour la performance, mais attention à deux pièges : (1) les navigateurs et les runtimes JIT peuvent introduire des comportements « untrusted-ish », et (2) la menace interne et la chaîne d’approvisionnement sont réelles. Si vous désactivez des atténuations, documentez-le, isolez le système et vérifiez en continu qu’il reste isolé.

Rendez la politique exécutable

Une politique qui vit dans un wiki est une histoire du soir. Une politique qui vit dans l’automatisation est un contrôle. Vous voulez :

  • Labels de nœud (par ex., « smt_off_required », « mitigations_strict »)
  • Profils de paramètres de démarrage gérés par la gestion de configuration
  • Vérifications de conformité continues : version microcode, flags noyau, statut des vulnérabilités
  • Garde-fous de régression de performance pour les déploiements noyau/microcode

Tâches pratiques : auditer, vérifier et choisir des atténuations (avec commandes)

Ce ne sont pas des théories. Ce sont les vérifications que vous exécutez pendant un incident, un déploiement ou un audit de conformité. Chaque tâche inclut : commande, sortie d’exemple, ce que cela signifie, et la décision à prendre.

Tâche 1 : Vérifier le statut des vulnérabilités rapporté par le noyau

cr0x@server:~$ grep . /sys/devices/system/cpu/vulnerabilities/*
/sys/devices/system/cpu/vulnerabilities/gather_data_sampling:Mitigation: Clear CPU buffers; SMT Host state unknown
/sys/devices/system/cpu/vulnerabilities/itlb_multihit:KVM: Mitigation: VMX disabled
/sys/devices/system/cpu/vulnerabilities/l1tf:Mitigation: PTE Inversion; VMX: conditional cache flushes, SMT vulnerable
/sys/devices/system/cpu/vulnerabilities/mds:Mitigation: Clear CPU buffers; SMT vulnerable
/sys/devices/system/cpu/vulnerabilities/meltdown:Mitigation: PTI
/sys/devices/system/cpu/vulnerabilities/mmio_stale_data:Mitigation: Clear CPU buffers; SMT Host state unknown
/sys/devices/system/cpu/vulnerabilities/reg_file_data_sampling:Not affected
/sys/devices/system/cpu/vulnerabilities/retbleed:Mitigation: IBRS
/sys/devices/system/cpu/vulnerabilities/spec_rstack_overflow:Mitigation: Safe RET
/sys/devices/system/cpu/vulnerabilities/spec_store_bypass:Mitigation: Speculative Store Bypass disabled via prctl and seccomp
/sys/devices/system/cpu/vulnerabilities/spectre_v1:Mitigation: usercopy/swapgs barriers and __user pointer sanitization
/sys/devices/system/cpu/vulnerabilities/spectre_v2:Mitigation: Enhanced IBRS, IBPB: conditional, RSB filling, STIBP: conditional
/sys/devices/system/cpu/vulnerabilities/srbds:Mitigation: Microcode
/sys/devices/system/cpu/vulnerabilities/tsx_async_abort:Not affected

Ce que cela signifie : Le noyau vous indique quelles atténuations sont actives et où le risque demeure (notamment les lignes incluant « SMT vulnerable » ou « Host state unknown »).

Décision : Si vous exécutez du multi-tenant ou du code non fiable et voyez « SMT vulnerable », escaladez pour envisager la désactivation du SMT ou une isolation plus stricte pour ces nœuds.

Tâche 2 : Confirmer l’état du SMT (hyper-threading)

cr0x@server:~$ cat /sys/devices/system/cpu/smt/active
1

Ce que cela signifie : 1 signifie que le SMT est actif ; 0 signifie désactivé.

Décision : Sur les nœuds partagés traitant des charges non fiables, préférez 0 à moins d’avoir une raison chiffrée de ne pas le faire. Sur les machines dédiées mono-tenant, décidez selon la charge et la tolérance au risque.

Tâche 3 : Voir avec quelles atténuations le noyau a démarré

cr0x@server:~$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-6.6.15 root=UUID=... ro mitigations=auto,nosmt spectre_v2=on

Ce que cela signifie : Les paramètres du noyau définissent le comportement global. mitigations=auto,nosmt demande des atténuations automatiques tout en désactivant le SMT.

Décision : Traitez ceci comme l’état désiré. Puis vérifiez l’état réel via /sys/devices/system/cpu/vulnerabilities/* car certains flags sont ignorés s’ils ne sont pas supportés.

Tâche 4 : Vérifier la révision de microcode chargée

cr0x@server:~$ dmesg | grep -i microcode | tail -n 5
[    0.612345] microcode: Current revision: 0x000000f6
[    0.612346] microcode: Updated early from: 0x000000e2
[    1.234567] microcode: Microcode Update Driver: v2.2.

Ce que cela signifie : Vous pouvez voir si le microcode a été mis à jour tôt et quelle révision est active.

Décision : Si la flotte a des révisions mixtes sur le même modèle CPU, vous avez de la dérive. Corrigez la dérive avant de débattre de la performance. Microcode mixte égale comportement mixte.

Tâche 5 : Corréler le modèle CPU et le stepping (parce que ça compte)

cr0x@server:~$ lscpu | egrep 'Model name|Vendor ID|CPU family|Model:|Stepping:|Flags'
Vendor ID:                       GenuineIntel
Model name:                      Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz
CPU family:                      6
Model:                           106
Stepping:                        6
Flags:                           fpu vme de pse tsc ... ssbd ibrs ibpb stibp arch_capabilities

Ce que cela signifie : Des flags comme ibrs, ibpb, stibp, ssbd et arch_capabilities indiquent quels mécanismes d’atténuation existent.

Décision : Utilisez cela pour segmenter les classes d’hôtes. Ne déployez pas le même profil d’atténuation sur des CPU avec des capacités fondamentalement différentes sans mesurer.

Tâche 6 : Valider le statut KPTI / PTI (lié à Meltdown)

cr0x@server:~$ dmesg | egrep -i 'pti|kpti|page table isolation' | tail -n 5
[    0.000000] Kernel/User page tables isolation: enabled

Ce que cela signifie : PTI est activé. Cela augmente typiquement le coût des appels système sur les systèmes affectés.

Décision : Si vous observez une latence soudaine sur des charges riches en appels système, PTI est un suspect. Mais ne le désactivez pas à la légère ; préférez upgrader le matériel là où c’est moins coûteux ou inutile.

Tâche 7 : Vérifier les détails du mode d’atténuation Spectre v2

cr0x@server:~$ cat /sys/devices/system/cpu/vulnerabilities/spectre_v2
Mitigation: Enhanced IBRS, IBPB: conditional, RSB filling, STIBP: conditional

Ce que cela signifie : Le noyau a choisi un mélange spécifique. « Conditional » signifie souvent que le noyau l’applique lors des changements de contexte ou quand il détecte des transitions risquées.

Décision : Si vous exploitez du trading à faible latence ou du RPC haute fréquence, mesurez les coûts de changement de contexte et envisagez des upgrades CPU ou des niveaux d’isolation plutôt que de désactiver globalement les atténuations.

Tâche 8 : Confirmer si le noyau considère le SMT sûr pour des problèmes de type MDS

cr0x@server:~$ cat /sys/devices/system/cpu/vulnerabilities/mds
Mitigation: Clear CPU buffers; SMT vulnerable

Ce que cela signifie : Vider les buffers CPU aide, mais le SMT laisse encore des chemins d’exposition que le noyau signale.

Décision : Pour les hôtes multi-tenant, c’est un signal fort pour désactiver le SMT ou passer à une location dédiée.

Tâche 9 : Mesurer rapidement le changement de contexte et la pression d’appels système

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0 842112  52124 912340    0    0    12    33  820 1600 12  6 82  0  0
 3  0      0 841900  52124 912500    0    0     0     4 1100 4200 28 14 58  0  0
 4  0      0 841880  52124 912600    0    0     0     0 1300 6100 35 18 47  0  0
 1  0      0 841870  52124 912650    0    0     0     0  900 2000 18  8 74  0  0

Ce que cela signifie : Surveillez cs (changements de contexte) et sy (CPU noyau). Si cs explose et que sy augmente après des changements d’atténuation, vous avez trouvé où la taxe s’applique.

Décision : Envisagez de réduire le taux d’appels système (regroupement, E/S asynchrone, moins de processus), ou déplacez cette charge vers des CPU plus récents avec des atténuations moins coûteuses.

Tâche 10 : Repérer la surcharge liée aux atténuations via perf (niveau élevé)

cr0x@server:~$ sudo perf stat -a -- sleep 5
 Performance counter stats for 'system wide':

        24,118.32 msec cpu-clock                 #    4.823 CPUs utilized
     1,204,883,112      context-switches         #   49.953 K/sec
        18,992,114      cpu-migrations           #  787.471 /sec
         2,113,992      page-faults              #  87.624 /sec
  62,901,223,111,222      cycles                 #    2.608 GHz
  43,118,441,902,112      instructions           #    0.69  insn per cycle
     9,882,991,443      branches                #  409.687 M/sec
       412,888,120      branch-misses           #    4.18% of all branches

       5.000904564 seconds time elapsed

Ce que cela signifie : Un IPC faible et des ratés de branche élevés peuvent se corréler avec des barrières de spéculation et des effets sur les prédicteurs, bien que ce ne soit pas une preuve en soi.

Décision : Si les branch misses augmentent après un déploiement d’atténuation, ne devinez pas. Reproduisez dans un environnement de staging et comparez avec une paire noyau/microcode de référence.

Tâche 11 : Vérifier si KVM est présent et ce qu’il rapporte

cr0x@server:~$ lsmod | grep -E '^kvm|^kvm_intel|^kvm_amd'
kvm_intel             372736  0
kvm                  1032192  1 kvm_intel

Ce que cela signifie : L’hôte est un hyperviseur. Les contrôles de spéculation peuvent s’appliquer aux entrées/sorties VM, et certaines vulnérabilités exposent un risque cross-VM.

Décision : Traitez cette classe d’hôte comme à sensibilité accrue. Évitez les bascules « performance » personnalisées à moins de démontrer que la sécurité cross-VM est préservée.

Tâche 12 : Confirmer les paquets microcode installés (exemple Debian/Ubuntu)

cr0x@server:~$ dpkg -l | egrep 'intel-microcode|amd64-microcode'
ii  intel-microcode  3.20231114.1ubuntu1  amd64  Processor microcode firmware for Intel CPUs

Ce que cela signifie : Le microcode géré par l’OS est présent et versionné, ce qui facilite les mises à jour de flotte comparé à une approche BIOS seule.

Décision : Si le microcode n’est fourni que via le BIOS et que vous n’avez pas de pipeline firmware, vous serez en retard sur les atténuations. Construisez ce pipeline.

Tâche 13 : Confirmer les paquets microcode installés (exemple RHEL)

cr0x@server:~$ rpm -qa | egrep '^microcode_ctl|^linux-firmware'
microcode_ctl-20240109-1.el9.x86_64
linux-firmware-20240115-2.el9.noarch

Ce que cela signifie : La livraison du microcode fait partie du patching OS, avec son propre calendrier.

Décision : Traitez les mises à jour de microcode comme des mises à jour de noyau : déploiement par étapes, canaris et vérifications de régression de performance.

Tâche 14 : Valider si les atténuations ont été désactivées (intentionnellement ou accidentellement)

cr0x@server:~$ grep -Eo 'mitigations=[^ ]+|nospectre_v[0-9]+|spectre_v[0-9]+=[^ ]+|nopti|nosmt' /proc/cmdline
mitigations=off
nopti

Ce que cela signifie : Cet hôte tourne avec les atténuations explicitement désactivées. Ce n’est pas un « peut-être ». C’est un choix.

Décision : Si ce n’est pas un environnement dédié, isolé et avec acceptation de risque documentée, traitez cela comme un incident de sécurité (ou au minimum une faille de conformité) et remédiez.

Tâche 15 : Quantifier le delta de performance en toute sécurité (A/B au boot noyau)

cr0x@server:~$ sudo systemctl reboot --boot-loader-entry=auto-mitigations
Failed to reboot: Boot loader entry not supported

Ce que cela signifie : Tous les environnements ne supportent pas le changement facile d’entrée de boot. Vous pouvez avoir besoin d’une autre approche (profils GRUB, kexec ou hôtes canaris dédiés).

Décision : Construisez un mécanisme canari reproductible. Si vous ne pouvez pas tester A/B des combos noyau+microcode, vous discuterez de la performance indéfiniment.

Tâche 16 : Vérifier noyau temps réel vs noyau générique (sensibilité à la latence)

cr0x@server:~$ uname -a
Linux server 6.6.15-rt14 #1 SMP PREEMPT_RT x86_64 GNU/Linux

Ce que cela signifie : PREEMPT_RT ou noyaux basse latence interagissent différemment avec le coût des atténuations parce que la planification et le préemption changent.

Décision : Si vous exécutez des charges RT, testez les atténuations sur noyaux RT spécifiquement. Ne tirez pas de conclusions à partir de noyaux génériques.

Guide de diagnostic rapide

Ceci s’adresse au jour où vous patchez un noyau ou un microcode et vos tableaux de bord SLO deviennent de l’art moderne.

Première étape : prouvez si la régression est liée aux atténuations

  1. Vérifiez rapidement l’état des atténuations : grep . /sys/devices/system/cpu/vulnerabilities/*. Cherchez les formulations changées par rapport au dernier état connu.
  2. Vérifiez les flags de démarrage : cat /proc/cmdline. Confirmez que vous n’avez pas hérité de mitigations=off ou ajouté par accident des flags plus stricts dans une nouvelle image.
  3. Vérifiez la révision microcode : dmesg | grep -i microcode. Un changement de microcode peut modifier le comportement sans changement de noyau.

Deuxième étape : localisez le coût (où le CPU est allé ?)

  1. Pression appels système / changements de contexte : vmstat 1. Si sy et cs montent, les atténuations affectant les traversées noyau sont suspectes.
  2. Churn d’ordonnancement : vérifiez les migrations et la pression des runqueues. Des cpu-migrations élevées dans perf stat ou un r élevé dans vmstat indique des interactions avec le scheduler.
  3. Symptômes prédicteurs/branche : perf stat ciblant les branch misses et IPC. Pas définitif, mais utile comme boussole.

Troisième étape : isolez les variables et choisissez le correctif le moins mauvais

  1. Canariser une seule classe d’hôte : même modèle CPU, même charge, même forme de trafic. Changez une seule variable : noyau ou microcode, pas les deux.
  2. Comparez politiques « strict » vs « auto » : si vous devez ajuster, faites-le par pool de nœuds, pas globalement.
  3. Privilégiez les correctifs structurels : hôtes dédiés pour workloads sensibles, réduisez les traversées noyau, évitez les modèles de threads à fort churn, pincez les processus critiques en latence.

Si vous ne pouvez pas répondre « quelle transition est devenue plus lente ? » (user→noyau, VM→hôte, thread→thread), vous ne diagnosez pas ; vous négociez avec la physique.

Trois mini-histoires du monde de l’entreprise

Mini-histoire 1 : L’incident causé par une mauvaise hypothèse

Une entreprise SaaS de taille moyenne exploitait une flotte mixte : serveurs plus récents pour bases de données, nœuds plus anciens pour le batch et un grand cluster Kubernetes pour « tout le reste ». Après une campagne sécurité, ils ont activé un profil d’atténuation plus strict sur le pool Kubernetes. Tout semblait propre dans la gestion de configuration : un réglage, un déploiement, une coche verte.

Puis la latence de l’API client a dérivé à la hausse sur deux jours. Pas une chute brutale — une dégradation lente qui a déclenché des débats : « C’est le code », « C’est la base », « C’est le réseau », « C’est le LB ». Classique.

La mauvaise hypothèse était simple : ils supposaient que tous les nœuds du pool avaient le même comportement CPU. En réalité, le pool contenait deux générations CPU. Sur une génération, le mode d’atténuation reposait fortement sur des transitions coûteuses, et la charge API était riche en appels système à cause d’une librairie de logs et de réglages TLS qui augmentaient les traversées noyau. Sur la génération plus récente, les mêmes réglages étaient bien moins coûteux.

Ils l’ont découvert seulement après avoir comparé les sorties de /sys/devices/system/cpu/vulnerabilities/spectre_v2 entre nœuds et remarqué des chaînes d’atténuation différentes sur des nœuds supposés « identiques ». Les révisions microcode étaient aussi inégales parce que certains serveurs avaient le microcode géré par l’OS, d’autres dépendaient des BIOS jamais planifiés pour mise à jour.

La correction n’a pas été « désactiver les atténuations ». Ils ont scindé le pool par modèle CPU et base microcode, puis rééquilibré les charges : les pods API riches en appels système sont passés sur le pool plus récent. Ils ont aussi intégré une vérification de conformité microcode à l’admission des nœuds.

La leçon : quand votre risque et votre performance dépendent de la microarchitecture, des pools homogènes ne sont pas un luxe. Ce sont un contrôle.

Mini-histoire 2 : L’optimisation qui s’est retournée contre eux

Une équipe fintech chassait la latence tail dans un service de tarification. Ils ont tout fait : épingler les threads, régler les queues NIC, réduire les allocations et déplacer les chemins chauds hors du noyau quand possible. Puis ils sont devenus audacieux. Ils ont désactivé le SMT en théorie pour réduire le partage de ressources et donc le jitter. Ça a un peu aidé.

Encouragés, ils ont ensuite desserré certains réglages d’atténuation dans un environnement dédié. Le système était « mono-tenant », après tout. La perf s’est améliorée sur leurs benchmarks synthétiques, et ils se sont crus malins. Ils ont déployé en production avec une note d’acceptation de risque.

Deux mois plus tard, un projet séparé a réutilisé la même image d’hôte pour exécuter des jobs CI pour des dépôts internes. « Interne » est vite devenu « semi-fiable », car il y avait des contractuels et des dépendances externes. Les charges CI étaient bruyantes, JIT-heavy et dangereusement proches du processus de tarification en termes d’ordonnancement. Rien n’a été exploité (à leur connaissance), mais une revue sécurité a signalé le décalage : l’image d’hôte partait d’un modèle de menace qui n’était plus vrai.

Pire, quand ils ont réactivé les atténuations, la régression de performance a été plus nette que prévu. Le tuning du système dépendait des réglages relaxés antérieurs : plus de threads, plus de changements de contexte et quelques hypothèses de « fast path ». Ils s’étaient optimisés dans un coin.

La correction a été ennuyeuse et coûteuse : séparer les pools et images d’hôte. Le pricing a tourné sur des nœuds stricts dédiés. Le CI a tourné ailleurs avec isolation renforcée et attentes de performance différentes. Ils ont aussi commencé à considérer les réglages d’atténuation comme une partie de l’« API » entre plateforme et équipes applicatives.

La leçon : les optimisations qui modifient la posture sécurité ont tendance à être réutilisées hors contexte. Les images se propagent. Le risque aussi.

Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la situation

Une grande entreprise exploitait un cloud privé avec plusieurs fournisseurs matériels et de longs cycles de serveur. Ils vivaient dans le monde réel : cycles d’approvisionnement, fenêtres de maintenance, apps legacy et auditeurs de conformité qui préfèrent la paperasse à la disponibilité.

Après 2018, ils ont fait quelque chose de douloureusement peu sexy : ils ont construit un pipeline d’inventaire. Chaque hôte rapportait modèle CPU, révision microcode, version noyau, paramètres de boot et le contenu de /sys/devices/system/cpu/vulnerabilities/*. Ces données alimentaient un dashboard et un moteur de politiques. Les nœuds qui dérivaient hors conformité étaient mis en quarantaine dans Kubernetes ou drainés dans leur ordonnanceur VM.

Des années plus tard, une nouvelle mise à jour microcode a introduit un changement de performance mesurable sur un sous-ensemble d’hôtes. Parce qu’ils avaient inventaire et canaris, ils l’ont remarqué en quelques heures. Parce qu’ils avaient des classes d’hôtes, le rayon d’impact a été contenu. Parce qu’ils avaient une voie de rollback, ils ont récupéré avant qu’un impact client ne devienne une manchette.

La traçabilité a aussi compté. La sécurité a demandé : « Quels nœuds sont encore vulnérables dans ce mode ? » Ils ont répondu par une requête, pas une réunion.

La leçon : l’opposé de la surprise n’est pas la prédiction. C’est l’observabilité plus le contrôle.

Erreurs courantes : symptôme → cause racine → correctif

1) Symptom : « Le CPU est élevé après le patch »

  • Cause racine : Plus de temps passé dans les traversées noyau (PTI/KPTI, barrières de spéculation), souvent amplifié par des charges riches en appels système.
  • Correctif : Mesurez vmstat (sy, cs), réduisez le taux d’appels système (regroupement, E/S asynchrone), upgradez vers des CPU avec des atténuations moins coûteuses, ou isolez la charge vers une classe de nœuds appropriée.

2) Symptom : « La latence tail a explosé, la moyenne semble correcte »

  • Cause racine : Atténuations conditionnelles aux frontières de changement de contexte interagissant avec le churn du scheduler ; contention entre siblings SMT ; voisins bruyants.
  • Correctif : Désactivez le SMT pour les pools sensibles, épinglez les threads critiques, réduisez les migrations et séparez les charges bruyantes. Validez avec perf stat et des métriques de scheduler.

3) Symptom : « Certains nœuds sont rapides, d’autres lents, même image »

  • Cause racine : Dérive microcode et stepping CPU mixte ; le noyau sélectionne des chemins d’atténuation différents.
  • Correctif : Faites respecter des baselines microcode, segmentez les pools par modèle/stepping CPU et faites de l’état d’atténuation une condition d’aptitude du nœud.

4) Symptom : « Le scan de sécurité dit vulnérable, mais nous avons patché »

  • Cause racine : Patch appliqué seulement au niveau OS ; firmware/microcode manquant ; ou atténuations désactivées via des flags de boot.
  • Correctif : Vérifiez via /sys/devices/system/cpu/vulnerabilities/* et la révision microcode ; remédiez via paquets microcode ou mises à jour BIOS ; retirez les flags de boot risqués.

5) Symptom : « Les charges VM sont plus lentes, le bare metal ne l’est pas »

  • Cause racine : Le surcoût d’entrée/sortie VM a augmenté à cause des hooks d’atténuation ; hyperviseur appliquant des barrières plus strictes.
  • Correctif : Mesurez le surcoût de virtualisation ; envisagez des hôtes dédiés, des générations CPU plus récentes, ou ajustez la densité VM. Évitez de désactiver globalement les atténuations sur les hyperviseurs.

6) Symptom : « Nous avons désactivé les atténuations et rien de mal ne s’est passé »

  • Cause racine : Confondre absence de preuve et preuve d’absence ; le modèle de menace a changé silencieusement plus tard (nouvelles charges, nouveaux locataires, nouveaux runtimes).
  • Correctif : Traitez les changements d’atténuation comme une API sensible à la sécurité. Exigez une politique explicite, des garanties d’isolation et une révalidation périodique du modèle de menace.

Listes de contrôle / plan étape par étape

Étape par étape : construisez une posture de type Spectre avec laquelle vous pouvez vivre

  1. Classifiez les pools de nœuds par frontière de confiance. Multi-tenant partagé, internal partagé, dédié sensible, dédié général.
  2. Inventoriez CPU et microcode. Collectez lscpu, révision microcode depuis dmesg, et version noyau depuis uname -r.
  3. Inventoriez l’état des atténuations. Collectez /sys/devices/system/cpu/vulnerabilities/* par nœud et stockez centralement.
  4. Définissez des profils d’atténuation. Pour chaque pool, spécifiez les flags de boot noyau (ex. mitigations=auto, optionnel nosmt) et la baseline microcode requise.
  5. Rendez la conformité exécutable. Les nœuds hors profil ne doivent pas accepter de charges sensibles (cordon/drain, taints scheduler, contraintes de placement VM).
  6. Canarisez chaque déploiement noyau/microcode. Une classe d’hôte à la fois ; comparez latence, débit et compteurs CPU.
  7. Benchmarkez avec des formes de trafic réelles. Les microbenchmarks synthétiques manquent les patterns d’appels système, le comportement cache et le churn des allocateurs.
  8. Documentez les acceptations de risque avec expiration. Si vous désactivez quelque chose, mettez une date d’expiration et forcez une ré-approbation.
  9. Formez les intervenants incident. Ajoutez le « Guide de diagnostic rapide » au runbook d’astreinte et entraînez-le.
  10. Planifiez le renouvellement matériel avec la sécurité en tête. Les CPU plus récents peuvent réduire la taxe d’atténuation ; c’est un cas business, pas un gadget.

Checklist : avant de désactiver le SMT

  • Confirmez si le noyau rapporte « SMT vulnerable » pour les problèmes concernés.
  • Mesurez la différence de performance sur des charges représentatives.
  • Décidez par pool, pas par hôte.
  • Assurez-vous d’un delta de capacité pour la baisse de débit.
  • Mettez à jour les règles d’ordonnancement pour que les charges sensibles atterrissent sur le pool prévu.

Checklist : avant d’assouplir les atténuations pour la performance

  • Le système est-il vraiment mono-tenant de bout en bout ?
  • Du code non fiable peut-il s’exécuter (jobs CI, plugins, navigateurs, runtimes JIT, scripts clients) ?
  • L’hôte est-il atteignable par des attaquants avec exécution locale ?
  • Avez-vous du matériel dédié et un contrôle d’accès strict ?
  • Avez-vous une voie de rollback qui ne requiert pas un héros ?

FAQ

1) Les surprises de type Spectre sont-elles terminées ?

Non. La onde de choc initiale est passée, mais la dynamique sous-jacente reste : les fonctions de performance créent de l’état partagé, et l’état partagé fuit. Attendez-vous à une recherche continue et à des mises à jour périodiques d’atténuation.

2) Si mon noyau dit « Mitigation: … » suis-je en sécurité ?

Vous êtes plus en sécurité que « Vulnerable », mais « en sécurité » dépend de votre modèle de menace. Faites attention aux phrases comme « SMT vulnerable » et « Host state unknown ». Ce sont les messages du noyau indiquant le risque résiduel.

3) Dois-je désactiver le SMT partout ?

Non. Désactivez le SMT là où vous avez un risque cross-tenant ou du code non fiable et où le noyau indique une exposition liée au SMT. Gardez le SMT là où l’isolation matérielle et la confiance de la charge le justifient, et où vous avez mesuré le bénéfice.

4) Est-ce principalement un problème cloud ?

Le cloud multi-tenant aiguise le modèle de menace, mais les canaux secondaires importent aussi on-prem : clusters partagés, multi-location interne, systèmes CI, et tout environnement où l’exécution locale est plausible.

5) Quel est le mode d’échec opérationnel le plus courant ?

La dérive : microcode mixte, CPUs mixtes et flags de démarrage incohérents. Les flottes deviennent une patchwork, et vous vous retrouvez avec un risque inégal et une performance imprévisible.

6) Puis-je compter sur l’isolation par conteneur pour me protéger ?

Les conteneurs partagent le noyau, et les canaux secondaires ne respectent pas les namespaces. Les conteneurs sont excellents pour le packaging et le contrôle de ressources, pas une frontière de sécurité stricte contre les fuites microarchitecturales.

7) Pourquoi les atténuations nuisent parfois plus à la latence qu’au débit ?

Parce que beaucoup d’atténuations taxent les transitions (changements de contexte, appels système, sorties VM). La latence tail est sensible à l’ajout de travail sur le chemin critique et à l’interférence du scheduler.

8) Que devrais-je stocker dans mon CMDB ou système d’inventaire ?

Modèle/stepping CPU, révision microcode, version noyau, paramètres de boot, état SMT et le contenu de /sys/devices/system/cpu/vulnerabilities/*. Cet ensemble vous permet de répondre à la plupart des questions d’audit et d’incident rapidement.

9) Les nouveaux CPU sont-ils « immunisés » ?

Non. Les CPU plus récents ont souvent un meilleur support d’atténuation et peuvent réduire le coût de performance, mais « immunisé » est trop fort. La sécurité est une cible mouvante, et de nouvelles fonctionnalités peuvent introduire de nouveaux chemins de fuite.

10) Si la performance est critique, quel est le meilleur mouvement à long terme ?

Achetez la solution là où ça compte : générations CPU plus récentes, hôtes dédiés pour workloads sensibles, et choix d’architecture qui réduisent les traversées noyau. Désactiver les atténuations n’est rarement une stratégie stable.

Prochaines étapes pratiques

Si vous voulez moins de surprises, ne visez pas la prédiction parfaite. Visez la vérification rapide et le déploiement contrôlé.

  1. Mettez en place un audit continu des atténuations en scrapant /sys/devices/system/cpu/vulnerabilities/*, /proc/cmdline et la révision microcode dans votre pipeline de métriques.
  2. Séparez les pools par génération CPU et baseline microcode. L’homogénéité est une fonctionnalité de performance et un contrôle de sécurité.
  3. Créez deux ou trois profils d’atténuation alignés sur les frontières de confiance, et appliquez-les via automatisation (labels de nœud, taints, règles de placement).
  4. Construisez un processus canari pour les mises à jour noyau et microcode avec des benchmarks de charge réelle et un suivi de la latence tail.
  5. Décidez explicitement de la position SMT pour chaque pool, consignez-la et rendez la dérive détectable.

L’ère de Spectre ne s’est pas terminée. Elle a mûri. Les équipes qui traitent la sécurité CPU comme tout autre problème de production — inventaire, canaris, observabilité et contrôles ennuyeux — sont celles qui dorment.

VoIP sur VPN : arrêter l’audio robotique avec MTU, gigue et notions de QoS

Vous connaissez ce son. L’appel commence bien, puis quelqu’un se met à ressembler à un fax qui auditionne pour un film de robots.
Tout le monde accuse « le VPN », puis le FAI, puis le softphone, puis la phase de la lune. En réalité, le coupable est souvent ennuyeux :
un décalage MTU/MSS, de la gigue due au bufferbloat, ou une QoS qui ne survit pas au passage dans le tunnel.

J’exploite des réseaux en production où la voix n’est qu’une charge de travail — jusqu’à ce qu’elle cesse de l’être. La voix punit les hypothèses paresseuses.
Elle se fiche que votre test de débit soit excellent ; elle exige que les petits paquets arrivent à l’heure, de façon constante, avec peu de pertes et sans réordonnancement.

Un modèle mental qui prédit vraiment les pannes

Si vous ne retenez qu’une chose : la voix est un flux temps réel qui circule sur un réseau best-effort. Un VPN ajoute des en-têtes, masque les marquages QoS internes à moins que vous ne les préserviez volontairement, et peut altérer le rythme d’envoi des paquets.
Les modes de défaillance habituels ne sont pas mystérieux. Ce sont de la physique et des files d’attente.

Ce que « audio robotique » signifie généralement

« Robotique » est rarement un problème de qualité du codec. C’est l’action de la perte de paquets et du mécanisme de dissimulation.
L’audio RTP arrive en petits paquets (souvent 20 ms d’audio par paquet). Perdre quelques paquets, subir des pics de gigue, le jitter buffer s’allonge, le décodeur devine, et vous entendez le robot.
La voix peut tolérer un peu de perte ; elle ne peut tout simplement pas bien la masquer.

La pile VoIP-sur-VPN en un diagramme (conceptuel)

Pensez au paquet comme à un ensemble d’enveloppes imbriquées :

  • Interne : signalisation SIP + média RTP (souvent UDP) avec des marquages DSCP que vous souhaitez conserver
  • Ensuite : votre enveloppe VPN (WireGuard/IPsec/OpenVPN) ajoute une surcharge et peut modifier le MTU
  • Externe : files d’attente de l’ISP et d’internet (où vit le bufferbloat) et où la QoS peut ne pas s’appliquer
  • Points de terminaison : softphone ou téléphone IP, et un PBX/ITSP

La casse se produit généralement dans un des trois endroits :
(1) taille (MTU/fragmentation),
(2) temporisation (gigue/files d’attente),
(3) priorisation (QoS/DSCP et shaping).

Paraphrase d’une idée de W. Edwards Deming : « Sans données, vous n’êtes qu’une autre personne avec une opinion. » Traitez les problèmes de voix comme des incidents : mesurez, isolez, changez une variable, re-mesurez.

Playbook de diagnostic rapide

Quand le PDG dit « les appels sont cassés », vous ne commencez pas par débattre des codecs. Vous commencez par réduire le périmètre affecté et localiser la file d’attente.
Voici l’ordre qui trouve rapidement les causes racines.

Première étape : confirmer s’il s’agit de perte, de gigue ou de MTU

  1. Vérifiez les statistiques RTP dans le client/PBX : pourcentage de perte, gigue, paquets en retard. Si vous n’avez pas cela, capturez des paquets et calculez-les (ensuite).
    Si vous observez même 1–2% de perte pendant les moments « robotiques », traitez-le comme un problème réseau jusqu’à preuve du contraire.
  2. Exécutez un test rapide de path MTU à travers le VPN. Si PMTUD est cassé, vous aurez des paquets trop grands mis en black-hole, surtout sur des VPN basés sur UDP.
  3. Vérifiez le délai de mise en file sous charge sur le lien le plus étroit (généralement l’upload de l’utilisateur). Le bufferbloat est le tueur silencieux de la voix.

Deuxième étape : isoler où ça se casse

  1. Contournez le VPN pour un appel test (split tunnel ou politique temporaire). Si la voix s’améliore drastiquement, concentrez-vous sur la surcharge du tunnel, le MTU, et la gestion QoS aux bords du tunnel.
  2. Comparez filaire vs Wi‑Fi. Si le Wi‑Fi est pire, vous êtes dans la contention d’airtime et les retransmissions. Corrigez cela séparément.
  3. Testez depuis un réseau connu bon (un circuit de labo, un autre FAI, ou une VM cloud exécutant un softphone). Si c’est propre, le problème est au bord utilisateur.

Troisième étape : appliquer les « correctifs ennuyeux »

  • Fixez explicitement le MTU de l’interface VPN et appliquez le clamp MSS TCP quand pertinent.
  • Appliquez une gestion de file intelligente (fq_codel/cake) au point de goulot réel et shapez légèrement en-dessous du débit nominal.
  • Marquez le trafic voix et priorisez-le là où vous contrôlez la file (souvent le bord WAN), pas seulement en souhaitant que ça marche.

Blague #1 : Un VPN, c’est comme une valise — si vous continuez d’empiler des en-têtes, la fermeture éclair (MTU) finira par lâcher au pire moment.

MTU, MSS et fragmentation : pourquoi « robotique » signifie souvent « petite perte »

Les problèmes de MTU ne ressemblent pas toujours à un « impossible de se connecter ». Ils peuvent ressembler à « ça se connecte, mais parfois ça sonne hanté ».
C’est parce que la signalisation peut survivre alors que certains paquets média ou re-invites sont perdus, ou parce que la fragmentation augmente la sensibilité aux pertes.

Ce qui change quand vous ajoutez un VPN

Chaque tunnel ajoute une surcharge :

  • WireGuard ajoute un en-tête UDP/IP externe plus l’overhead WireGuard.
  • IPsec ajoute overhead ESP/AH (plus éventuellement encapsulation UDP pour NAT-T).
  • OpenVPN ajoute un overhead en espace utilisateur et peut ajouter du framing supplémentaire selon le mode.

Le paquet interne qui passait en MTU 1500 peut ne plus rentrer. Si votre chemin ne gère pas la fragmentation comme vous le pensez, quelque chose est supprimé.
Et UDP ne retransmet pas ; il vous déçoit en temps réel.

Path MTU discovery (PMTUD) et ses échecs

PMTUD dépend des messages ICMP « Fragmentation Needed » (IPv4) ou Packet Too Big (IPv6). Beaucoup de réseaux bloquent ou limitent ICMP.
Résultat : vous envoyez des paquets trop grands, les routeurs les jettent, et l’expéditeur ne l’apprend jamais. C’est ce qu’on appelle un « PMTUD black hole ».

Pourquoi le RTP n’est généralement pas « trop gros » — mais souffre quand même

Les paquets voix RTP sont typiquement petits : des dizaines à quelques centaines d’octets de payload, plus en-têtes. Pourquoi le MTU affecte-t-il donc les appels ?

  • Signalisation et changements de session (SIP INVITE/200 OK avec SDP, enregistrements TLS) peuvent devenir volumineux.
  • L’encapsulation VPN peut fragmenter des paquets même modérés, augmentant la probabilité de perte.
  • Des pics de gigue surviennent quand fragmentation et réassemblage interagissent avec des files congestionnées.
  • Certains softphones agrègent ou envoient des paquets UDP plus grands selon certains réglages (comfort noise, SRTP, ou ptime inhabituel).

Conseils exploitables

  • Pour WireGuard, commencez par MTU 1420 si vous n’êtes pas sûr. Ce n’est pas magique ; c’est une valeur conservatrice qui évite les pièges d’overhead courants.
  • Pour OpenVPN, soyez explicite avec le MTU du tunnel et le clamp MSS pour les flux TCP qui traversent le tunnel.
  • Ne « baissez pas le MTU partout » aveuglément. Vous pouvez corriger un chemin et en nuire à un autre. Mesurez, puis définissez.

Gigue, bufferbloat et pourquoi les tests de vitesse mentent

Vous pouvez avoir 500 Mbps en download et toujours sonner comme si vous appeliez depuis un sous-marin. La voix a besoin d’une faible variation de latence, pas de chiffres impressionnants.
L’ennemi pratique numéro un est le bufferbloat : des files trop profondes dans routeurs/modems qui se remplissent sous charge et ajoutent des centaines de millisecondes de délai.

Gigue vs latence vs perte de paquets

  • Latence : le temps qu’un paquet met pour aller d’un bout à l’autre.
  • Gigue : la variation de cette latence paquet par paquet.
  • Perte : paquets qui n’arrivent jamais (ou arrivent trop tard pour être utiles).

Les codecs vocaux utilisent des jitter buffers. Ces buffers peuvent lisser la variation jusqu’à un certain point, au prix d’un délai ajouté.
Quand la gigue devient importante, les buffers s’allongent (augmentant la latence) ou laissent tomber les paquets en retard (augmentant la perte). Dans les deux cas : audio robotique.

Où naît la gigue

La plupart des gigue liées à la VoIP-sur-VPN ne viennent pas « d’internet ». Elles viennent de la file d’attente du bord :

  • Routeur domicile de l’utilisateur avec un buffer monté en amont
  • Pare-feu de succursale effectuant inspection et buffering
  • Concentrateur VPN saturé côté CPU provoquant des délais d’ordonnancement des paquets
  • Contention/retransmissions Wi‑Fi (semblera comme de la gigue et de la perte)

Gestion de file qui fonctionne réellement

Si vous contrôlez le goulot, vous pouvez corriger la voix.
Les algorithmes SQM comme fq_codel et cake empêchent activement les files de s’allonger indéfiniment et maintiennent une latence stable sous charge.

L’astuce : vous devez shapez légèrement en-dessous du débit réel du lien pour que votre appareil — et non le modem ISP — devienne le goulot et contrôle donc la file.
Sinon, vous demandez poliment au modem de se contenir. Il ne le fera pas.

Blague #2 : Le bufferbloat, c’est ce qui arrive quand votre routeur thésaurise des paquets comme s’ils étaient des antiquités de collection.

Notions de QoS/DSCP pour la voix via VPN (et ce qui est supprimé)

La QoS n’est pas une case magique « rendre ça bon ». C’est une façon de décider ce qui souffre en premier quand le lien est congestionné.
C’est tout. S’il n’y a pas de congestion, la QoS ne change rien.

DSCP et le mythe de la « QoS de bout en bout »

La voix marque souvent le RTP en DSCP EF (Expedited Forwarding) et le SIP en CS3/AF31 selon votre politique.
Dans votre LAN, cela peut aider. Sur Internet, la plupart des fournisseurs l’ignorent. À travers un VPN, il se peut qu’il ne survive même pas à l’encapsulation.

Ce que vous pouvez contrôler

  • LAN edge : prioriser la voix depuis les téléphones/softphones vers votre passerelle VPN.
  • WAN egress de la passerelle VPN : shaper et prioriser les paquets externes correspondant aux flux voix.
  • Succursale/edge utilisateur : si vous le gérez, déployez SQM et marquez la voix localement.

Spécificités VPN : marquages internes vs externes

Beaucoup d’implémentations de tunnel encapsulent les paquets internes dans un paquet externe. Le paquet externe est celui que l’ISP voit et forwarde.
Si le paquet externe n’est pas marqué (ou s’il est marqué puis supprimé), votre « EF » interne n’est que décoratif.

L’approche praticable :

  • Classez la voix avant le chiffrement quand c’est possible, puis appliquez la priorité au flux chiffré (en-tête externe) à la sortie.
  • Préservez DSCP à travers le tunnel si votre équipement le supporte et si la politique le permet.
  • Ne faites pas confiance à Wi‑Fi WMM pour vous sauver si votre file d’attente d’upload fond.

Prudence QoS : vous pouvez empirer les choses

Une mauvaise politique QoS peut priver le trafic de contrôle, ou créer des micro-bursts et du réordonnancement. La voix aime la priorité, mais elle aime aussi la stabilité.
Gardez les classes simples : voix, interactif, bulk. Ensuite shapez.

Tâches pratiques : commandes, sorties et décisions

Voici des tâches « lancez-les maintenant ». Chacune inclut une commande, ce que la sortie vous dit, et la décision à prendre.
Utilisez-les sur des endpoints Linux, des passerelles VPN, ou des hôtes de dépannage. Adaptez les noms d’interfaces et IP à votre environnement.

Task 1: Confirm interface MTU on the VPN tunnel

cr0x@server:~$ ip link show dev wg0
4: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/none

Signification : le MTU de wg0 est 1420. Bon point de départ conservateur pour WireGuard.
Décision : Si le MTU d’un tunnel est 1500, supposez un problème sauf si vous avez prouvé que le chemin le supporte. Si l’audio robotique corrèle avec certains chemins, testez un MTU plus bas.

Task 2: Measure path MTU with “do not fragment” ping (IPv4)

cr0x@server:~$ ping -M do -s 1372 -c 3 10.20.30.40
PING 10.20.30.40 (10.20.30.40) 1372(1400) bytes of data.
1380 bytes from 10.20.30.40: icmp_seq=1 ttl=63 time=18.4 ms
1380 bytes from 10.20.30.40: icmp_seq=2 ttl=63 time=18.7 ms
1380 bytes from 10.20.30.40: icmp_seq=3 ttl=63 time=18.2 ms

--- 10.20.30.40 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms

Signification : un paquet de 1400 octets (headers inclus) passe sans fragmentation.
Décision : Augmentez -s jusqu’à l’échec pour trouver le maximum. Réglez le MTU du tunnel en dessous de cela moins l’overhead d’encapsulation.

Task 3: Observe PMTUD failure symptoms (IPv4)

cr0x@server:~$ ping -M do -s 1472 -c 2 10.20.30.40
PING 10.20.30.40 (10.20.30.40) 1472(1500) bytes of data.
ping: local error: message too long, mtu=1420
ping: local error: message too long, mtu=1420

--- 10.20.30.40 ping statistics ---
2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1013ms

Signification : votre MTU d’interface locale est 1420 ; le noyau refuse d’envoyer plus grand avec DF activé.
Décision : Si des applications envoient quand même des paquets plus grands (l’encapsulation VPN UDP peut le faire), clamp ou configurez MTU/MSS pour les empêcher.

Task 4: Check TCP MSS clamping rules (iptables)

cr0x@server:~$ sudo iptables -t mangle -S | grep -i mss
-A FORWARD -o wg0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

Signification : les paquets SYN TCP voient leur MSS clampée selon le PMTU.
Décision : Si vous transportez SIP sur TCP/TLS via le VPN et observez des blocages ou retransmissions, activez ceci. Cela ne résoudra pas RTP (UDP), mais stabilisera la signalisation.

Task 5: Verify DSCP markings on outbound packets

cr0x@server:~$ sudo tcpdump -ni eth0 -vv udp and portrange 10000-20000 -c 5
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:10:41.112233 IP (tos 0xb8, ttl 63, id 44211, offset 0, flags [DF], proto UDP (17), length 214) 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186
12:10:41.132244 IP (tos 0xb8, ttl 63, id 44212, offset 0, flags [DF], proto UDP (17), length 214) 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186

Signification : TOS 0xb8 correspond à DSCP EF (46). Votre hôte marque le RTP.
Décision : Vérifiez ensuite si le marquage survit à l’encapsulation et si votre file WAN l’honore. S’il disparaît sur le paquet externe, vous devez faire de la QoS à la sortie du tunnel, pas espérer.

Task 6: Confirm DSCP on the VPN outer packet

cr0x@server:~$ sudo tcpdump -ni eth0 -vv udp port 51820 -c 5
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:12:03.220011 IP (tos 0x00, ttl 64, id 12001, offset 0, flags [DF], proto UDP (17), length 208) 203.0.113.5.51820 > 203.0.113.9.51820: UDP, length 180
12:12:03.240022 IP (tos 0x00, ttl 64, id 12002, offset 0, flags [DF], proto UDP (17), length 208) 203.0.113.5.51820 > 203.0.113.9.51820: UDP, length 180

Signification : les paquets externes ne sont pas marqués (tos 0x00). Même si le RTP interne est EF, l’ISP ne voit que l’externe.
Décision : Appliquez la classification sur la passerelle VPN : identifiez les flux voix avant le chiffrement (ou par heuristiques de port/peer) et définissez DSCP/priorité à la sortie.

Task 7: Identify the real bottleneck and current qdisc

cr0x@server:~$ tc qdisc show dev eth0
qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn

Signification : fq_codel est actif. C’est une bonne base pour la latence sous charge.
Décision : Si vous voyez pfifo_fast ou un qdisc fournisseur profond en bord WAN, prévoyez de déployer shaping + fq_codel/cake là où la congestion survient.

Task 8: Check qdisc stats for drops/overlimits (shaping trouble)

cr0x@server:~$ tc -s qdisc show dev eth0
qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn
 Sent 98234123 bytes 84521 pkt (dropped 213, overlimits 0 requeues 12)
 backlog 0b 0p requeues 12
  maxpacket 1514 drop_overlimit 213 new_flow_count 541 ecn_mark 0

Signification : Quelques drops sont survenus. Les drops ne sont pas toujours mauvais — les drops contrôlés peuvent prévenir une latence massive. Mais drops + audio robotique suggèrent que vous perdez du RTP, pas du bulk.
Décision : Ajoutez de la classification pour que la voix ait la priorité (ou au moins l’isolation), et assurez-vous que le taux de shaping correspond à l’uplink réel.

Task 9: Quick jitter and loss check with mtr (baseline)

cr0x@server:~$ mtr -rwzc 50 203.0.113.9
Start: 2025-12-28T12:20:00+0000
HOST: server                          Loss%   Snt   Last   Avg  Best  Wrst StDev
  1. 192.0.2.1                         0.0%    50    1.1   1.3   0.9   3.8   0.6
  2. 198.51.100.1                      0.0%    50    8.2   8.5   7.9  13.4   1.1
  3. 203.0.113.9                       0.0%    50   19.0  19.2  18.6  26.8   1.4

Signification : Pas de perte, latence stable, faible gigue (StDev). Bon baseline.
Décision : Si vous voyez de la perte au saut 1 sous charge, c’est votre LAN/Wi‑Fi/routeur. Si la perte commence plus loin, c’est en amont — encore peut-être corrigeable avec du shaping à votre bord.

Task 10: See if VPN gateway CPU is causing packet scheduling delays

cr0x@server:~$ mpstat -P ALL 1 5
Linux 6.5.0 (vpn-gw) 	12/28/2025 	_x86_64_	(8 CPU)

12:21:01     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
12:21:02     all   18.20    0.00   22.40    0.10    0.00   21.70    0.00    0.00    0.00   37.60
12:21:02       0   20.00    0.00   28.00    0.00    0.00   30.00    0.00    0.00    0.00   22.00

Signification : Un softirq élevé peut indiquer un fort traitement de paquets (chiffrement, forwarding).
Décision : Si softirq est saturé pendant les problèmes d’appel, envisagez d’activer le multiqueue, passer à un crypto plus rapide, ajouter de la marge CPU, ou réduire l’overhead VPN (MTU et offloads).

Task 11: Inspect NIC offloads (can break captures, sometimes timing)

cr0x@server:~$ sudo ethtool -k eth0 | egrep 'tso|gso|gro'
tcp-segmentation-offload: on
generic-segmentation-offload: on
generic-receive-offload: on

Signification : Les offloads sont activés. Généralement OK, mais peuvent perturber les captures et dans certains cas interagir mal avec les tunnels.
Décision : Pour un dépannage précis, désactivez temporairement GRO/LRO sur un hôte test, puis retestez. Ne désactivez pas les offloads sur des passerelles chargées sans plan.

Task 12: Check UDP receive errors and drops

cr0x@server:~$ netstat -su
Udp:
    128934 packets received
    12 packets to unknown port received
    0 packet receive errors
    4311 packets sent
UdpLite:
IpExt:
    InOctets: 221009331
    OutOctets: 198887112

Signification : Les erreurs de réception UDP sont nulles. Bien.
Décision : Si les erreurs de réception augmentent pendant les appels, vous pourriez atteindre des limites de buffers de sockets ou des drops noyau ; ajustez les buffers, corrigez la saturation CPU, ou réduisez la contention du trafic.

Task 13: Verify SIP/RTP packet rate during a call (sanity check)

cr0x@server:~$ sudo tcpdump -ni any udp portrange 10000-20000 -ttt -c 10
tcpdump: listening on any, link-type LINUX_SLL2, snapshot length 262144 bytes
 0.000000 IP 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186
 0.019884 IP 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186
 0.020042 IP 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186
 0.019901 IP 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186

Signification : Un timing inter-paquets autour de 20 ms suggère ptime=20ms (commun). De grands écarts indiquent de la gigue ou des délais d’ordonnancement.
Décision : Si le timing est irrégulier au point de capture proche de l’expéditeur, examinez CPU/ Wi‑Fi de l’émetteur. S’il est régulier à l’émetteur mais irrégulier au récepteur, c’est le réseau/les files d’attente.

Task 14: Identify whether traffic is going through the VPN or bypassing it

cr0x@server:~$ ip route get 198.51.100.20
198.51.100.20 via 10.10.0.1 dev wg0 src 10.10.0.2 uid 1000
    cache

Signification : La route vers le point média utilise wg0.
Décision : Si votre test « bypass VPN » route encore via wg0, vous n’avez rien contourné. Corrigez le policy routing/split tunnel, puis comparez la qualité d’appel.

Task 15: Confirm MTU on the physical WAN interface (and spot jumbo mismatch)

cr0x@server:~$ ip link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff

Signification : L’interface WAN est en MTU standard 1500.
Décision : Si vous êtes en PPPoE ou certains liens cellulaires, le MTU WAN peut être plus petit (1492, 1428, etc.). Cela vous pousse à abaisser le MTU du tunnel.

Task 16: Spot bufferbloat under load with a simple ping while saturating uplink

cr0x@server:~$ ping -i 0.2 -c 20 1.1.1.1
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=57 time=18.9 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=57 time=210.4 ms
64 bytes from 1.1.1.1: icmp_seq=3 ttl=57 time=245.7 ms
64 bytes from 1.1.1.1: icmp_seq=4 ttl=57 time=198.1 ms

--- 1.1.1.1 ping statistics ---
20 packets transmitted, 20 received, 0% packet loss, time 3812ms
rtt min/avg/max/mdev = 18.4/156.2/265.1/72.9 ms

Signification : Les latences explosent sous charge : bufferbloat classique.
Décision : Déployez du SQM shaping sur l’uplink et priorisez la voix ; ne perdez pas de temps à chasser les codecs.

Trois mini-récits d’entreprise depuis le terrain

Incident #1 : La mauvaise hypothèse (MTU « ça ne peut pas être, on utilise 1500 partout »)

Une entreprise de taille moyenne a migré un centre d’appels vers des softphones sur un VPN full-tunnel. Ça a fonctionné en pilote. Puis ils l’ont déployé à quelques centaines d’agents distants.
En moins d’une journée, la file d’assistance est devenue un second centre d’appels — mais avec un audio pire.

L’équipe réseau a d’abord tenu l’hypothèse classique : « Le MTU ne peut pas être en cause ; Ethernet c’est 1500, et le VPN est bien configuré. »
Ils se sont concentrés sur le fournisseur SIP, puis ont accusé le Wi‑Fi domestique, puis ont tenté de changer les codecs.
Les améliorations étaient aléatoires, ce qui est le pire type d’amélioration car cela encourage les superstitions.

Le motif qui a résolu l’affaire : l’audio robotique montait pendant certains flux d’appel — transferts, appels consultatifs, et lors des renégociations SRTP du softphone.
C’est à ces moments que les paquets de signalisation grossissaient, et dans certains scénarios le chemin VPN requérait de la fragmentation. Les messages ICMP « fragmentation needed » étaient bloqués sur le bord utilisateur par un réglage « sécurité ».
PMTUD black holes. Pas glamour. Très réel.

La correction fut ennuyeuse et décisive : définir un MTU conservateur pour le tunnel, clamp MSS pour la signalisation TCP, et documenter « ne pas bloquer tout ICMP » dans la baseline d’accès distant.
Ils ont aussi ajouté un test d’une page : DF ping à travers le tunnel vers un endpoint connu. Ça a permis d’attraper des régressions plus tard.

Leçon : « 1500 partout » n’est pas une conception. C’est un souhait.

Incident #2 : L’optimisation qui s’est retournée contre eux (prioriser la voix… en accélérant tout)

Une autre organisation disposait d’une passerelle VPN capable et voulait « une qualité voix premium ». Quelqu’un a activé l’accélération matérielle et les fast-paths sur le pare-feu bord.
Le débit a augmenté. La latence dans un test synthétique a baissé. Tout le monde a célébré.

Deux semaines plus tard, plaintes : « Audio robotique seulement pendant les gros uploads. » Ce détail comptait.
Sous charge, le fast path contournait des parties de la pile QoS et de gestion de file. Le bulk et la voix atterrissaient dans la même file profonde côté WAN.
L’accélération a amélioré le pic de débit, mais elle a supprimé le mécanisme qui gardait la latence stable.

Les ingénieurs ont fait ce que font les ingénieurs : ils ont ajouté plus de règles QoS. Plus de classes. Plus de correspondances. Ça a empiré.
La classification coûtait du CPU sur le slow path, tandis que le fast path continuait de renvoyer la majeure partie du trafic dans la même file de goulot.
Maintenant ils avaient de la complexité et toujours du bufferbloat.

La solution finale n’était pas « plus de QoS ». C’était : shaper l’uplink juste en dessous de la capacité réelle, activer un qdisc moderne, et garder le modèle de classes simple.
Puis décider si l’accélération était compatible avec cette politique. Là où ce n’était pas le cas, la voix a gagné.

Leçon : optimiser le débit sans respecter le comportement des files est la recette pour construire un moyen plus rapide de sonner terriblement.

Incident #3 : La pratique ennuyeuse qui a sauvé la situation (tests standard + contrôle de changement)

Une entreprise globale utilisait la voix sur IPsec entre succursales et le siège. Rien d’extraordinaire. La différence clé : ils traitaient la voix comme un service de production.
Chaque changement réseau avait une checklist pré-flight et post-flight, incluant quelques tests VoIP pertinents.

Un vendredi, un FAI a remplacé du matériel d’accès dans un bureau régional. Les utilisateurs ont remarqué un « léger robot » sur les appels.
L’équipe locale a exécuté les tests standards : ping idle vs ping sous charge uplink, DF pings pour MTU, et un rapide check DSCP sur la sortie WAN.
Ils n’ont pas débattu. Ils ont mesuré.

Les données ont montré que PMTUD était cassé sur le nouvel accès, et que le buffer upstream était plus profond qu’avant. Deux problèmes. Tous deux actionnables.
Ils ont légèrement abaissé le MTU du tunnel, activé MSS clamping, et ajusté le shaping pour maintenir la latence stable. Les appels se sont stabilisés immédiatement.

Lundi, ils ont escaladé vers le FAI avec des preuves nettes : horodatages, seuil d’échec MTU, et graphiques de latence sous charge.
Le FAI a fini par corriger la gestion d’ICMP plus tard. Mais l’entreprise n’a pas eu à attendre pour retrouver une voix utilisable.

Leçon : la fonctionnalité de fiabilité la plus efficace est un test répétable que vous lancez réellement.

Erreurs courantes : symptôme → cause racine → correction

1) Symptom: robotic audio during uploads or screen sharing

  • Root cause: bufferbloat on upstream; voice packets stuck behind bulk traffic in a deep queue.
  • Fix: enable SQM (fq_codel/cake) and shape slightly below uplink; add simple priority for RTP/SIP on WAN egress.

2) Symptom: call connects, then audio drops or becomes choppy after a minute

  • Root cause: MTU/PMTUD black hole triggered by rekey, SRTP renegotiation, or SIP re-INVITE size increase.
  • Fix: set tunnel MTU explicitly; allow ICMP “frag needed”/PTB; for TCP signaling, clamp MSS.

3) Symptom: one-way audio (you hear them, they don’t hear you)

  • Root cause: NAT traversal issue or asymmetric routing; RTP pinned to wrong interface; firewall state/timeouts for UDP.
  • Fix: ensure correct NAT settings (SIP ALG usually off), confirm routes, increase UDP timeout on stateful devices, validate symmetric RTP if supported.

4) Symptom: fine on wired, bad on Wi‑Fi

  • Root cause: airtime contention, retries, or power-save behavior; VPN adds overhead and jitter sensitivity.
  • Fix: move calls to 5 GHz/6 GHz, reduce channel contention, disable aggressive client power save for voice devices, prefer wired for call-heavy roles.

5) Symptom: only remote users on a certain ISP have issues

  • Root cause: ISP upstream shaping/CGNAT behavior, poor peering, or ICMP filtering affecting PMTUD.
  • Fix: reduce tunnel MTU, enforce shaping at user edge if managed, test alternative transport/port, and collect evidence to escalate.

6) Symptom: “QoS is enabled” but voice still degrades under load

  • Root cause: QoS configured on the LAN while congestion is on the WAN; or DSCP marked on inner packets but not on outer tunnel packets.
  • Fix: prioritize at the egress queue that is actually congested; map/classify voice to outer packets; verify with tcpdump and qdisc stats.

7) Symptom: sporadic bursts of robot, especially at peak hours

  • Root cause: microbursts and queue oscillation; VPN concentrator CPU contention; or upstream congestion.
  • Fix: check softirq/CPU, enable pacing/SQM, ensure adequate gateway headroom, and avoid overcomplicated class hierarchies.

8) Symptom: calls are fine, but “hold/resume” breaks or transfers fail

  • Root cause: SIP signaling fragmentation or MTU issues affecting larger SIP messages; sometimes SIP over TCP/TLS affected by MSS.
  • Fix: MSS clamping, reduce MTU, allow ICMP PTB, and validate SIP settings (and disable SIP ALG where it mangles packets).

Listes de contrôle / plan étape par étape

Étape par étape : stabiliser la VoIP sur VPN en une semaine (pas un trimestre)

  1. Choisissez un utilisateur représentatif en échec et reproduisez le problème à la demande (upload pendant un appel suffit généralement).
    Pas de reproductibilité, pas de progrès.
  2. Collectez les statistiques voix (depuis softphone/PBX) : gigue, perte, dissimulation, RTT si disponible.
    Décidez : perte (réseau) vs CPU (endpoint) vs signalisation (SIP/MTU).
  3. Confirmez le routage : assurez-vous que les médias traversent bien le VPN comme vous le pensez. Corrigez la confusion split tunnel tôt.
  4. Mesurez le path MTU à travers le tunnel en utilisant des DF pings vers des endpoints connus.
    Décidez d’un MTU sûr (conservateur vaut mieux que théorique).
  5. Définissez le MTU du tunnel explicitement aux deux extrémités (et documentez pourquoi).
    Évitez le réglage « auto » sauf si vous l’avez testé sur tous les types d’accès (ADSL, LTE, Wi‑Fi d’hôtel, etc.).
  6. Clamp MSS TCP sur le tunnel pour les flux TCP forwardés (SIP/TLS, provisioning, management).
  7. Trouvez le vrai goulot (généralement l’uplink). Utilisez ping-under-load pour confirmer le bufferbloat.
  8. Déployez du SQM shaping au goulot, légèrement en dessous du débit, avec fq_codel ou cake.
  9. Gardez les classes QoS simples et priorisez la voix uniquement là où cela compte : la file de sortie.
  10. Vérifiez la gestion DSCP avec des captures : marquage interne, marquage externe, et si la file respecte cela.
  11. Retestez le cas d’échec original (appel + upload) et confirmez les améliorations de gigue/perte.
  12. Déployez progressivement avec un groupe canari et un plan de rollback. Les changements voix sont visibles par les utilisateurs immédiatement ; traitez-les comme un déploiement en production.

Checklist opérationnelle : à chaque intervention sur VPN ou WAN

  • Enregistrez les réglages MTU actuels (WAN + tunnel) et les politiques qdisc/shaping.
  • Exécutez un test DF ping MTU à travers le tunnel vers un endpoint stable.
  • Exécutez ping idle vs ping sous charge pour mesurer la régression bufferbloat.
  • Capturez 30 secondes de RTP pendant un appel test et vérifiez pertes/pics de gigue.
  • Confirmez DSCP sur le paquet externe côté WAN (si vous comptez sur le marquage).
  • Vérifiez softirq CPU de la passerelle sous charge.

Faits intéressants et contexte historique

  • Fait 1 : RTP (Real-time Transport Protocol) a été normalisé au milieu des années 1990 pour transporter des médias temps réel sur IP.
  • Fait 2 : SIP a gagné en popularité en partie parce qu’il ressemblait à HTTP pour les appels — textuel, extensible — parfait pour les fonctionnalités, parfois pénible pour le MTU.
  • Fait 3 : Une grande partie de la douleur PMTUD vient des pratiques de filtrage ICMP devenues courantes comme réponse de sécurité à l’époque précoce d’internet.
  • Fait 4 : Les premiers déploiements VoIP s’appuyaient souvent sur les marquages DiffServ (DSCP) en entreprise, mais la « QoS sur l’internet public » n’est jamais devenue fiable à grande échelle.
  • Fait 5 : L’adoption des VPN a explosé pour le travail à distance, et les plaintes sur la qualité voix ont suivi car les uplinks grand public sont typiquement le segment le plus étroit et le plus sujet au bufferbloat.
  • Fait 6 : WireGuard est devenu populaire en partie parce qu’il est léger et rapide, mais le « crypto rapide » n’annule pas les « mauvaises files ».
  • Fait 7 : Le bufferbloat a été identifié et nommé parce que le matériel grand public livrait des buffers trop profonds qui amélioraient les benchmarks de débit tout en détruisant les applications sensibles à la latence.
  • Fait 8 : Les qdiscs Linux modernes comme fq_codel ont été conçus pour maintenir la latence bornée sous charge, ce qui est crucial pour la voix et le gaming.
  • Fait 9 : Beaucoup de designs VPN d’entreprise supposaient historiquement un underlay 1500 octets ; la généralisation de PPPoE, LTE, et l’empilement de tunnels a rendu cette hypothèse fragile.

FAQ

1) Pourquoi l’audio est-il « robotique » et pas juste faible ou retardé ?

Parce que vous entendez la dissimulation de perte de paquets. Le décodeur devine les trames audio manquantes. Un son faible/volume bas est généralement un problème de gain ou de périphérique ; le robotique est généralement perte/gigue.

2) Quel pourcentage de perte devient audible pour la VoIP ?

Cela dépend du codec et de la dissimulation, mais même ~1% de perte peut être perceptible, surtout quand elle est en rafales. 0,1% stable peut passer ; 2% en rafales souvent non.

3) Le MTU n’est-il pas seulement un problème TCP ? RTP, c’est UDP.

Le MTU affecte aussi UDP. Si les paquets dépassent le path MTU et que PMTUD est cassé, ils sont jetés. De plus, la fragmentation augmente la sensibilité aux pertes et la gigue quand les fragments se retrouvent en concurrence dans les files.

4) Dois-je simplement mettre MTU à 1200 et passer à autre chose ?

Non. Vous perdrez en efficacité et vous pourriez casser d’autres protocoles ou chemins inutilement. Mesurez le path MTU, choisissez une valeur sûre et documentez-la. Conservateur, pas extrême.

5) Le marquage DSCP aide-t-il sur l’internet public ?

Parfois dans le domaine d’un FAI, souvent non de bout en bout. La victoire fiable est de prioriser là où vous contrôlez la file : votre sortie WAN et les bords gérés.

6) La QoS peut-elle corriger la perte de paquets due à un mauvais FAI ?

La QoS ne peut pas créer de bande passante. Elle peut empêcher des pertes/gigue auto-infligées en gérant vos propres files. Si l’ISP droppe en amont, il faut un meilleur chemin ou un autre fournisseur.

7) Pourquoi ça casse seulement quand quelqu’un upload un fichier ?

L’upload sature l’amont. Les files en amont s’allongent, la latence et la gigue explosent, et le RTP arrive en retard. Les tests de vitesse masquent souvent ça car ils valorisent les buffers profonds.

8) WireGuard est-il automatiquement meilleur qu’OpenVPN pour la voix ?

WireGuard est généralement moins lourd et plus simple à raisonner, mais la qualité voix dépend surtout de la correction MTU, de la gestion des files, et de la stabilité du routage. On peut casser la voix sur n’importe quel VPN.

9) Quelle est la politique QoS la plus simple qui fonctionne réellement ?

Shapez la sortie WAN légèrement en dessous du débit réel, puis priorisez la voix (RTP) au-dessus du bulk. Gardez le modèle de classes réduit. Vérifiez avec les stats qdisc et des appels réels.

10) Comment prouver que c’est le MTU et pas « le codec » ?

Reproduisez avec des DF ping et observez les échecs autour de tailles de paquets spécifiques ; corrélez avec des événements d’appel qui augmentent la taille de signalisation ; corrigez le MTU et voyez le problème disparaître sans toucher aux codecs.

Étapes pratiques suivantes

Si vous voulez des appels qui sonnent humains, traitez la voix comme un SLO de latence, pas comme une impression.

  1. Exécutez le playbook de diagnostic rapide sur un utilisateur affecté et capturez des preuves : MTU, gigue sous charge, comportement DSCP.
  2. Définissez explicitement le MTU du tunnel (commencez conservateur), et clamp MSS pour les chemins de signalisation TCP.
  3. Déployez du SQM shaping au goulot d’uplink avec fq_codel/cake et priorisez la voix sur cette file.
  4. Vérifiez avec des mesures (stats qdisc, tcpdump DSCP, mtr, et stats de gigue/perte du client), pas avec des impressions.
  5. Documentez : le MTU choisi, pourquoi il a été choisi, et les tests de régression. Le vous du futur sera fatigué et peu impressionné.

La plupart des « mystères » VoIP-over-VPN ne sont que des réseaux qui font ce que font les réseaux. Réduisez la taille des paquets, rendez les files plus intelligentes, et faites en sorte que vos priorités soient réelles là où la congestion se produit.

Délai d’attente des volumes NFS Docker : options de montage qui améliorent réellement la stabilité

Vous déployez un conteneur parfaitement banal. Il écrit quelques fichiers. Puis, à 02:17, tout se fige comme s’il attendait une autorisation.
df se bloque. Les threads de votre application s’accumulent. Les logs Docker n’indiquent rien d’utile. Le seul indice est une traînée de « nfs: server not responding ».

NFS dans des environnements conteneurisés échoue de manières qui ressemblent à des bugs applicatifs, des bugs du noyau ou des « vibes réseau ».
En général, ce n’est aucune de ces choses. C’est la sémantique de montage qui entre en collision avec des fautes réseau transitoires, des surprises DNS et un driver de volume qui ne vous dit pas ce qu’il a fait.

Pourquoi les volumes NFS Docker arrivent en timeout (et pourquoi ça semble aléatoire)

Les « volumes NFS » de Docker ne sont que des montages NFS Linux créés par l’hôte, puis bind-montés dans les conteneurs.
Ça paraît simple, et ça l’est—jusqu’à ce que vous vous rappeliez que NFS est un système de fichiers réseau avec une sémantique de retry,
des timeouts RPC, du verrouillage et de l’état.

La plupart des « timeouts » ne sont pas un unique timeout

Quand quelqu’un rapporte « NFS a time-out », il veut généralement dire une des choses suivantes :

  • Le client réessaie indéfiniment (hard mount) et le thread de l’application bloque en sommeil non interruptible (état D). Cela ressemble à un blocage.
  • Le client a abandonné (soft mount) et a renvoyé une erreur d’E/S. Cela ressemble à de la corruption, des écritures échouées ou « mon appli error aléatoirement ».
  • Le serveur est parti puis revenu, mais la vue du client sur l’export ne correspond plus (stale file handles). On a l’impression que « ça marchait hier ».
  • Problèmes de plomberie RPC (surtout NFSv3 : portmapper/rpcbind, mountd, lockd) provoquent des échecs partiels où certaines opérations fonctionnent et d’autres timeout.
  • Fluctuations de résolution de nom ou de routage provoquent des blocages intermittents qui se rétablissent seuls. Ce sont les pires car elles nourrissent les superstitions.

Docker amplifie les modes de panne

NFS est sensible au timing des montages. Docker est très bon pour démarrer des conteneurs rapidement, en parallèle, et parfois avant que le réseau soit prêt.
Si le montage NFS est déclenché à la demande, votre « démarrage de conteneur » devient « démarrage de conteneur plus réseau plus DNS plus réactivité du serveur ».
C’est acceptable en labo. En production, c’est une façon élaborée de transformer une petite gigue réseau en panne majeure.

Hard vs soft n’est pas un réglage de performance ; c’est une décision de risque

Pour la plupart des charges avec état, la valeur sûre par défaut est hard : continuer à réessayer, ne pas prétendre que les écritures ont réussi.
Mais les montages hard peuvent suspendre des processus quand le serveur est inaccessible. Votre travail consiste donc à rendre « serveur inaccessible » rare et bref,
et à rendre le montage résilient face au chaos normal des réseaux.

Il y a une idée paraphrasée de Werner Vogels (CTO d’Amazon) qui vaut la peine d’être gardée en tête : « Tout échoue, donc concevez pour l’échec. »
Les montages NFS sont précisément l’endroit où cette philosophie cesse d’être inspirante et devient une checklist.

Faits et contexte intéressants (brefs, concrets, utiles)

  • NFS précède les conteneurs de plusieurs décennies. Il est né dans les années 1980 pour partager des fichiers sur un réseau sans complexité d’état côté client.
  • NFSv3 est en grande partie sans état. Cela simplifiait certains basculements, mais repoussait la complexité vers des daemons auxiliaires (rpcbind, mountd, lockd).
  • NFSv4 a consolidé les canaux latéraux. v4 utilise typiquement un port bien connu (2049) et intègre verrouillage et état, ce qui améliore souvent la compatibilité avec les pare-feux et le NAT.
  • « Hard mount » est le défaut historique pour une raison. Perdre des données silencieusement est pire qu’attendre ; les montages hard privilégient la correction plutôt que la disponibilité.
  • Le client NFS Linux a plusieurs couches de timeouts. Il y a le timeout par RPC (timeo), le nombre de retransmissions (retrans) et des comportements de récupération de plus haut niveau.
  • Les stale file handles sont une taxe classique de NFS. Ils surviennent quand la correspondance inode/handle côté serveur change sous le client—fréquent après un basculement serveur ou des changements d’export.
  • NFS sur TCP n’a pas toujours été le défaut. UDP était populaire au départ ; TCP est maintenant le choix sensé pour la fiabilité et le contrôle de congestion.
  • Le DNS compte plus que vous ne le pensez. Les clients NFS peuvent mettre en cache la résolution nom→IP différemment de votre application ; un changement DNS en vol peut produire des symptômes « la moitié du monde fonctionne ».

Blague #1 : NFS, c’est comme une imprimante partagée au bureau—quand ça marche, personne ne le remarque ; quand ça ne marche pas, tout le monde a soudain des documents « business-critical » urgents.

Mode opératoire de diagnostic rapide (trouver le goulot vite)

L’objectif n’est pas de « collecter chaque métrique ». L’objectif est de décider, rapidement, si vous avez un problème de chemin réseau,
un problème serveur, un problème de sémantique de montage côté client, ou un problème d’orchestration Docker.
Voici l’ordre qui fait gagner du temps.

Première étape : confirmer que c’est NFS et pas l’application

  1. Sur l’hôte Docker, essayez un simple stat ou ls sur le chemin monté. Si ça bloque, ce n’est pas votre appli. C’est le montage.
  2. Vérifiez dmesg pour server not responding / timed out / stale file handle. Les messages noyau sont directs et généralement fiables.

Deuxième étape : décider « réseau vs serveur » avec un test

  1. Depuis le client, vérifiez la connectivité vers le port 2049 et (si vous utilisez NFSv3) rpcbind/portmapper. Si vous ne pouvez pas vous connecter, cessez de blâmer les options de montage.
  2. Depuis un autre hôte sur le même segment réseau, testez la même chose. Si le problème est isolé à un seul client, suspectez un pare-feu local, une exhaustion de conntrack, un MTU, ou une mauvaise route.

Troisième étape : vérifier la version du protocole et les options de montage

  1. Vérifiez si vous êtes en NFSv3 ou NFSv4. Beaucoup de timeouts « aléatoires » sont en réalité des problèmes rpcbind/mountd de NFSv3 dans les réseaux modernes.
  2. Confirmez hard, timeo, retrans, tcp, et si vous avez utilisé intr (comportement obsolète) ou d’autres flags legacy.

Quatrième étape : inspecter les logs et la saturation côté serveur

  1. La moyenne de charge serveur n’est pas suffisante. Regardez les threads NFS, la latence disque, et les pertes réseau.
  2. Si le serveur est un appliance NAS, identifiez s’il est CPU-bound (chiffrement, checksum) ou I/O-bound (disques, rebuild, suppression de snapshot).

Si vous faites ces quatre phases, vous pouvez généralement nommer la classe de panne en moins de dix minutes. La partie longue, c’est la politique.

Patrons de configuration Docker qui ne vous sabordent pas

Patron 1 : driver local Docker avec options NFS (correct, mais vérifiez ce qui a été monté)

Le driver local de Docker peut monter NFS en utilisant type=nfs et o=....
C’est courant dans Compose et Swarm.
Le piège : les gens supposent que Docker « fait quelque chose d’intelligent ». Il ne le fait pas. Il transmet les options à l’helper de montage.
Si l’helper de montage revient à une autre version ou ignore une option, vous pouvez ne pas le remarquer.

Patron 2 : pré-monter sur l’hôte puis bind-monter dans les conteneurs (souvent plus prévisible)

Si vous pré-montez via /etc/fstab ou des unités mount systemd, vous pouvez contrôler l’ordonnancement, les retries, et observer le montage directement.
Docker se contente ensuite de bind-monter un chemin local. Cela réduit la « magie Docker », ce qui est généralement bon pour dormir tranquille.

Patron 3 : séparer les montages par classe de workload

N’utilisez pas un export NFS et un jeu d’options pour tout.
Traitez NFS comme un service avec des SLO : métadonnées basse latence (caches CI), débit en masse (média), correction d’abord (données applicatives).
Différents montages, différentes options, différentes attentes.

Tâches pratiques : commandes, sortie et la décision que vous prenez

Ce sont les actions de l’astreinte qui transforment « NFS est instable » en une action claire. Exécutez-les sur l’hôte Docker sauf indication contraire.
Chaque tâche inclut (1) la commande, (2) ce que signifie la sortie, (3) la décision à prendre.

Task 1: Identify which mounts are NFS and how they’re configured

cr0x@server:~$ findmnt -t nfs,nfs4 -o TARGET,SOURCE,FSTYPE,OPTIONS
TARGET                SOURCE                     FSTYPE OPTIONS
/var/lib/docker-nfs    nas01:/exports/appdata     nfs4   rw,relatime,vers=4.1,rsize=1048576,wsize=1048576,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.10.8.21

Signification : Confirme la version NFS, le proto, et si vous êtes en hard ou soft. Montre aussi si rsize/wsize sont énormes et potentiellement non appariés.

Décision : Si vous voyez vers=3 de façon inattendue, prévoyez de passer à v4 ou auditez les ports rpcbind/mountd. Si vous voyez soft sur des workloads intensifs en écriture, changez cela.

Task 2: Confirm Docker volume configuration (what Docker thinks it asked for)

cr0x@server:~$ docker volume inspect appdata
[
  {
    "CreatedAt": "2026-01-01T10:12:44Z",
    "Driver": "local",
    "Labels": {},
    "Mountpoint": "/var/lib/docker/volumes/appdata/_data",
    "Name": "appdata",
    "Options": {
      "device": ":/exports/appdata",
      "o": "addr=10.10.8.10,vers=4.1,proto=tcp,hard,timeo=600,retrans=2,noatime",
      "type": "nfs"
    },
    "Scope": "local"
  }
]

Signification : C’est la configuration, pas la vérité. Les options de Docker peuvent être correctes alors que le montage réel diffère.

Décision : Comparez avec findmnt. Si elles diffèrent, dépannez l’helper de montage, les valeurs par défaut et le support noyau.

Task 3: Look for kernel NFS client errors right now

cr0x@server:~$ dmesg -T | egrep -i 'nfs:|rpc:|stale|not responding|timed out' | tail -n 20
[Fri Jan  3 01:58:41 2026] nfs: server nas01 not responding, still trying
[Fri Jan  3 01:59:12 2026] nfs: server nas01 OK

Signification : « Not responding, still trying » indique un hard mount en train de réessayer pendant une perturbation.

Décision : Si ces événements correspondent aux blocages applicatifs, enquêtez sur des pertes réseau ou des blocages serveur ; ne « fixez » pas l’appli.

Task 4: Confirm the process states during a hang (is it stuck in D-state?)

cr0x@server:~$ ps -eo pid,stat,comm,wchan:40 | egrep 'D|nfs' | head
 8421 D    php-fpm          nfs_wait_on_request
 9133 D    rsync            nfs_wait_on_request

Signification : État D avec nfs_wait_on_request indique un I/O noyau bloqué en attente de NFS.

Décision : Considérez cela comme un incident d’infrastructure. Redémarrer des conteneurs n’aidera pas si le montage est bloqué en hard.

Task 5: Check basic TCP connectivity to the NFS server

cr0x@server:~$ nc -vz -w 2 10.10.8.10 2049
Connection to 10.10.8.10 2049 port [tcp/nfs] succeeded!

Signification : Le port 2049 est joignable maintenant.

Décision : Si cela échoue pendant l’incident, vos options de montage ne sont pas le problème principal ; corrigez le routage, les ACL, le pare-feu ou la disponibilité du serveur.

Task 6: If using NFSv3, confirm rpcbind is reachable (common hidden dependency)

cr0x@server:~$ nc -vz -w 2 10.10.8.10 111
Connection to 10.10.8.10 111 port [tcp/sunrpc] succeeded!

Signification : rpcbind/portmapper joignable. Sans cela, les montages NFSv3 peuvent échouer ou se bloquer pendant la négociation.

Décision : Si le port 111 est bloqué et que vous êtes en v3, passez à v4 ou ouvrez les ports nécessaires correctement (et documentez-les).

Task 7: Identify NFS version negotiated and server address used (catch DNS surprises)

cr0x@server:~$ nfsstat -m
/var/lib/docker-nfs from nas01:/exports/appdata
 Flags: rw,hard,noatime,vers=4.1,rsize=1048576,wsize=1048576,namlen=255,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.10.8.21,local_lock=none

Signification : Confirme les paramètres négociés. Notez le nom du serveur vs l’IP, et tout comportement local_lock.

Décision : Si le montage utilise un nom d’hôte et que votre DNS est instable, basculez sur l’IP ou épinglez des entrées host—puis planifiez une meilleure stratégie DNS.

Task 8: Measure retransmits and RPC-level pain (is it packet loss?)

cr0x@server:~$ nfsstat -rc
Client rpc stats:
calls      retrans    authrefrsh
148233     912        148245

Signification : Les retransmits indiquent des RPC qui ont dû être renvoyés. Une augmentation de retrans corrèle avec perte, congestion ou blocage serveur.

Décision : Si retrans saute pendant des incidents, inspectez les pertes réseau et la charge serveur ; envisagez d’augmenter timeo modérément, pas de le diminuer.

Task 9: Check interface errors and drops (don’t guess)

cr0x@server:~$ ip -s link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    RX:  bytes packets errors dropped  missed   mcast
    128G  98M     0     127      0      1234
    TX:  bytes packets errors dropped carrier collsns
    141G  92M     0      84      0      0

Signification : Des drops en RX/TX peuvent suffire à déclencher « not responding » NFS sous charge.

Décision : Si les drops augmentent, enquêtez sur les rings NIC, les mismatches MTU, la congestion du switch ou la saturation CPU de l’hôte.

Task 10: Spot MTU mismatch quickly (jumbo frames are innocent until proven guilty)

cr0x@server:~$ ping -c 3 -M do -s 8972 10.10.8.10
PING 10.10.8.10 (10.10.8.10) 8972(9000) bytes of data.
From 10.10.8.21 icmp_seq=1 Frag needed and DF set (mtu = 1500)
From 10.10.8.21 icmp_seq=2 Frag needed and DF set (mtu = 1500)
From 10.10.8.21 icmp_seq=3 Frag needed and DF set (mtu = 1500)

--- 10.10.8.10 ping statistics ---
3 packets transmitted, 0 received, +3 errors, 100% packet loss, time 2043ms

Signification : Le MTU du chemin est 1500, mais votre hôte pense 9000. Cela provoque du blackholing et des stalls « aléatoires ».

Décision : Corrigez le MTU bout en bout ou baissez à 1500. Puis réévaluez la stabilité NFS avant de toucher aux options de montage.

Task 11: Confirm the server export exists and permissions are sane (server-side view)

cr0x@server:~$ showmount -e 10.10.8.10
Export list for 10.10.8.10:
/exports/appdata 10.10.8.0/24
/exports/shared  10.10.0.0/16

Signification : Montre les exports (utile surtout pour NFSv3 ; pour v4 c’est toujours un indice utile).

Décision : Si l’export n’est pas listé ou que le sous-réseau client n’est pas autorisé, arrêtez de tuner le client et corrigez la politique d’export.

Task 12: Capture a short NFS packet trace during the event (prove loss vs server silence)

cr0x@server:~$ sudo tcpdump -i eth0 -nn host 10.10.8.10 and port 2049 -c 30
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
10:02:11.101223 IP 10.10.8.21.51344 > 10.10.8.10.2049: Flags [P.], seq 219:451, ack 1001, win 501, length 232
10:02:12.102988 IP 10.10.8.21.51344 > 10.10.8.10.2049: Flags [P.], seq 219:451, ack 1001, win 501, length 232
10:02:13.105441 IP 10.10.8.21.51344 > 10.10.8.10.2049: Flags [P.], seq 219:451, ack 1001, win 501, length 232

Signification : Retransmissions répétées sans réponses serveur indiquent serveur non-réactif ou réponses non retournées.

Décision : Si vous voyez des retransmissions client sans réponse serveur, passez à la santé serveur / chemin de retour réseau. Si vous voyez des réponses serveur mais que le client retransmet encore, suspectez un routage asymétrique ou un pare-feu d’état.

Task 13: Check Docker daemon logs for mount attempts and failures

cr0x@server:~$ journalctl -u docker --since "30 min ago" | egrep -i 'mount|nfs|volume|rpc' | tail -n 30
Jan 03 09:32:14 server dockerd[1321]: time="2026-01-03T09:32:14.112345678Z" level=error msg="error while mounting volume 'appdata': failed to mount local volume: mount :/exports/appdata:/var/lib/docker/volumes/appdata/_data, data: addr=10.10.8.10,vers=4.1,proto=tcp,hard,timeo=600,retrans=2,noatime"

Signification : Confirme que Docker n’a pas pu monter, versus l’appli qui échoue plus tard.

Décision : Si les montages échouent au démarrage des conteneurs, priorisez la préparation réseau et la joignabilité du serveur ; ne courez pas après le tuning runtime.

Task 14: Inspect systemd ordering (network-online is not the same as network)

cr0x@server:~$ systemctl status network-online.target
● network-online.target - Network is Online
     Loaded: loaded (/lib/systemd/system/network-online.target; static)
     Active: active since Fri 2026-01-03 09:10:03 UTC; 1h 2min ago

Signification : Si cette target n’est pas active quand les montages se produisent, votre montage NFS peut être en concurrence avec la disponibilité réseau.

Décision : Si vous observez des problèmes d’ordonnancement, déplacez les montages vers des unités systemd avec After=network-online.target et Wants=network-online.target, ou utilisez automount.

Task 15: Validate that the mount is responsive (fast sanity check)

cr0x@server:~$ time bash -c 'stat /var/lib/docker-nfs/. && ls -l /var/lib/docker-nfs >/dev/null'
real    0m0.082s
user    0m0.004s
sys     0m0.012s

Signification : Les opérations basiques de métadonnées sont rapides. Si cela prend parfois des secondes ou se bloque, vous avez de la latence intermittente ou des stalls.

Décision : Si les métadonnées sont lentes, investiguez la latence disque serveur et la saturation des threads NFS ; les options de montage ne sauveront pas un serveur qui se noie.

Trois mini-histoires d’entreprise (comment ça casse en pratique)

1) Incident causé par une mauvaise hypothèse : « Le volume est local, parce que Docker a dit ‘local’ »

Une entreprise de taille moyenne exécutait un cluster Swarm pour des services internes. Une équipe a créé un volume Docker avec le driver local et des options NFS.
Tout le monde a lu « local » et a supposé que les données vivaient sur chaque nœud. Cette hypothèse a façonné tout : exercices de panne, fenêtres de maintenance, et même la propriété des incidents.

Pendant une maintenance réseau, un switch top-of-rack a vacillé. Seuls certains nœuds ont perdu la connectivité avec le NAS pendant quelques secondes.
Les nœuds affectés avaient des volumes NFS montés en hard. Leurs conteneurs ne sont pas tombés ; ils ont juste cessé de progresser. Les health checks ont expiré.
L’orchestrateur a commencé à rescheduler, mais de nouvelles tâches sont tombées sur les mêmes nœuds dégradés car le scheduler ne connaissait pas le goulot NFS.

La réponse en astreinte était classique : redémarrer le service. Cela n’a fait que créer plus de processus bloqués. Quelqu’un a essayé de supprimer et recréer le volume.
Docker a obéi, mais le montage noyau était toujours coincé. L’hôte est devenu un musée de tâches figées.

La correction n’a pas été héroïque. Ils ont documenté que « driver local » peut être du stockage distant, ajouté une vérification pré-déploiement dans les pipelines pour vérifier le type de montage avec findmnt,
et isolé les services critiques NFS des nœuds qui ne pouvaient pas atteindre le VLAN de stockage.
Le plus grand changement a été culturel : le stockage a cessé d’être « le problème de quelqu’un d’autre » dès que les conteneurs sont entrés en jeu.

2) Optimisation qui s’est retournée contre eux : « Nous avons diminué les timeouts pour que les pannes échouent vite »

Une autre organisation avait un problème intermittent : les applications se figeaient quand NFS hiccupait. Quelqu’un a proposé un changement « simple » :
passer en soft, diminuer timeo, et augmenter retrans pour que le client abandonne rapidement et que l’appli s’en occupe.
Ça semblait raisonnable sur le papier.

En pratique, les applications n’étaient pas conçues pour gérer des EIO en plein écrit.
Un worker écrivait dans un fichier temporaire puis le renameait en place. Sous des montages soft et des timeouts bas,
l’écriture échouait parfois mais le workflow ne remontait pas toujours l’erreur. Le rename s’est produit avec du contenu partiel.
Les tâches en aval ont traité des données corrompues.

L’incident n’a pas été une coupure nette ; c’était pire. Le système est resté « up » tout en produisant des résultats faux.
Cela a déclenché une réponse au ralenti : rollback, retraitement, audit des sorties. Finalement les options de montage ont été rétablies.
Puis ils ont corrigé le vrai problème : perte de paquets intermittente due à une liaison LACP mal configurée et un mismatch MTU qui n’apparaissait qu’en charge.

La leçon inscrite dans leur runbook était douloureusement précise : « Fail fast » est excellent quand l’échec est correctement exposé.
Les montages soft ont rendu l’échec plus facile à ignorer, pas plus facile à gérer.

3) Pratique ennuyeuse mais correcte qui a sauvé la mise : pré-mount + automount + dépendances explicites

Une entreprise financière exécutait des jobs batch stateful en conteneurs, écrivant des artéfacts sur NFS.
Ils avaient une règle terne : les montages NFS sont gérés par systemd, pas par la création de volumes Docker à l’exécution.
Chaque montage avait une unité automount, un timeout défini, et une dépendance sur network-online.target.

Un matin, un cycle de reboot routinier a touché un nœud pendant que le NAS était en maintenance. Le NAS était joignable mais lent pendant quelques minutes.
Les conteneurs ont démarré, mais leurs chemins NFS ont été automountés seulement quand nécessaire. La tentative d’automount a attendu puis a réussi quand le NAS a récupéré.
Les jobs ont démarré avec un peu de retard, et personne ne s’est réveillé.

La différence n’était pas un meilleur matériel. C’était que le cycle de vie du montage n’était pas couplé au cycle de vie des conteneurs.
Docker n’a pas décidé du moment du montage, et les échecs étaient visibles au niveau système avec des logs clairs.

C’est le genre de pratique que les dirigeants ne félicitent jamais parce que rien ne s’est passé. C’est aussi la pratique qui vous garde en poste.

Erreurs fréquentes : symptôme → cause racine → correction

1) Symptom: containers « freeze » et ne veulent pas s’arrêter ; docker stop se bloque

Cause racine : montage NFS en hard bloqué ; processus en état D attendant l’I/O noyau.

Correction : restaurer la connectivité / la santé du serveur ; n’attendez pas des signaux qu’ils fonctionnent. Si vous devez récupérer un nœud, démontez après que le serveur soit revenu, ou redémarrez l’hôte en dernier recours. Prévenir par un réseau stable et des timeo/retrans raisonnables.

2) Symptom: « ça marche sur un nœud, timeouts sur un autre »

Cause racine : différences par nœud de routage/pare-feu/MTU, ou exhaustion de conntrack sur un sous-ensemble de nœuds.

Correction : comparez ip route, ip -s link et règles de pare-feu. Validez le MTU avec des pings DF. Assurez une configuration réseau identique sur toute la flotte.

3) Symptom: montages échouent au boot ou juste après le redémarrage de l’hôte

Cause racine : tentatives de montage qui rivalisent avec la disponibilité réseau ; Docker démarre les conteneurs avant que network-online soit vrai.

Correction : gérer les montages via des unités systemd avec un ordonnancement explicite, ou utiliser automount. Éviter les montages à la demande initiés par le démarrage des conteneurs.

4) Symptom: « permission denied » intermittent ou problèmes d’identité étranges

Cause racine : mismatch UID/GID, comportement root-squash, ou problèmes d’idmapping NFSv4. Les conteneurs aggravent cela car les espaces de noms utilisateurs et les utilisateurs d’images varient.

Correction : standardisez UID/GID pour les écrivains, validez les options d’export serveur, et pour NFSv4 confirmez la configuration d’idmapping. Ne masquer pas cela en mettant 0777 ; ce n’est pas de la stabilité, c’est une capitulation.

5) Symptom: fréquents « stale file handle » après un basculement NAS ou une maintenance d’export

Cause racine : la correspondance file-handle côté serveur a changé ; les clients conservent des références qui ne se résolvent plus.

Correction : évitez de déplacer/réécrire des exports sous les clients ; utilisez des chemins stables. Pour la récupération, remontez et redémarrez les workloads affectés. Pour l’architecture, préférez les méthodes HA stables supportées par votre NAS et testez le basculement avec de vrais clients.

6) Symptom: « montages aléatoires » seulement dans des réseaux sécurisés

Cause racine : ports dynamiques NFSv3 bloqués ; rpcbind/mountd/lockd non autorisés par les pare-feux / security groups.

Correction : passez à NFSv4 si possible. Si vous êtes contraint à v3, fixez les ports des daemons côté serveur et ouvrez-les intentionnellement—puis documentez-les pour que la prochaine personne ne « optimise » pas votre pare-feu.

7) Symptom: pics de latence élevés, puis récupération, qui se répètent sous charge

Cause racine : latence disque serveur (rebuild/snapshot), saturation des threads NFS, ou files réseau congestionnées.

Correction : mesurez la latence I/O côté serveur et les threads du service NFS ; corrigez le goulet. Les options client comme rsize/wsize ne sauveront pas un array saturé.

8) Symptom: passer à soft « corrige » les blocs mais introduit des problèmes de données mystérieux

Cause racine : les montages soft transforment les pannes en erreurs d’E/S ; les applications gèrent mal ces échecs partiels.

Correction : revenez à hard pour les écritures avec état, corrigez la connectivité sous-jacente, et mettez à jour les applications pour gérer les erreurs quand c’est approprié.

Checklists / plan étape par étape

Étape par étape : stabiliser un déploiement NFS Docker existant

  1. Inventoriez les montages avec findmnt -t nfs,nfs4. Notez vers, proto, hard/soft, timeo, retrans, et si vous utilisez des noms d’hôtes.
  2. Confirmez la réalité avec nfsstat -m. Si Docker dit une chose et que le noyau en a monté une autre, faites confiance au noyau.
  3. Décidez du protocole : préférez NFSv4.1+. Si vous êtes en v3, listez les dépendances firewall et les cas de panne que vous ne pouvez pas tolérer.
  4. Corrigez le réseau avant le tuning : validez le MTU bout en bout ; éliminez les drops d’interface ; vérifiez la symétrie de routage ; assurez la stabilité du port 2049.
  5. Choisissez la sémantique de montage :
    • Écritures état : hard, timeo modéré, retrans plutôt bas, TCP.
    • Lecture seule / cache : envisagez soft seulement si votre appli gère EIO et que vous acceptez « erreur plutôt que blocage ».
  6. Rendez les montages prédictibles : pré-montez via systemd ou utilisez automount. Évitez les montages à la demande déclenchés par le démarrage des conteneurs quand c’est possible.
  7. Testez la panne : débranchez le réseau serveur (en labo), redémarrez un client, faites flapper une route, et observez. Si votre test consiste à « attendre et espérer », ce n’est pas un test.
  8. Opérationnalisez : ajoutez des dashboards pour retransmits, drops d’interface, saturation des threads NFS serveur et latence disque. Ajoutez un runbook d’astreinte qui commence par le Mode opératoire de diagnostic rapide ci-dessus.

Une checklist courte pour les options de montage (priorité stabilité)

  • Utilisez TCP : proto=tcp
  • Privilégiez NFSv4.1+ : vers=4.1 (ou 4.2 si supporté)
  • Correction d’abord : hard
  • Ne pas sur-tuner : commencez avec timeo=600, retrans=2 et ajustez seulement sur preuve
  • Réduire le churn métadonnées : noatime pour les workloads typiques
  • Prudence avec actimeo et semblables ; la mise en cache n’est pas une performance gratuite
  • Considérez nconnect seulement après avoir mesuré la capacité serveur et firewall

FAQ

1) Dois-je utiliser NFSv3 ou NFSv4 pour les volumes Docker ?

Utilisez NFSv4.1+ sauf raison de compatibilité spécifique. Dans les réseaux riches en conteneurs, moins de daemons auxiliaires et de ports signifie généralement moins d’échecs « aléatoires » de montage.

2) soft est-il jamais acceptable ?

Oui—pour des données en lecture seule ou de type cache où une erreur d’E/S est préférable à un blocage, et où votre application est conçue pour traiter EIO comme normal. Pour des écritures avec état, c’est un piège dangereux.

3) Pourquoi docker stop se bloque quand NFS est down ?

Parce que les processus sont bloqués en I/O noyau sur un filesystem monté en hard. Les signaux ne peuvent pas interrompre un thread coincé en sommeil non interruptible. Corrigez la joignabilité du montage.

4) Que font réellement timeo et retrans ?

Ils gouvernent le comportement de retry RPC. timeo est le timeout de base pour un RPC ; retrans est le nombre de retries avant que le client n’enregistre « not responding » (et pour soft, avant d’échouer l’E/S).

5) Dois-je tuner rsize et wsize à des valeurs énormes ?

Pas par superstition. Les valeurs par défaut modernes sont souvent bonnes. Des valeurs surdimensionnées peuvent interagir mal avec le MTU, les limites serveur ou les pertes réseau. Faites le tuning seulement après avoir mesuré le débit et les retransmits.

6) Utiliser une IP au lieu d’un nom d’hôte aide-t-il ?

Ça peut aider. Si le DNS est instable, lent ou change de façon imprévue, utiliser une IP évite la résolution de nom comme dépendance de panne. Le compromis est de perdre la migration serveur facile à moins de gérer l’IP comme endpoint stable.

7) Qu’est-ce qui cause « stale file handle » et comment l’éviter ?

C’est généralement causé par des changements côté serveur qui invalident des file handles : déplacement de chemin d’export, comportement de basculement, ou changements de filesystem sous l’export. Évitez cela en gardant des exports stables et en utilisant des méthodes HA supportées par votre NAS, testées avec de vrais clients.

8) Dois-je monter via des volumes Docker ou pré-monter sur l’hôte ?

Pré-monter (mounts systemd/automount) est souvent plus prévisible et plus facile à déboguer. Le montage via volume Docker fonctionne, mais il couple le cycle de vie du montage à celui du conteneur, et ce n’est pas l’endroit où vous voulez faire reposer votre stratégie de fiabilité.

9) Et nolock pour réparer les blocs ?

Évitez-le sauf si vous êtes absolument sûr que votre workload ne dépend pas des verrous. Il peut « réparer » des problèmes liés à lockd en NFSv3 en désactivant le verrouillage, mais cela échange des pannes contre des bugs de correction.

10) Si mon serveur NFS va bien, pourquoi seuls certains clients voient des timeouts ?

Parce que « le serveur va bien » veut souvent dire « il répond au ping ». Les problèmes locaux au client comme mismatch MTU, routage asymétrique, limites conntrack et drops NIC peuvent casser sélectivement NFS tout en laissant d’autres trafics majoritairement corrects.

Conclusion : prochaines étapes pour réduire les alertes

Si vous luttez contre des timeouts de volumes NFS Docker, ne commencez pas par bidouiller timeo comme un bouton radio.
Commencez par nommer la panne : chemin réseau, saturation serveur, friction de version de protocole, ou timing d’orchestration.
Puis faites un choix délibéré sur la sémantique : montages hard correction-d’abord pour les écritures d’état, et soft très ciblé pour les données jetables.

Prochaines étapes pratiques à faire cette semaine :

  1. Auditez chaque hôte Docker avec findmnt et nfsstat -m ; enregistrez les options réelles et la version NFS.
  2. Standardisez sur NFSv4.1+ sur TCP sauf raison contraire.
  3. Corrigez MTU et compteurs de drops avant de changer le tuning des montages.
  4. Déplacez les montages critiques vers des montages gérés par systemd (idéalement automount) avec un ordonnancement explicite network-online.
  5. Rédigez un runbook basé sur le Mode opératoire de diagnostic rapide, et exercez-le une fois pendant une période calme.

Le but ultime n’est pas « NFS ne flanche jamais ». Le but est : quand ça flanche, que ça se comporte de manière prévisible, se rétablisse proprement, et ne transforme pas vos conteneurs en art moderne.

ZFS ZVOL vs Dataset : la décision qui façonne vos douleurs futures

Vous pouvez exploiter ZFS pendant des années sans jamais vous poser la question de la différence entre un dataset et un zvol.
Puis vous virtualisez quelque chose d’important, vous ajoutez des snapshots « au cas où », la réplication devient une exigence du comité,
et soudainement votre plateforme de stockage développe des opinions. Assumées.

Le choix zvol vs dataset n’est pas théorique. Il modifie la façon dont les IO sont façonnés, ce que le cache peut faire, le comportement des snapshots,
comment la réplication casse, et quels réglages existent réellement. Se tromper ne donne pas seulement des performances plus lentes — vous accumulez
une dette opérationnelle qui s’empile chaque trimestre.

Datasets et zvols : ce qu’ils sont vraiment (pas ce qu’on lit dans Slack)

Dataset : un système de fichiers avec les super-pouvoirs de ZFS

Un dataset ZFS est un système de fichiers ZFS. Il a une sémantique de fichiers : répertoires, permissions, appartenances, attributs étendus.
Il peut être exporté via NFS/SMB, monté localement et manipulé avec des outils classiques. ZFS ajoute sa propre couche de fonctionnalités :
snapshots, clones, compression, checksums, quotas/réservations, réglage du recordsize, et toute la sécurité transactionnelle qui
vous aide à dormir un peu mieux.

Lorsque vous placez des données dans un dataset, ZFS contrôle la manière dont il dispose des « records » de taille variable (blocs, mais pas des blocs de taille fixe comme
les systèmes de fichiers traditionnels). Cela importe parce que ça change l’amplification, l’efficacité du cache et les schémas d’IO. Le réglage clé est
recordsize.

Zvol : un périphérique bloc taillé dans ZFS

Un zvol est un volume ZFS : un périphérique bloc virtuel exposé comme /dev/zvol/pool/volume. Il ne comprend pas les fichiers.
Votre filesystem invité (ext4, XFS, NTFS) ou votre moteur de base de données voit un disque et écrit des blocs. ZFS stocke ces blocs en tant qu’objets
avec une taille de bloc fixe contrôlée par volblocksize.

Les zvols existent pour les cas où le consommateur veut un périphérique bloc : LUNs iSCSI, disques VM, certains runtimes de conteneurs, certains hyperviseurs,
et parfois des piles applicatives qui exigent des périphériques bruts.

La traduction en pratique

  • Dataset = « ZFS est le système de fichiers ; les clients parlent des protocoles de fichiers ; ZFS voit les fichiers et peut s’optimiser autour d’eux. »
  • Zvol = « ZFS fournit un faux disque ; autre chose construit un filesystem ; ZFS voit des blocs et devine. »

ZFS est extrêmement bon dans les deux cas, mais ils se comportent différemment. La douleur vient de supposer qu’ils se comportent de la même façon.

Petite blague courte, parce que le stockage exige de l’humilité : si vous voulez lancer un débat animé dans un datacenter, évoquez les niveaux RAID.
Si vous voulez le clore, mentionnez « zvol volblocksize » et regardez tout le monde vérifier discrètement ses notes.

Règles de décision : quand utiliser un dataset, quand utiliser un zvol

Position par défaut : les datasets sont le choix ennuyeux — et l’ennui gagne

Si votre charge peut consommer le stockage comme un système de fichiers (montage local, NFS, SMB), utilisez un dataset. Vous obtenez des opérations plus simples :
inspection facilitée, copie/restauration plus aisée, permissions directes, et moins de cas limites liés aux tailles de blocs et au TRIM/UNMAP.
Vous bénéficiez aussi du comportement ZFS adapté aux fichiers par défaut.

Les datasets sont aussi plus faciles à déboguer parce que vos outils parlent toujours « fichier ». Vous pouvez mesurer la fragmentation au niveau fichier, regarder les
répertoires, raisonner sur les métadonnées et garder un modèle mental propre.

Quand un zvol est l’outil approprié

Utilisez un zvol lorsque le consommateur exige un périphérique bloc :

  • Disques VM (surtout pour des hyperviseurs qui veulent des volumes bruts, ou quand vous voulez des snapshots ZFS gérés du disque virtuel)
  • Cibles iSCSI (les LUNs sont par définition bloc)
  • Certains déploiements clusterisés qui répliquent des périphériques blocs ou nécessitent la sémantique SCSI
  • Applications legacy qui ne supportent que « placer la base de données sur un périphérique brut » (rare, mais ça arrive)

Le modèle zvol est puissant : les snapshots ZFS d’un disque VM sont rapides, les clones sont instantanés, la réplication fonctionne, et vous pouvez tout compresser et
checksummer.

Mais : les périphériques bloc multiplient les responsabilités

Quand vous utilisez un zvol, vous prenez en charge la couche entre le filesystem invité et ZFS. L’alignement compte. La taille de bloc compte.
Les barrières d’écriture comptent. Le comportement Trim/UNMAP compte. Les réglages sync deviennent une question de politique, pas un détail de tuning.

Une matrice de décision simple que vous pouvez défendre

  • Besoins NFS/SMB ou fichiers locaux ? Dataset.
  • Besoins LUN iSCSI ou bloc brut pour hyperviseur ? Zvol.
  • Besoin de visibilité par fichier, restauration facile d’un fichier isolé ? Dataset.
  • Besoin de clones VM instantanés depuis une image maîtresse ? Zvol (ou un dataset avec fichiers creux, mais connaissez vos outils).
  • Besoin d’un snapshot cohérent d’une application qui gère son propre filesystem ? Zvol (et coordonnez flush/quiesce).
  • Vous essayez « d’optimiser les performances » sans connaître les patterns IO ? Dataset, mesurez ensuite. Le tuning héroïque vient plus tard.

Recordsize vs volblocksize : où la performance se décide

Datasets : recordsize est la taille maximale d’un bloc de données que ZFS utilisera pour les fichiers. Les gros fichiers séquentiels (sauvegardes,
médias, logs) aiment un large recordsize comme 1M. Les bases de données et les IO aléatoires préfèrent des valeurs plus petites (16K, 8K) parce que réécrire une petite
région n’entraîne pas une forte rotation de gros blocs.

Zvols : volblocksize est fixé à la création. Choisir mal et vous ne pouvez pas le changer ultérieurement sans reconstruire le zvol.
Ce n’est pas « ennuyeux ». C’est « nous planifions une migration au T4 parce que les graphiques de latence ressemblent à une scie. »

Snapshots : apparemment similaires, opérationnellement différents

Snapshoter un dataset capture l’état du système de fichiers. Snapshoter un zvol capture des blocs bruts. Dans les deux cas, ZFS utilise le copy-on-write,
donc le snapshot est peu coûteux à la création et coûteux ensuite si vous continuez à réécrire des blocs référencés par des snapshots.

Avec les zvols, cette dépense est plus facile à déclencher, parce que les disques VM réécrivent des blocs constamment : mises à jour de métadonnées, churn des journaux,
entretien du filesystem, même « rien ne se passe » peut signifier que quelque chose réécrit. Les snapshots gardés trop longtemps deviennent une taxe.

Quotas, réservations et le piège du thin-provisioning

Les datasets vous donnent quota et refquota. Les zvols vous donnent une taille fixe, mais vous pouvez créer des volumes creux
et faire croire que vous avez plus d’espace que vous n’en avez. C’est une décision business qui se déguise en fonctionnalité technique.

Le thin-provisioning va quand la surveillance, l’alerte et la supervision adulte existent. C’est un désastre quand utilisé pour éviter de dire
« non » dans une file de tickets.

Deuxième blague courte (et la dernière) : le thin provisioning, c’est comme commander un pantalon une taille en dessous comme « motivation ».
Parfois ça marche, mais la plupart du temps vous ne pouvez juste pas respirer.

Modes de défaillance que vous rencontrez seulement en production

Amplification d’écriture due à des blocs mal dimensionnés

Un dataset avec recordsize=1M servant de backend à une base de données qui fait des écritures aléatoires de 8K peut causer une amplification douloureuse : chaque petite
mise à jour touche un grand record. ZFS a des logiques pour gérer des écritures plus petites (et stockera des blocs plus petits dans certains cas), mais ne comptez pas dessus pour
vous sauver d’une mauvaise adéquation. Pendant ce temps, un zvol avec volblocksize=128K servant un filesystem VM qui écrit des blocs de 4K est tout aussi inadapté.

Symptom : débit correct dans les benchs, latence tail misérable en charge réelle.

Semantiques sync : où la latence se cache

ZFS respecte les écritures synchrones. Si une application (ou un hyperviseur) émet des écritures sync, ZFS doit les confirmer de façon sûre — c’est-à-dire sur un stockage stable, pas seulement en RAM.
Sans un SLOG dédié (rapide, protégé contre les pertes d’alimentation), les charges à forte proportion de sync peuvent s’enliser sur la latence du pool principal.

Les consommateurs de zvol utilisent souvent les écritures sync plus agressivement que les protocoles fichiers. Les VMs et les bases de données tiennent à la durabilité.
Les clients NFS peuvent émettre des écritures sync selon les options de montage et le comportement applicatif. Quoi qu’il en soit, si vos pics de latence corrèlent avec la charge d’écriture
sync, le débat « zvol vs dataset » devient « comprenons-nous notre chemin d’écriture sync ? ».

TRIM/UNMAP et le mythe du « l’espace revient automatiquement »

Les datasets peuvent libérer des blocs quand des fichiers sont supprimés. Les zvols dépendent du guest qui émet le TRIM/UNMAP (et de votre pile qui le fait passer).
Sans cela, votre zvol paraît plein pour toujours, les snapshots gonflent, et vous commencez à blâmer la « fragmentation ZFS » pour ce qui est en réalité une absence de signal de garbage collection.

Explosions de rétention de snapshots

Conserver des snapshots horaires pendant 90 jours d’un zvol VM semble responsable jusqu’à ce que vous réalisiez que vous retenez chaque bloc churné à travers chaque mise à jour Windows, exécution du gestionnaire de paquets et rotation de logs.
Les mathématiques deviennent rapidement moches. Les datasets souffrent aussi de ça, mais le churn VM est un genre d’enthousiasme spécial.

Surprise de réplication : les datasets sont plus amicaux pour la logique incrémentale

La réplication ZFS fonctionne pour datasets et zvols, mais la réplication de zvol peut être plus volumineuse et plus sensible au churn des blocs.
Un petit changement dans un filesystem invité peut réécrire des blocs partout. Votre send incrémental peut ressembler à un envoi complet, et votre lien WAN vous envoie une lettre de démission.

Frottement des outils : les humains font partie du système

La plupart des équipes ont plus d’expérience opérationnelle autour des systèmes de fichiers que des périphériques bloc. Elles savent vérifier les permissions, comment copier des fichiers, comment monter et inspecter.
Les workflows zvol vous poussent vers d’autres outils : tables de partition, filesystems invités, contrôles au niveau bloc. La friction se manifeste à 3h du matin, pas en réunion de conception.

Faits et historique intéressants à manier en revue de conception

  1. ZFS a introduit le checksumming de bout en bout comme fonctionnalité centrale, pas comme un add-on ; cela a changé la manière dont on débat de la « corruption silencieuse ».
  2. Le copy-on-write n’était pas nouveau quand ZFS est arrivé, mais ZFS l’a rendu opérationnellement courant pour les piles de stockage générales.
  3. Les zvols ont été conçus pour s’intégrer aux écosystèmes bloc comme iSCSI et les plateformes VM, bien avant que « hyperconvergé » ne devienne un mot commercial.
  4. Les valeurs par défaut de recordsize ont été choisies pour des charges de fichiers polyvalentes, pas pour les bases de données ; les défauts sont de la politique intégrée au code.
  5. Volblocksize est immuable après création du zvol dans la plupart des implémentations courantes ; ce seul détail motive de nombreuses migrations.
  6. L’ARC (Adaptive Replacement Cache) a rendu le comportement de cache de ZFS distinct de beaucoup de systèmes de fichiers traditionnels ; ce n’est pas « juste un page cache ».
  7. L2ARC est arrivé comme cache de second niveau, mais il n’a jamais remplacé la nécessité de dimensionner correctement la RAM ; il change surtout les taux de hit, pas qu’il fasse des miracles.
  8. Les dispositifs SLOG sont devenus un modèle standard parce que la latence des écritures synchrones domine certaines charges ; « SSD rapide » sans protection contre la perte d’alimentation n’est pas un SLOG.
  9. Le send/receive de réplication a donné à ZFS un primitif de sauvegarde intégré ; ce n’est pas « rsync », c’est un flux de transactions de blocs.

Tâches pratiques : commandes, sorties et ce qu’il faut décider

Ce ne sont pas des commandes « mignonnes de démonstration ». Ce sont celles que vous exécutez quand vous essayez de choisir entre un dataset et un zvol, ou quand
vous devez vous prouver que vous avez fait le bon choix.

Tâche 1 : Faire l’inventaire de ce que vous avez réellement

cr0x@server:~$ zfs list -o name,type,used,avail,refer,mountpoint -r tank
NAME                         TYPE   USED  AVAIL  REFER  MOUNTPOINT
tank                         filesystem  1.12T  8.44T   192K  /tank
tank/vm                      filesystem   420G  8.44T   128K  /tank/vm
tank/vm/web01                volume      80.0G  8.44T  10.2G  -
tank/vm/db01                 volume     250G   8.44T   96.4G  -
tank/nfs                     filesystem  320G  8.44T   320G  /tank/nfs

Signification : Vous avez à la fois des datasets (filesystem) et des zvols (volume). Les zvols n’ont pas de mountpoint.

Décision : Identifiez quelles charges résident sur des volumes et demandez-vous : nécessitent-elles vraiment du bloc, ou est-ce de l’inertie ?

Tâche 2 : Vérifier le recordsize du dataset (et s’il correspond à la charge)

cr0x@server:~$ zfs get -o name,property,value recordsize tank/nfs
NAME      PROPERTY    VALUE
tank/nfs  recordsize  128K

Signification : Ce dataset utilise des records de 128K, une valeur générale correcte.

Décision : Si ce dataset héberge des bases de données ou des images VM en tant que fichiers, envisagez 16K ou 32K ; s’il héberge des sauvegardes, envisagez 1M.

Tâche 3 : Vérifier le volblocksize du zvol (le réglage « qu’on ne peut pas changer plus tard »)

cr0x@server:~$ zfs get -o name,property,value volblocksize tank/vm/db01
NAME         PROPERTY     VALUE
tank/vm/db01 volblocksize 8K

Signification : Ce zvol utilise des blocs de 8K — couramment raisonnable pour des bases de données à IO aléatoire et de nombreux filesystems VM.

Décision : Si vous voyez 64K/128K ici pour des disques de démarrage VM généraux, attendez-vous à de l’amplification d’écriture et envisagez une reconstruction correcte.

Tâche 4 : Vérifier la politique sync, car elle contrôle durabilité vs latence

cr0x@server:~$ zfs get -o name,property,value sync tank/vm
NAME     PROPERTY  VALUE
tank/vm  sync      standard

Signification : ZFS respectera les écritures sync mais n’obligera pas que toutes les écritures soient sync.

Décision : Si quelqu’un a mis sync=disabled « pour les performances », planifiez une conversation sur les risques et un plan de retour en arrière.

Tâche 5 : Voir l’état de la compression et le ratio (cela décide souvent du coût)

cr0x@server:~$ zfs get -o name,property,value,source compression,compressratio tank/vm/db01
NAME         PROPERTY       VALUE   SOURCE
tank/vm/db01 compression    lz4     local
tank/vm/db01 compressratio  1.62x   -

Signification : LZ4 est activé et aide.

Décision : Gardez-le. Si la compression est désactivée sur des disques VM, activez-la sauf si vous avez une preuve d’un goulot CPU.

Tâche 6 : Vérifier combien de snapshots vous traînez

cr0x@server:~$ zfs list -t snapshot -o name,used,refer -S used | head
NAME                               USED  REFER
tank/vm/db01@hourly-2025-12-24-23  8.4G  96.4G
tank/vm/db01@hourly-2025-12-24-22  7.9G  96.4G
tank/vm/web01@hourly-2025-12-24-23 2.1G  10.2G
tank/nfs@daily-2025-12-24          1.4G  320G

Signification : La colonne USED est l’espace de snapshot unique à ce snapshot.

Décision : Si des snapshots horaires VM consomment chacun plusieurs gigaoctets, raccourcissez la rétention ou réduisez la fréquence ; les snapshots ne sont pas gratuits.

Tâche 7 : Identifier « l’espace détenu par les snapshots » sur un zvol

cr0x@server:~$ zfs get -o name,property,value usedbysnapshots,usedbydataset,logicalused tank/vm/db01
NAME         PROPERTY         VALUE
tank/vm/db01 usedbysnapshots  112G
tank/vm/db01 usedbydataset    96.4G
tank/vm/db01 logicalused      250G

Signification : Les snapshots retiennent plus d’espace que les données vivantes actuelles.

Décision : Votre « politique de sauvegarde » est maintenant une politique de planification de capacité. Corrigez la rétention et envisagez le comportement TRIM/UNMAP du guest.

Tâche 8 : Vérifier l’état du pool et les erreurs d’abord (le tuning sur un pool malade, c’est comique)

cr0x@server:~$ zpool status -v tank
  pool: tank
 state: ONLINE
  scan: scrub repaired 0B in 02:14:33 with 0 errors on Sun Dec 22 03:10:11 2025
config:

        NAME                        STATE     READ WRITE CKSUM
        tank                        ONLINE       0     0     0
          mirror-0                  ONLINE       0     0     0
            ata-SSD1                ONLINE       0     0     0
            ata-SSD2                ONLINE       0     0     0

errors: No known data errors

Signification : Pool sain, scrub propre.

Décision : Si vous voyez des erreurs de checksum ou des vdevs dégradés, arrêtez. Réparez le matériel/les chemins avant de débattre zvol vs dataset.

Tâche 9 : Surveiller les IO en temps réel par dataset/zvol

cr0x@server:~$ zpool iostat -v tank 2 3
                              capacity     operations     bandwidth
pool                        alloc   free   read  write   read  write
--------------------------  -----  -----  -----  -----  -----  -----
tank                        1.12T  8.44T    210    980  12.4M  88.1M
  mirror-0                  1.12T  8.44T    210    980  12.4M  88.1M
    ata-SSD1                    -      -    105    490  6.2M   44.0M
    ata-SSD2                    -      -    105    490  6.2M   44.1M
--------------------------  -----  -----  -----  -----  -----  -----

Signification : Vous voyez les IOPS de lecture/écriture et la bande passante ; cela vous indique si vous êtes limité en IOPS ou en débit.

Décision : Des IOPS d’écriture élevées avec faible bande passante suggèrent des écritures aléatoires petites — volblocksize et le chemin sync comptent beaucoup.

Tâche 10 : Vérifier la pression ARC (parce que la RAM est votre premier niveau de stockage)

cr0x@server:~$ arcstat 1 3
    time  read  miss  miss%  dmis  dm%  pmis  pm%  mmis  mm%  size     c
12:20:01   532    41      7    12   2     29   5      0   0  48.1G  52.0G
12:20:02   611    58      9    16   2     42   7      0   0  48.1G  52.0G
12:20:03   590    55      9    15   2     40   7      0   0  48.1G  52.0G

Signification : Les taux de miss ARC sont faibles ; le cache est sain.

Décision : Si miss% est constamment élevé sous charge, ajouter de la RAM bat souvent des changements astucieux de layout de stockage.

Tâche 11 : Vérifier si un zvol est sparse (thin) et si c’est sûr

cr0x@server:~$ zfs get -o name,property,value refreservation,volsize,used tank/vm/web01
NAME          PROPERTY        VALUE
tank/vm/web01 refreservation  none
tank/vm/web01 volsize         80G
tank/vm/web01 used            10.2G

Signification : Pas de réservation : c’est effectivement thin du point de vue de l’espace garanti.

Décision : Si vous exécutez des VMs critiques, définissez refreservation pour garantir l’espace ou acceptez le risque d’incidents pool-full.

Tâche 12 : Confirmer ashift (votre base d’alignement des secteurs physiques)

cr0x@server:~$ zdb -C tank | grep -E 'ashift|vdev_tree' -n | head
45:        vdev_tree:
67:            ashift: 12

Signification : ashift=12 signifie des secteurs 4K. Généralement correct pour SSD et HDD modernes.

Décision : Si ashift est incorrect (trop petit), les performances peuvent être altérées de façon permanente ; on reconstruit le pool, on ne le « tune » pas.

Tâche 13 : Évaluer la taille d’envoi snapshot avant de répliquer sur un lien réduit

cr0x@server:~$ zfs send -nvP tank/vm/db01@hourly-2025-12-24-23 | head
send from @hourly-2025-12-24-22 to tank/vm/db01@hourly-2025-12-24-23 estimated size is 18.7G
total estimated size is 18.7G

Signification : L’incrémental est encore de 18.7G — le churn de blocs est élevé.

Décision : Réduisez la fréquence/la rétention des snapshots, améliorez le TRIM invité, ou changez d’architecture (stockage applicatif basé dataset) si possible.

Tâche 14 : Vérifier si TRIM est activé sur le pool

cr0x@server:~$ zpool get -o name,property,value autotrim tank
NAME  PROPERTY  VALUE
tank  autotrim  on

Signification : Le pool effectue le trimming des blocs libérés vers les SSD.

Décision : Si autotrim est off sur des pools SSD, envisagez de l’activer — surtout si vous dépendez des invités zvol pour rendre l’espace.

Tâche 15 : Vérifier les propriétés par dataset qui changent le comportement de façon sournoise

cr0x@server:~$ zfs get -o name,property,value atime,xattr,primarycache,logbias tank/vm
NAME     PROPERTY      VALUE
tank/vm  atime         off
tank/vm  xattr         sa
tank/vm  primarycache  all
tank/vm  logbias       latency

Signification : atime off réduit les écritures de métadonnées ; xattr=sa stocke les xattrs plus efficacement ; logbias=latency privilégie la latence sync.

Décision : Pour des zvols VM, logbias=latency est typiquement raisonnable. Si logbias=throughput apparaît, vérifiez que ce n’était pas une astuce mimétique.

Guide de diagnostic rapide (trouver le goulot en quelques minutes)

Quand les performances sont mauvaises, le débat zvol vs dataset devient souvent une distraction. Utilisez cette séquence pour localiser vite la vraie limite.

Première étape : prouver que le pool n’est pas malade

  1. Exécutez : zpool status -v
    Recherchez : vdevs dégradés, erreurs de checksum, resilver en cours, scrub lent.
    Interprétation : Si le pool est en mauvaise santé, tout le reste est du bruit.
  2. Exécutez : dmesg | tail (et vos logs OS spécifiques)
    Recherchez : réinitialisations de lien, timeouts, erreurs NVMe, problèmes HBA.
    Interprétation : Un chemin de disque qui clignote ressemble à des « pics de latence aléatoires ».

Deuxième étape : classer les IO (petit aléatoire vs grand séquentiel, sync vs async)

  1. Exécutez : zpool iostat -v 2
    Recherchez : IOPS élevées avec peu de MB/s (aléatoire) vs MB/s élevés (séquentiel).
    Interprétation : L’IO aléatoire sollicite la latence, le séquentiel sollicite la bande passante.
  2. Exécutez : zfs get sync tank/... et vérifiez les paramètres applicatifs
    Recherchez : charges sync sans SLOG ou sur des médias lents.
    Interprétation : Les écritures sync exposeront le chemin durable le plus lent.

Troisième étape : vérifier la mémoire et le caching avant d’acheter du matériel

  1. Exécutez : arcstat 1
    Recherchez : miss% élevé, ARC qui ne grossit pas, pression mémoire.
    Interprétation : Si vous manquez constamment le cache, vous forcez des lectures disque évitables.
  2. Exécutez : zfs get primarycache secondarycache tank/...
    Recherchez : quelqu’un a mis le cache sur metadata-only « pour économiser de la RAM ».
    Interprétation : Cela peut être valide dans certains designs, mais souvent c’est s’automutiler par accident.

Quatrième étape : valider la taille des blocs et la taxe des snapshots

  1. Exécutez : zfs get recordsize tank/dataset ou zfs get volblocksize tank/volume
    Interprétation : Mismatch = amplification = latence tail.
  2. Exécutez : zfs get usedbysnapshots tank/...
    Interprétation : Si les snapshots retiennent beaucoup d’espace, ils augmentent aussi le travail des métadonnées et des allocations.

Trois mini-récits d’entreprise (anonymisés, mais douloureusement réels)

Mini-récit 1 : Un incident causé par une mauvaise hypothèse (« un zvol, c’est essentiellement un dataset »)

Une entreprise SaaS de taille moyenne a migré d’un SAN legacy vers ZFS. L’ingénieur stockage — intelligent, rapide, un peu trop confiant — a standardisé sur les zvols
pour tout « parce que les disques VM sont des volumes, et c’est ce que faisait le SAN. » Le stockage applicatif basé NFS a aussi été déplacé dans des ext4 sur zvols.
Ça a marché en test. Ça a même marché en production pendant un moment.

Les premiers signes étaient subtils : les fenêtres de sauvegarde ont commencé à s’étirer. La réplication manquait son RPO, mais seulement sur certains volumes.
Puis un pool stable depuis des mois a soudainement atteint un cliff de capacité. « Nous avons 30% de libre », a dit quelqu’un en montrant le dashboard. « Alors pourquoi ne peut-on pas créer un nouveau disque VM ? »

La réponse était les snapshots. Les zvols étaient snapshotés chaque heure, et les filesystems invités remuaient les blocs constamment. Les fichiers supprimés à l’intérieur des guests ne
se traduisaient pas en bloc libéré à moins que TRIM ne traverse toute la pile. Ce n’était pas le cas, parce que les OS invités n’étaient pas configurés et que le chemin hyperviseur ne le passait pas proprement.

Pendant ce temps, la charge de type NFS exécutée à l’intérieur du guest ext4 n’avait aucune raison d’être dans un zvol. Ils voulaient des sémantiques fichier mais ont construit un gâteau de couches file-on-block-on-ZFS.
La réponse on-call a été de supprimer des « vieux snapshots » jusqu’à ce que le pool arrête de hurler, ce qui a fonctionné brièvement puis est devenu un rituel d’urgence.

La correction n’était pas glamour : migrer les données de type NFS vers des datasets exportés directement, mettre en place une rétention de snapshots sensée pour les zvols VM,
et valider TRIM de bout en bout. Il a fallu un mois de migration minutieuse pour démêler un design basé sur la fausse supposition que « volume vs filesystem n’est qu’un emballage ».

Mini-récit 2 : Une optimisation qui s’est retournée (« set sync=disabled, ça va »)

Une autre organisation, proche de la finance et extrêmement allergique aux indisponibilités, exploitait un cluster de bases de données virtualisé. La latence montait pendant les heures de pointe.
Quelqu’un a fouillé les forums, trouvé les mots magiques sync=disabled, et l’a proposé comme solution rapide. Le changement a été appliqué sur la hiérarchie zvol qui supportait les disques VM.

La latence s’est améliorée immédiatement. Les graphiques étaient magnifiques. L’équipe a déclaré la victoire et est passée à d’autres incendies. Pendant quelques semaines, tout était calme, ce qui est précisément la manière dont le risque vous apprend à l’ignorer.

Puis il y a eu un événement d’alimentation : pas un arrêt propre, pas un basculement gracieux — juste un instant où le plan UPS a rencontré la réalité et la réalité a gagné.
L’hyperviseur est revenu. Plusieurs VMs ont démarré. Certaines non. La base de données a redémarré, mais a rollbacké plus de transactions qu’on ne l’aimait, et au moins un filesystem a nécessité une réparation.

La revue d’incident a été gênante parce que personne n’a pu dire, en toute honnêteté, qu’il n’avait pas troqué la durabilité contre la performance. Ils l’avaient fait. C’est ce que fait ce réglage.
Le retour en arrière a consisté à restaurer sync=standard et ajouter un SLOG approprié avec protection contre la perte d’alimentation. La solution à long terme a été culturelle : aucune « correction de performance » qui change la sémantique de durabilité sans acceptation de risque écrite et un test simulant une perte d’alimentation.

Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise (tester la taille d’envoi et la discipline des snapshots)

Une grande équipe plateforme interne exploitait deux datacenters avec réplication ZFS entre eux. Ils avaient l’habitude, vue comme fastidieuse, de réaliser : avant de mettre en production une nouvelle charge,
ils lançaient une « répétition de réplication » d’une semaine avec snapshots et zfs send -nvP pour estimer les tailles incrémentales. Ils imposaient aussi des politiques de rétention de snapshots comme des adultes : faible rétention pour les volumes churny, plus longue pour les datasets stables.

Une équipe produit a demandé « snapshots horaires pendant six mois » pour une flotte de VMs. L’équipe plateforme n’a pas argumenté philosophiquement. Ils ont lancé la répétition. Les incrémentaux étaient énormes et erratiques, et le lien WAN aurait été saturé régulièrement.
Au lieu de dire « non », ils ont proposé une alternative ennuyeuse : quotidienne pour longue rétention, horaire pour courte rétention, plus des sauvegardes au niveau applicatif pour les données critiques. Ils ont aussi déplacé certaines données hors des disques VM vers des datasets exportés NFS, parce que c’étaient des données fichier faisant semblant d’être bloc.

Des mois plus tard, une panne sur un site a forcé un basculement. La réplication était à jour, la récupération prévisible, et le post-mortem agréablement sans histoire. Le mérite est allé à une pratique que personne ne voulait faire parce que ce n’était pas « de l’ingénierie », mais du « process ».
Elle les a sauvés quand même.

Erreurs communes : symptômes → cause racine → correction

1) Le stockage VM est lent et instable, surtout pendant les mises à jour

  • Symptômes : pics de latence tail, gel d’UI, démarrages lents, blocages IO périodiques.
  • Cause racine : zvol volblocksize mal assorti à la taille IO invité ; snapshots conservés trop longtemps ; écritures sync bloquées sur des médias lents.
  • Correction : reconstruire les zvols avec un volblocksize sensé (souvent 8K ou 16K pour les VMs générales), réduire la rétention des snapshots, valider SLOG pour les charges sync-heavy.

2) Le pool affiche beaucoup d’espace libre, mais vous tombez en « out of space »

  • Symptômes : allocations échouent, écritures bloquées, création de nouveaux zvols impossible, ENOSPC bizarre dans les guests.
  • Cause racine : thin provisioning sans refreservation ; snapshots retenant de l’espace ; pool trop plein (ZFS a besoin de marge).
  • Correction : imposer des réservations pour les zvols critiques, supprimer/expirer les snapshots, garder le pool sous un seuil d’utilisation raisonnable, et implémenter des alertes de capacité incluant la croissance des snapshots.

3) Les incrémentaux de réplication sont énormes pour des VMs basées sur zvol

  • Symptômes : send/receive tourne des heures, saturation réseau, RPO manqués.
  • Cause racine : churn du filesystem invité ; absence de TRIM/UNMAP ; intervalle de snapshot mal choisi.
  • Correction : activer et vérifier TRIM depuis le guest jusqu’au zvol, ajuster la cadence des snapshots, déplacer les données de type fichier vers des datasets, et tester les tailles d’envoi estimées avant de définir la politique.

4) « Nous avons désactivé sync et rien de grave n’est arrivé » (encore)

  • Symptômes : latence incroyable ; dashboards suspectement calmes ; aucune panne immédiate.
  • Cause racine : changement des sémantiques de durabilité ; vous confirmez des écritures avant qu’elles soient sûres.
  • Correction : revenir à sync=standard ou sync=always selon le cas ; ajouter un SLOG approprié ; tester les scénarios de perte d’alimentation et documenter l’acceptation du risque si vous tenez à tricher avec la physique.

5) Une charge NFS performe mal quand elle est stockée à l’intérieur d’une VM sur un zvol

  • Symptômes : charges riches en métadonnées lentes ; sauvegardes et restaurations compliquées ; dépannage douloureux.
  • Cause racine : couche inutile : workload fichier placé dans un filesystem invité sur un zvol, perdant les optimisations et la visibilité fichier-level de ZFS.
  • Correction : stocker et exporter en tant que dataset directement ; régler recordsize et atime ; garder la pile simple.

6) Le rollback de snapshot « fonctionne » mais l’app revient corrompue

  • Symptômes : après rollback, le filesystem monte mais les données applicatives sont incohérentes.
  • Cause racine : inadéquation crash-consistency ; les snapshots zvol capturent des blocs, pas la quiescence de l’application ; les snapshots dataset sont aussi sujets à cela s’ils ne sont pas coordonnés.
  • Correction : quiescer les applications (fsfreeze, flush DB, hooks des agents invités hyperviseur) avant les snapshots ; valider les procédures de restauration périodiquement.

Checklists / plan étape par étape

Étape par étape : choisir dataset vs zvol pour une nouvelle charge

  1. Identifier l’interface nécessaire : protocole fichier (NFS/SMB/montage local) → dataset ; protocole bloc (iSCSI/disque VM) → zvol.
  2. Noter les hypothèses de pattern IO : principalement séquentiel ? principalement aléatoire ? sync-heavy ? Cela détermine recordsize/volblocksize et les besoins SLOG.
  3. Choisir la couche la plus simple opérationnelle : éviter file-on-block-on-ZFS sauf nécessité.
  4. Activer par défaut compression lz4 sauf preuve du contraire.
  5. Décider la politique de snapshots dès le départ : fréquence et rétention ; ne laissez pas cela devenir un substitut de sauvegarde.
  6. Décider des attentes de réplication : faire une répétition avec estimation des tailles d’envoi si vous tenez à RPO/RTO.
  7. Garde-fous de capacité : réservations pour zvols critiques ; quotas pour datasets ; garder une marge dans le pool.
  8. Documenter la récupération : comment restaurer un fichier, une VM ou une base de données ; inclure les étapes de quiescence.

Checklist : configurer un zvol pour disques VM (baseline production)

  • Créer avec un volblocksize sensé (souvent 8K ou 16K ; faites correspondre au guest et à l’hyperviseur).
  • Activer compression=lz4.
  • Garder sync=standard ; ajouter un SLOG si la latence sync importe.
  • Planifier la rétention des snapshots en fonction du churn ; tester zfs send -nvP pour dimensionner la réplication.
  • Vérifier le TRIM/UNMAP de bout en bout si vous attendez une reclamation d’espace.
  • Envisager refreservation pour les guests critiques afin d’éviter les catastrophes pool-full.

Checklist : configurer un dataset pour applications et stockage de fichiers

  • Choisir recordsize selon l’IO : 1M pour sauvegardes/médias ; plus petit pour les patterns type base de données.
  • Activer compression=lz4.
  • Désactiver atime sauf si vraiment nécessaire.
  • Utiliser quota/refquota pour empêcher « un tenant de manger le pool ».
  • Snapshotter avec rétention, pas thésaurisation.
  • Exporter via NFS/SMB avec paramètres clients sensés ; mesurer avec des charges réelles.

FAQ

1) Un zvol est-il toujours plus rapide qu’un dataset pour disques VM ?

Non. Un zvol peut être excellent pour des disques VM, mais « plus rapide » dépend du comportement sync, de la taille de bloc, du churn des snapshots et du chemin IO hyperviseur.
Un dataset hébergeant des fichiers QCOW2/raw peut aussi très bien performer avec le bon recordsize et le comportement de cache adapté. Mesurez, ne vibrez pas.

2) Puis-je changer volblocksize plus tard ?

Pratiquement : non. Traitez volblocksize comme immuable. Si vous avez mal choisi, la solution propre est la migration vers un nouveau zvol avec la bonne taille et une coupure contrôlée.

3) Dois-je définir recordsize=16K pour les bases de données sur datasets ?

Souvent raisonnable, mais pas universel. Beaucoup de bases utilisent des pages 8K ; 16K peut être un compromis acceptable. Mais si votre charge est principalement faite de scans séquentiels ou de gros blobs, un recordsize plus grand peut aider.
Profilez votre IO.

4) Les snapshots ZFS sont-ils des sauvegardes ?

Ce sont un composant puissant, pas une stratégie de sauvegarde. Les snapshots ne protègent pas contre la perte du pool, les erreurs opérateur sur le pool, ou la corruption répliquée si vous répliquez trop tôt. Utilisez snapshots avec réplication et/ou stockage de sauvegarde séparé et une rétention adaptée.

5) Pourquoi supprimer des fichiers dans une VM ne libère pas d’espace sur le pool ZFS ?

Parce que ZFS voit un périphérique bloc. À moins que le guest n’émette TRIM/UNMAP et que la pile le fasse traverser, ZFS ne sait pas quels blocs sont libres à l’intérieur du filesystem invité.

6) Puis-je utiliser dedup sur des zvols pour économiser de l’espace ?

Généralement non. La déduplication consomme beaucoup de RAM et est opérationnellement impitoyable. La compression vous donne typiquement des gains sûrs avec moins de risques.
Si vous voulez dedup, prouvez-le avec des données réalistes et budgétez la RAM en conséquence.

7) Un SLOG aide-t-il toutes les écritures ?

Non. Un SLOG aide les écritures synchrones. Si votre charge est majoritairement asynchrone, un SLOG ne bougera pas beaucoup la courbe. Si votre charge est sync-heavy, un SLOG approprié peut faire la différence entre « ça va » et « tout est en feu ».

8) Quand préférer les datasets pour les conteneurs ?

Si votre plateforme de conteneurs peut utiliser directement des datasets ZFS (commun sur beaucoup de setups Linux), les datasets donnent généralement une meilleure visibilité et des opérations plus simples que d’empiler le stockage des conteneurs dans des disques VM sur zvols. Minimisez les couches.

9) Puis-je utiliser sans risque sync=disabled pour des disques VM si j’ai un UPS ?

Un UPS réduit le risque ; il ne l’élimine pas. Kernel panics, réinitialisations de contrôleurs, bugs de firmware et erreurs humaines existent toujours. Si vous avez besoin de durabilité, gardez les sémantiques sync correctes et concevez le chemin matériel (SLOG avec protection contre la perte d’alimentation) pour le supporter.

10) Quel est le meilleur choix par défaut : zvol ou dataset ?

Par défaut, optez pour dataset sauf si le consommateur nécessite du bloc. Quand vous avez besoin de bloc, utilisez les zvols intentionnellement : choisissez volblocksize, planifiez les snapshots et confirmez TRIM et le comportement sync.

Prochaines étapes réalisables cette semaine

Voici le chemin pratique qui réduit la douleur future sans transformer votre stockage en expérience scientifique.

  1. Inventoriez votre environnement : listez datasets vs volumes, et cartographiez-les aux charges. Tout ce qui est « de type fichier » vivant dans un zvol est un drapeau rouge à enquêter.
  2. Auditez les réglages irréversibles : vérifiez volblocksize sur les zvols et recordsize sur les datasets clés. Notez les divergences avec les patterns de charge.
  3. Mesurez la taxe des snapshots : identifiez quels zvols/datasets ont de gros usedbysnapshots. Alignez la rétention sur le besoin business, pas sur l’anxiété.
  4. Validez le comportement sync : trouvez tout sync=disabled et traitez-le comme une demande de changement nécessitant une acceptation de risque explicite. Si la latence sync est un problème, concevez une solution avec un SLOG, pas de l’optimisme.
  5. Faites une répétition de réplication : utilisez zfs send -nvP pour estimer les incrémentaux sur une semaine. Si les chiffres sont délirants, corrigez les sources de churn avant de promettre des RPO serrés.

Une idée paraphrasée de John Allspaw (opérations/fiabilité) : Les incidents viennent du travail normal dans des systèmes complexes, pas d’une seule mauvaise personne ayant une mauvaise journée. (idée paraphrasée)

Le choix zvol vs dataset est exactement ce genre de décision « travail normal ». Faites-le délibérément. Le vous du futur aura toujours des incidents,
mais ils seront du genre intéressant — ceux que vous pouvez réparer — plutôt que le genre lent et usant causé par un primitif de stockage mal choisi il y a des années et défendu par fierté.

Docker Compose : les variables d’environnement vous trahissent — les erreurs .env qui cassent la prod

La panne commence petit. Un conteneur démarre avec NODE_ENV=development, ou votre base de données accepte tout à coup
des connexions avec un mot de passe par défaut. Rien n’a « changé » dans l’application. Le job CI est vert. Vous avez déployé le même
fichier Compose que la semaine dernière.

Ce qui a changé, c’est la partie la plus fragile de votre déploiement : la chaîne invisible de variables d’environnement qui traverse
Docker Compose, votre shell, et un minuscule fichier .env que personne ne relit parce que « ce n’est pas du code ».
C’est du code. Il n’est juste pas linté.

Un modèle mental qui ne vous mentira pas

Docker Compose utilise les variables d’environnement de deux manières différentes, et la plupart des incidents en production surviennent quand les équipes
les traitent comme une seule et même chose :

1) Variables utilisées par Compose lui‑même (au moment du rendu)

Ces variables existent pour rendre la configuration Compose : des éléments comme ${IMAGE_TAG} dans
compose.yaml, COMPOSE_PROJECT_NAME, ou COMPOSE_PROFILES.
Compose les résout avant de démarrer les conteneurs. Si Compose se trompe, vos conteneurs ne seront peut‑être même pas ceux que vous pensez avoir déployés.

2) Variables transmises aux conteneurs (au runtime)

Ces variables font partie de l’environnement du conteneur : ce que votre app lit via getenv.
Elles proviennent de environment:, env_file:, et parfois de votre shell via des passages implicites.

Les variables de rendu influencent le YAML final. Les variables d’exécution influencent le comportement du processus dans le conteneur.
Confondre les deux mène à réparer un bug de conteneur en éditant un profil shell sur l’hôte, puis découvrir que systemd ne lit pas ce profil shell.

Une vérité opérationnelle : vous ne pouvez pas « simplement vérifier le .env ». Il faut vérifier ce que Compose a effectivement rendu et ce que le conteneur a réellement reçu.

Citation à garder sur votre bureau : idée paraphrasée« L’espoir n’est pas une stratégie. » (idée paraphrasée attribuée à
Gordon Sullivan, souvent citée en ingénierie et fiabilité)

Blague n°1 : Les variables d’environnement sont comme les commérages au bureau — tout le monde jure qu’il ne les a pas lancés, mais elles se retrouvent pourtant dans toutes les pièces.

Faits et historique à connaître (pour arrêter de vous disputer avec YAML)

  • Fait 1 : Le fichier .env utilisé par Docker Compose n’est pas automatiquement au même format qu’un script shell. C’est un parseur plus simple « KEY=VALUE » avec ses propres bizarreries.
  • Fait 2 : Compose provient à l’origine de Fig (2014), et beaucoup de son comportement sur les variables est une commodité héritée plutôt qu’une élégance de conception pure.
  • Fait 3 : Compose v2 est implémenté comme un plugin de l’interface Docker CLI, et le comportement peut varier subtilement entre versions parce que le moteur est désormais plus proche de l’écosystème Docker CLI.
  • Fait 4 : Compose utilise les variables d’environnement à la fois pour le rendu de la configuration et pour l’environnement des conteneurs ; des règles de priorité différentes s’appliquent à chaque chemin.
  • Fait 5 : L’interpolation des variables arrive avant la plupart des validations. Une variable manquante peut devenir silencieusement une chaîne vide et former malgré tout une valeur YAML « valide ».
  • Fait 6 : env_file est une entrée d’exécution pour les conteneurs ; il n’influence généralement pas l’interpolation de Compose à moins que vous ne chargiez explicitement les variables dans le shell ou n’utilisiez une chaîne d’outils le faisant.
  • Fait 7 : La commande docker compose config est l’outil le plus proche d’un sérum de vérité : elle montre la configuration entièrement rendue que Compose va exécuter.
  • Fait 8 : Le même projet sur deux hôtes peut se rendre différemment parce que Compose lit l’environnement de l’hôte, le répertoire courant, et les entrées optionnelles --env-file.
  • Fait 9 : COMPOSE_PROJECT_NAME affecte les noms de réseau, de volume et de conteneur. Un changement de project-name peut « orpheliner » d’anciens volumes et en créer de nouveaux.

Carnet de diagnostic rapide

Quand la production brûle, vous n’avez pas besoin de philosophie. Vous avez besoin d’une séquence qui rétrécit rapidement le rayon d’action.
Voici l’ordre que j’utilise parce qu’il sépare les bugs « au moment du rendu » des bugs « au runtime » en quelques minutes.

Première étape : confirmer ce que Compose a rendu

  1. Exécutez docker compose config et inspectez les valeurs interpolées (tags d’images, ports, chemins de volumes,
    nom de projet, profils). Si la config rendue est erronée, ne perdez pas de temps à l’intérieur des conteneurs.
  2. Cherchez des chaînes vides, des valeurs « null » implicites, des valeurs par défaut inattendues, ou des définitions de services dupliquées à cause des profils.

Deuxième étape : confirmer ce que le conteneur a réellement reçu

  1. Inspectez l’environnement du conteneur (docker inspect) ou affichez-le depuis l’intérieur du conteneur
    (env).
  2. Comparez avec ce que vous pensez avoir défini via env_file et environment.

Troisième étape : confirmer quel .env et quel environnement hôte ont été utilisés

  1. Vérifiez le répertoire de travail et le fichier env sélectionné. Si vous avez lancé Compose depuis le mauvais répertoire, vous pourriez
    utiliser le mauvais .env.
  2. Vérifiez CI/CD : passe-t-il --env-file ? Exporte-t-il des variables ? systemd n’écrase-t-il pas l’environnement ?

Si le stockage ou le réseau semble étrange, suspectez le nom de projet et les noms de volumes

Un COMPOSE_PROJECT_NAME changé ou un changement de nom de répertoire peut créer de nouveaux réseaux et de nouveaux volumes.
L’application a « perdu » ses données parce qu’elle écrit sur un volume différent avec un nom différent.

Tâches pratiques : commandes, sorties et décisions

Ce sont les tests sur le terrain. Chacun inclut : une commande, ce que signifie une sortie typique, et la décision à prendre.
Exécutez-les dans l’ordre quand vous ne savez pas où se cache la vérité.

Tâche 1 : Vérifier la version de Compose (le comportement varie)

cr0x@server:~$ docker compose version
Docker Compose version v2.24.6

Sens : Vous êtes sur Compose v2.x. Bien — la plupart des comportements et flags modernes s’appliquent.
Si c’était v1, plusieurs flags et comportements limites diffèrent.
Décision : Notez cette version dans les notes d’incident ; si le comportement diffère entre hôtes, alignez les versions.

Tâche 2 : Voir quel nom de projet Compose pense utiliser

cr0x@server:~$ docker compose ls
NAME                STATUS              CONFIG FILES
payments-prod        running(6)          /srv/payments/compose.yaml

Sens : Le projet est payments-prod. Les réseaux/volumes seront préfixés par ce nom.
Décision : Si vous attendiez un autre nom de projet, arrêtez‑vous : vous pourriez opérer sur le mauvais projet.

Tâche 3 : Rendre la configuration entièrement interpolée (la « vérité »)

cr0x@server:~$ cd /srv/payments
cr0x@server:~$ docker compose config
services:
  api:
    environment:
      DB_HOST: db
      LOG_LEVEL: info
    image: registry.local/payments-api:1.9.3
    ports:
      - mode: ingress
        target: 8080
        published: "8080"
        protocol: tcp
  db:
    environment:
      POSTGRES_DB: payments
    image: postgres:15
volumes:
  payments-prod_db-data: {}
networks:
  default:
    name: payments-prod_default

Sens : L’interpolation a eu lieu. C’est ce que Compose exécutera.
Décision : Si le tag d’image ou le port est erroné ici, le bug se situe dans la résolution des variables (pas dans le runtime du conteneur).

Tâche 4 : Identifier quel fichier env est utilisé

cr0x@server:~$ ls -la /srv/payments/.env
-rw------- 1 root root 412 Jan  2 09:11 /srv/payments/.env

Sens : Un .env local existe dans le répertoire du projet.
Décision : Vérifiez que vous lancez Compose depuis ce répertoire ; sinon vous ne lisez pas ce fichier.

Tâche 5 : Repérer les mines d’espaces et de guillemets dans .env

cr0x@server:~$ sed -n '1,120p' /srv/payments/.env
IMAGE_TAG=1.9.3
DB_PASSWORD=correct-horse-battery-staple
LOG_LEVEL=info
API_BASE_URL=https://payments.internal
BAD_SPACES =oops
QUOTED="literal quotes?"

Sens : BAD_SPACES =oops est suspect : beaucoup de parseurs traitent cette clé comme BAD_SPACES (avec un espace final) ou la rejettent.
QUOTED="literal quotes?" peut préserver les guillemets selon le parseur.
Décision : Corrigez le format : pas d’espaces autour de =, évitez les guillemets sauf si vous connaissez le comportement du parseur.

Tâche 6 : Vérifier si une variable manque au moment du rendu

cr0x@server:~$ grep -n 'IMAGE_TAG' -n /srv/payments/compose.yaml
12:    image: registry.local/payments-api:${IMAGE_TAG}

Sens : Compose a besoin de IMAGE_TAG pour rendre la chaîne d’image.
Décision : Assurez-vous que IMAGE_TAG est défini dans le bon .env ou exporté dans l’environnement utilisé par Compose.

Tâche 7 : Détecter l’interpolation vide silencieuse

cr0x@server:~$ IMAGE_TAG= docker compose config | grep -n 'image:'
7:    image: registry.local/payments-api:

Sens : Un IMAGE_TAG vide rend une référence d’image un peu invalide qui peut quand même passer le parsing YAML.
Décision : Ajoutez des gardes pour variables requises en utilisant les défauts d’interpolation de Compose (voir plus loin) et échouez en CI sur les champs vides.

Tâche 8 : Inspecter l’environnement à l’intérieur d’un conteneur en cours d’exécution

cr0x@server:~$ docker compose exec -T api env | egrep 'DB_|LOG_LEVEL|API_BASE_URL'
API_BASE_URL=https://payments.internal
DB_HOST=db
LOG_LEVEL=info

Sens : Le conteneur a reçu des variables. Si quelque chose manque, c’est un problème d’injection d’environnement au runtime.
Décision : Comparez avec docker compose config et le contenu du env_file.

Tâche 9 : Confirmer ce que Docker pense être l’environnement (autorité)

cr0x@server:~$ docker inspect payments-prod-api-1 --format '{{json .Config.Env}}'
["API_BASE_URL=https://payments.internal","DB_HOST=db","LOG_LEVEL=info","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]

Sens : C’est ce que Docker passera au processus. Si ce n’est pas là, votre application ne le verra pas.
Décision : Si la config Compose le montre mais que l’inspect ne le montre pas, vous avez une dérive de déploiement ou un problème de recréation.

Tâche 10 : Détecter les problèmes de « conteneur non recréé »

cr0x@server:~$ docker compose up -d
[+] Running 2/2
 ✔ Container payments-prod-db-1   Running
 ✔ Container payments-prod-api-1  Running

Sens : Compose n’a pas recréé les conteneurs. Les changements d’env ne s’appliquent pas à un conteneur en cours d’exécution sauf s’il est recréé.
Décision : Si vous avez changé des variables d’env, forcez la recréation : docker compose up -d --force-recreate.

Tâche 11 : Forcer la recréation et confirmer que le nouvel env est appliqué

cr0x@server:~$ docker compose up -d --force-recreate
[+] Running 2/2
 ✔ Container payments-prod-db-1   Running
 ✔ Container payments-prod-api-1  Started

Sens : Le conteneur API a été redémarré/recréé.
Décision : Relancez les Tâches 8/9 pour confirmer que les changements d’environnement ont bien été appliqués.

Tâche 12 : Détecter la dérive de nom de projet qui crée des volumes « nouveaux »

cr0x@server:~$ docker volume ls | grep -E 'payments.*db-data'
local     payments-prod_db-data
local     payments_db-data

Sens : Deux volumes aux noms similaires existent, probablement issus de noms de projet différents.
Décision : Confirmez quel volume est attaché au conteneur DB en cours d’exécution avant de « nettoyer » quoi que ce soit.

Tâche 13 : Confirmer quel volume un conteneur utilise réellement

cr0x@server:~$ docker inspect payments-prod-db-1 --format '{{range .Mounts}}{{println .Name .Destination}}{{end}}'
payments-prod_db-data /var/lib/postgresql/data

Sens : La DB utilise payments-prod_db-data.
Décision : Si l’application « a perdu des données », comparez avec le volume que vous attendiez. Ne supprimez pas de volumes tant que vous n’avez pas prouvé qu’ils sont inutilisés.

Tâche 14 : Identifier quel .env est utilisé quand vous lancez depuis un autre répertoire

cr0x@server:~$ cd /tmp
cr0x@server:~$ docker compose -f /srv/payments/compose.yaml config | head
services:
  api:
    image: registry.local/payments-api:

Sens : L’exécution depuis /tmp a probablement fait manquer à Compose le /srv/payments/.env prévu, donc l’interpolation a échoué.
Décision : Lancez toujours depuis le répertoire du projet ou fournissez --env-file /srv/payments/.env.

Tâche 15 : Prouver la priorité entre l’hôte et .env

cr0x@server:~$ cd /srv/payments
cr0x@server:~$ export IMAGE_TAG=2.0.0
cr0x@server:~$ docker compose config | grep -n 'image:'
7:    image: registry.local/payments-api:2.0.0

Sens : La variable exportée sur l’hôte a écrasé la valeur dans .env.
Décision : En production, évitez de vous reposer sur « ce qui est exporté dans le shell ». R rendez la source d’env explicite.

Tâche 16 : Détecter des CRLF accidentels Windows dans .env (oui, toujours)

cr0x@server:~$ file /srv/payments/.env
/srv/payments/.env: ASCII text, with CRLF line terminators

Sens : Les CRLF peuvent se glisser dans les clés/valeurs, provoquant des « variable non trouvée » déroutants ou des valeurs avec un \r caché.
Décision : Convertissez en LF : sed -i 's/\r$//' /srv/payments/.env, puis rendez de nouveau la config.

Tâche 17 : Confirmer des retours chariot cachés dans une valeur spécifique

cr0x@server:~$ python3 -c 'import os;print(repr(open("/srv/payments/.env","rb").read().splitlines()[-1]))'
b'QUOTED="literal quotes?"\r'

Sens : Ce \r final est réel. Il peut casser des tokens d’auth, des URL ou des mots de passe.
Décision : Normalisez les fins de ligne en CI, et traitez .env comme un artefact texte qui nécessite des contrôles.

Tâche 18 : Montrer les différences entre env_file et environment dans la config finale

cr0x@server:~$ docker compose config | sed -n '1,80p'
services:
  api:
    environment:
      DB_HOST: db
      LOG_LEVEL: info
    image: registry.local/payments-api:1.9.3

Sens : Vous voyez clairement les valeurs inline de environment:. Si vous avez utilisé env_file, il peut ne pas s’expanser inline dans la sortie comme vous l’attendez.
Décision : Si votre audit dépend de voir les variables, ne vous fiez pas seulement à env_file ; utilisez des étapes explicites de validation de configuration.

Priorité et portée : qui gagne quand les variables se percutent

La plupart des équipes ne peuvent pas répondre à cette question sans deviner : « Si je définis FOO dans le shell, dans .env,
et dans environment:, lequel gagne ? » La réponse dépend de si vous parlez du moment du rendu ou du runtime.
C’est pourquoi cela casse en production : les gens parlent à côté les uns des autres.

Priorité au rendu (interpolation dans compose.yaml)

Quand Compose interpole ${VAR} dans le YAML, il regarde ses sources d’environnement. En pratique, l’environnement exporté du processus Compose est le compétiteur majeur. Le .env local est souvent un repli pratique.

En d’autres termes : si votre CI exporte IMAGE_TAG, il écrasera typiquement le .env. Si votre unité systemd exécute Compose avec un environnement essentiellement vide, elle ignorera ce que votre shell interactif avait.

Règle opérationnelle : les variables de rendu doivent être explicites. Passez‑les via la CI de façon contrôlée ou fournissez un fichier d’env explicite via --env-file. Ne laissez pas des shells aléatoires décider.

Priorité au runtime (ce que reçoit le conteneur)

L’environnement du conteneur est construit à partir des définitions de services Compose :

  • environment: les entrées sont explicites et visibles dans le fichier Compose.
  • env_file: charge des paires clé/valeur depuis un fichier dans l’environnement du conteneur.
  • Certaines variables peuvent être transmises depuis l’hôte si vous les référencez dans environment: sans valeur, selon la syntaxe.

Règle pratique : traitez environment: comme une API pour ce que le conteneur attend et
traitez env_file comme un détail d’implémentation pour la manière dont vous lui fournissez les valeurs. En debug, vérifiez toujours ce qui est effectivement arrivé via docker inspect.

Le nom de projet fait partie de l’histoire d’environnement en coulisse

COMPOSE_PROJECT_NAME ressemble à « juste un nom ». Ce n’est pas le cas.
Il change les noms de réseaux et de volumes. Si vous liez vos données à des volumes et votre monitoring aux noms de conteneurs,
le nom de projet est une variable de production, que vous l’admettiez ou pas.

Interpolation et parsing : les bords tranchants de .env et Compose

Le format .env ressemble à du shell. Ce n’est pas du shell. C’est un fichier clé/valeur avec juste assez de flexibilité
pour vous rendre trop confiant.

Espaces blancs : le tueur silencieux

Beaucoup de parseurs traitent KEY =value comme une clé différente de KEY=value, ou ils le rejettent.
Dans les deux cas, vous vous retrouvez avec « KEY non défini » et Compose substitue silencieusement une chaîne vide.

Ne soyez pas « tolérant ». Soyez strict. Pour les fichiers env de production :
pas d’espaces autour du signe égal, et les clés devraient correspondre à [A-Z0-9_]+.

Guillemets : parfois littéraux, parfois retirés, toujours déroutants

Dans certains écosystèmes, FOO="bar" signifie que la valeur est bar sans guillemets. Dans d’autres, ces
guillemets font partie de la valeur. Le comportement de Compose peut vous surprendre selon la voie de parsing empruntée.
La seule position sûre : évitez les guillemets dans .env sauf si vous avez vérifié le comportement avec docker compose config et un conteneur en cours d’exécution.

Défauts d’interpolation : utilisez-les, mais comprenez‑les

Compose prend en charge des motifs comme :
${VAR:-default} et ${VAR?error} dans de nombreux contextes.
C’est là que les équipes peuvent transformer une défaillance silencieuse en une défaillance bruyante.

Si IMAGE_TAG doit exister en prod, rendez‑le obligatoire. Si LOG_LEVEL peut avoir une valeur par défaut, donnez‑lui une valeur par défaut.
Échouez rapidement sur tout ce qui change le comportement d’une manière que vous ne pouvez pas observer.

Vide est différent d’absence

L’interpolation de Compose considère souvent une variable vide comme « définie », ce qui peut contrer les valeurs par défaut. Si une pipeline définit
IMAGE_TAG en chaîne vide (oui, ça arrive), votre ${IMAGE_TAG:-latest} peut ou non se comporter comme vous l’attendez.
Testez cela explicitement dans votre environnement.

.env vs env_file : même look, sémantique différente

Le fichier .env (pour Compose) est utilisé pour l’interpolation des variables de Compose et certaines options Compose.
Le directive env_file: alimente l’environnement des conteneurs au runtime.
Les gens les confondent parce que le contenu des fichiers se ressemble. Le résultat est invariablement chaotique.

Si vous voulez que des valeurs influencent l’interpolation, elles doivent être dans l’environnement que Compose utilise pour le rendu
(export shell, --env-file explicite, ou la gestion d’environnement de votre orchestration). Si vous voulez des valeurs à l’intérieur du conteneur,
elles doivent être dans environment: ou env_file:.

Blague n°2 : Un fichier .env ressemble beaucoup à un tout‑petit — le silence ne signifie pas que tout va bien, cela signifie qu’il faut vérifier immédiatement.

Trois mini‑histoires d’entreprise issues du terrain

Histoire 1 : L’incident causé par une mauvaise hypothèse

Une équipe fintech faisait tourner une API orientée client sur une paire de VM avec Docker Compose. Ils avaient un .env dans le repo
et un prod.env séparé stocké sur l’hôte. Dans leur tête, « Compose charge l’env depuis env_file ». Ils avaient
en partie raison et étaient entièrement condamnés.

Le fichier Compose utilisait ${IMAGE_TAG} pour ancrer l’image de l’API. Les variables runtime du conteneur venaient de
env_file: ./prod.env. Un candidat de release nécessitait un hotfix, donc un ingénieur a mis à jour IMAGE_TAG
dans prod.env, a lancé docker compose up -d, et s’attendait à ce que la nouvelle image se déploie.

Ce ne fut pas le cas. L’interpolation de Compose ne regardait pas env_file pour rendre le champ image:.
Les conteneurs sont restés sur l’ancien tag. Parallèlement, l’ingénieur a aussi mis à jour une variable runtime dans prod.env
en supposant que le conteneur l’avait prise ; ce ne fut pas le cas, parce que Compose n’a pas recréé le conteneur. Ils se sont retrouvés
avec de l’ancien code, de vieilles variables, et une nouvelle croyance.

Deux heures plus tard, l’API renvoyait des erreurs ressemblant à une régression du hotfix. Ce n’en était pas une. Le hotfix n’avait jamais été déployé. Leur monitoring affichait « déploiement réussi » parce que le job s’était terminé ; il n’avait pas validé la sortie de docker compose config ni vérifié les IDs d’images des conteneurs en cours.

La correction fut ennuyeuse : rendre les tags d’images variables de rendu obligatoires et les définir explicitement dans la commande de déploiement,
vérifier avec docker compose config, puis forcer la recréation ou faire rouler correctement les conteneurs. Ils ont aussi cessé
d’utiliser prod.env comme un fichier magique qui « contrôle tout ». Il ne contrôle que ce que vous y branchez.

Histoire 2 : L’optimisation qui s’est retournée contre eux

Une société média voulait accélérer les déploiements. Quelqu’un a remarqué que recréer les conteneurs prend du temps, surtout pour un
service avec beaucoup de sidecars. Ils ont changé le processus : mettre à jour .env, puis lancer docker compose up -d
sans forcer la recréation, pour « éviter les temps d’arrêt ».

Pendant un temps, cela semblait fonctionner — parce que la plupart des changements étaient des tags d’image, et Compose tirerait et redémarrerait
les services quand il détectait une nouvelle image. Mais les variables d’environnement ne sont pas des images. Un changement de configuration critique
a basculé un flag de fonctionnalité pour le routage des requêtes. La moitié de la flotte s’est mise à jour (les nœuds où les conteneurs ont été recréés),
l’autre moitié non. Le résultat fut un comportement « split‑brain » où les requêtes prenaient des chemins différents selon la VM qui les recevait.

Le débogage fut pénible parce que le fichier Compose semblait correct, le .env semblait correct, et les conteneurs étaient tous « up ». Le bug venait du process : ils avaient optimisé en ôtant l’étape qui applique de manière fiable les changements d’env.
Ils avaient introduit de la non‑déterminisme dans le déploiement de configuration.

Le playbook de récupération fut brutal : si l’env a changé, les conteneurs sont recréés. Si vous voulez zéro downtime,
faites‑le avec des load balancers et des redémarrages roulants, pas en espérant que Compose déduise votre intention.

Histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Une équipe B2B SaaS faisait tourner des stacks basés sur Compose pour des services internes : métriques, runners de jobs, et une base legacy.
Ils étaient allergiques au « clever ». Leur déploiement en production exigeait trois vérifications :
rendre la config, valider les IDs d’images en cours, et enregistrer la somme de contrôle effective de l’environnement.

Un vendredi, un changement merge a introduit une nouvelle variable RATE_LIMIT_MODE utilisée dans l’interpolation de Compose
pour sélectionner une image sidecar. Le développeur l’a ajoutée à .env.example mais a oublié la source d’env de production.
Le pipeline CI ne l’exportait pas non plus.

Le job de déploiement a échoué tôt parce que leur fichier Compose utilisait ${RATE_LIMIT_MODE?must be set}.
C’est tout le truc : ils ont transformé une interpolation vide silencieuse en un arrêt brutal. Pas de déploiement partiel, pas de comportement mystérieux.

Ils ont corrigé le pipeline, déployé lundi, et personne n’a été pagé. C’était si peu événementiel que l’équipe en a été agacée.
C’est ainsi qu’on sait que c’était correct.

Erreurs courantes : symptôme → cause racine → correctif

1) Symptom : le tag d’image devient vide ou « latest » de façon inattendue

Cause racine : Variable de rendu manquante ou vide, Compose interpole en chaîne vide ; ou la CI exporte une variable vide qui écrase .env.

Correctif : Utilisez l’interpolation requise : image: myapp:${IMAGE_TAG?set IMAGE_TAG}. En CI, échouez si IMAGE_TAG est vide. Validez avec docker compose config.

2) Symptom : « J’ai mis à jour .env mais le conteneur n’a pas changé de comportement »

Cause racine : Le conteneur n’a pas été recréé ; le conteneur en cours conserve l’ancien environnement.

Correctif : Appliquez les changements avec docker compose up -d --force-recreate (ou docker compose restart si approprié, mais la recréation est plus sûre pour les changements d’env). Vérifiez avec docker inspect ... Config.Env.

3) Symptom : la prod utilise des réglages dev même si prod.env existe

Cause racine : Compose lit .env depuis le répertoire de travail courant, pas depuis le chemin prévu ; ou --env-file non fourni dans l’automatisation.

Correctif : Dans systemd/CI, exécutez depuis le répertoire de projet ou spécifiez --env-file /srv/app/.env. Ajoutez une garde qui imprime la somme de contrôle du fichier env pendant le déploiement.

4) Symptom : l’authentification par mot de passe échoue, alors que la valeur « semble correcte »

Cause racine : CRLF ou espaces finaux dans .env injectent des caractères cachés (souvent \r) dans la valeur.

Correctif : Normalisez les fins de ligne (sed -i 's/\r$//'), et validez en affichant la repr ou un hexdump de la valeur dans un conteneur de test contrôlé.

5) Symptom : la base de données a « perdu » des données après un redeploy

Cause racine : Changement de nom de projet (changement de nom de répertoire, COMPOSE_PROJECT_NAME modifié), créant un nouveau volume avec un nom différent.

Correctif : Pincez le nom de projet explicitement pour la production. Auditez docker volume ls et docker inspect des mounts avant nettoyage. Traitez les noms de volumes comme de l’état.

6) Symptom : les variables dans le conteneur ne correspondent pas à ce qui est dans .env

Cause racine : Confusion entre .env (rendu de Compose) et env_file (runtime du conteneur) ; ou l’environnement hôte écrase des valeurs.

Correctif : Décidez quelle source est faisant autorité. Pour des valeurs runtime critiques, utilisez des clés environment: explicites et alimentez‑les depuis un fichier env contrôlé. Pour le rendu, passez via --env-file et validez la sortie de configuration.

7) Symptom : une variable avec des guillemets a un comportement étrange

Cause racine : Les guillemets sont traités littéralement ou retirés différemment de ce que vous attendiez ; plusieurs parseurs dans la chaîne d’outils.

Correctif : Retirez les guillemets dans .env sauf si nécessaire. Quand c’est nécessaire, validez avec docker compose config et inspectez depuis l’intérieur du conteneur.

8) Symptom : le service ne démarre pas, le mapping de ports est absurde

Cause racine : L’interpolation a produit une chaîne de port invalide (vide, non numérique, contient des espaces), mais le YAML passe quand même.

Correctif : Rendre les variables obligatoires et valider les ports en CI en grepant la config rendue. N’utilisez des défauts que pour des valeurs sûres en dev.

9) Symptom : « marche en local, échoue en CI » avec le même fichier Compose

Cause racine : Le shell local exporte des variables et la CI ne le fait pas ; ou la CI a un autre encodage/newlines ; ou la CI exécute depuis un autre répertoire.

Correctif : Rendre la source d’env explicite en CI. Imprimez docker compose config (ou au moins les lignes pertinentes) et assurez-vous que c’est déterministe.

10) Symptom : des secrets apparaissent dans les logs ou les bundles de support

Cause racine : Stocker des secrets dans .env et imprimer la config rendue ou l’environnement du conteneur pendant le debug ; les variables d’env fuient facilement via les listings de processus et les dumps.

Correctif : Utilisez les secrets Compose quand c’est possible, ou des credentials montés en fichier avec des permissions strictes. Dans les outils d’incident, masquez par défaut les sorties d’environnement.

Listes de contrôle / plan étape par étape pour la production

Checklist A : Rendre l’interpolation Compose déterministe

  1. Fixez la source d’env : Dans l’automatisation, lancez toujours avec un chemin --env-file explicite et un répertoire de travail fixé.
  2. Rendez les variables critiques obligatoires : Utilisez ${VAR?message} pour les tags d’images, endpoints externes et noms de projet.
  3. Arrêtez d’exporter des variables au hasard : Nettoyez l’environnement dans les jobs CI. Si c’est nécessaire, définissez‑les explicitement.
  4. Rendez et diff : Conservez la sortie de docker compose config comme artefact de build et diff‑ez‑la par rapport aux déploiements précédents.

Checklist B : Rendre l’environnement runtime du conteneur auditable

  1. Documentez le contrat : Listez les variables d’environnement runtime requises par service (noms, signification, valeurs autorisées).
  2. Privilégiez les clés environment: explicites : Cela rend le contrat visible en revue de code.
  3. Utilisez env_file pour des valeurs en bloc, pas pour un comportement mystérieux : Gardez‑le minimal et structuré. Évitez de mélanger « dev » et « prod » dans le même fichier.
  4. Recréez sur changement d’env : Si l’environnement runtime a changé, les conteneurs doivent être recréés. Planifiez le downtime/les redémarrages roulants en conséquence.

Checklist C : Ne laissez pas l’état dériver (volumes/réseaux)

  1. Pincez le nom de projet : Définissez name: dans le modèle Compose ou COMPOSE_PROJECT_NAME dans une source d’env contrôlée.
  2. Déclarez explicitement les volumes : Utilisez des volumes nommés pour les services état‑ful ; évitez les volumes anonymes accidentels.
  3. Auditez avant nettoyage : Inspectez toujours les mounts et références de conteneurs avant de supprimer des volumes.

Checklist D : Traitez .env comme du code de production

  1. Permissions : chmod 600 .env si il contient du matériel sensible.
  2. Normalisez les fins de ligne : Imposer LF en CI.
  3. Règles de lint : Pas d’espaces autour de =, pas de tabulations, pas d’espaces finaux, schéma de clés prévisible.
  4. Contrôle des changements : Exigez une revue pour les modifications d’env, et conservez un historique (même si le fichier est stocké de manière sécurisée hors Git).

Conseils opérationnels qui préviennent la plupart des incidents .env

Utilisez des valeurs par défaut seulement pour l’ergonomie des développeurs, pas pour la sécurité en production

Les valeurs par défaut comme ${LOG_LEVEL:-debug} conviennent pour le travail local. En production, elles peuvent transformer une configuration manquante en comportement surprenant. Préférez des valeurs explicites dans les sources d’env de production et des variables requises pour tout ce qui touche l’intégrité des données, l’auth ou le routage.

Échouez tôt sur l’hôte, pas tard dans le conteneur

Si une variable est requise, faites échouer au moment du rendu. Vous voulez que le déploiement s’arrête avant de tirer des images, avant de toucher aux volumes, avant de redémarrer quoi que ce soit. C’est moins coûteux et plus sûr.

Cessez de traiter les secrets comme « juste des variables d’environnement »

Les variables d’environnement fuient. Elles se retrouvent dans les rapports de plantage, endpoints de debug, listings de processus, bundles de support accidentels, et captures d’écran humaines. Elles restent aussi dans les métadonnées des conteneurs plus longtemps que vous ne le pensez.
Utilisez les mécanismes de secrets quand vous le pouvez. Si vous ne pouvez pas, séparez au moins secrets et non-secrets et concevez vos commandes de diagnostic pour masquer par défaut.

Rendez la configuration observable

Votre système doit rapporter la version de configuration effective sans divulguer les secrets. Une somme de contrôle de config,
un SHA git, un digest d’image, et une variable « mode » non sensible suffisent généralement pour confirmer que le système est bien celui que vous croyez.

FAQ

1) Compose charge-t-il automatiquement .env ?

Typiquement, oui — .env dans le répertoire du projet est utilisé comme source pratique pour l’interpolation des variables Compose et certaines options Compose. Mais le « répertoire du projet » dépend d’où vous exécutez la commande et comment vous référencez le fichier Compose. Si vous lancez depuis le mauvais répertoire, vous pouvez charger silencieusement le mauvais .env ou aucun.

2) .env est-il identique à env_file ?

Non. .env influence couramment l’interpolation au moment du rendu de Compose. env_file injecte
des variables dans le conteneur au runtime. Les fichiers se ressemblent ; les sémantiques sont différentes. Les confondre est un mode d’échec classique.

3) Pourquoi mon .env n’a-t-il pas pris effet après docker compose up -d ?

Parce que les conteneurs n’absorbent pas magiquement de nouvelles variables d’environnement. Si Compose ne recrée pas le conteneur,
l’environnement en cours reste le même. Utilisez docker compose up -d --force-recreate quand l’env a changé,
et vérifiez via docker inspect.

4) Qui gagne : les variables exportées shell ou .env ?

Dans beaucoup de configurations communes, les variables exportées dans l’environnement qui exécute Compose écrasent les valeurs du .env.
C’est pourquoi « ça marche sur ma machine » arrive : votre shell exporte quelque chose que la CI n’a pas, ou inversement. Rendez la source d’env explicite en automatisation.

5) Puis-je avoir plusieurs fichiers env ?

Oui, mais soyez intentionnel sur leur but : un pour le rendu (passé avec --env-file) et éventuellement un ou plusieurs pour l’injection runtime (env_file: par service). Évitez d’empiler tellement de fichiers que personne ne peut prédire le résultat.

6) Pourquoi mon application voit‑elle des guillemets dans les valeurs ?

Parce que votre parseur peut traiter les guillemets littéralement. Le format .env n’est pas une norme universelle et
différents outils interprètent guillemets et échappements différemment. Si vous avez besoin de caractères spéciaux, testez le chemin exact :
rendu via docker compose config et runtime via docker inspect.

7) Comment empêcher que des variables vides glissent en production ?

Utilisez l’interpolation requise (${VAR?message}) pour les valeurs critiques et ajoutez des contrôles CI qui échouent si
la config rendue contient des tags d’image vides, des ports vides ou des hostnames vides. C’est l’un des correctifs les plus efficaces à déployer.

8) Pourquoi un redéploiement a‑t‑il créé de nouveaux volumes et « effacé » les données ?

Probablement un changement de nom de projet. Compose préfixe les noms de volume et de réseau avec le nom de projet, qui vient du
nom du répertoire, d’une configuration explicite ou de l’environnement. Pincez‑le pour la prod afin que les volumes restent stables. Ensuite, confirmez que le conteneur DB est attaché au volume attendu avant tout nettoyage.

9) Est‑il sûr d’imprimer docker compose config dans les logs CI ?

Pas toujours. Si vous inlinez des secrets dans le fichier Compose ou les interpolez dans des champs affichés, vous pouvez fuir des identifiants. Si vous devez imprimer la config, redactez les clés sensibles ou n’imprimez que des lignes ciblées (références d’images, ports, réglages non sensibles).

10) Quand dois‑je utiliser les secrets Compose plutôt que des vars d’environnement ?

Utilisez les secrets quand vous le pouvez : identifiants, tokens API, clés privées, tout ce que vous regretteriez de voir dans un log ou un dump.
Les variables d’environnement conviennent pour la configuration non sensible et les toggles de fonctionnalités. Si vous devez utiliser des vars pour des secrets, verrouillez les permissions et réduisez les endroits où elles sont affichées.

Prochaines étapes à faire cette semaine

  1. Ajoutez une « vérification de rendu » en CI : exécutez docker compose config et échouez sur les champs critiques vides
    (tags d’image, ports, hostnames). Sauvegardez la config rendue comme artefact en masquant les secrets.
  2. Rendez les variables critiques obligatoires : convertissez ${VAR} en ${VAR?set VAR} pour
    les points d’interpolation critiques en production.
  3. Pincez le nom de projet en production : évitez la dérive accidentelle des volumes et réseaux. Traitez‑le comme de l’état.
  4. Standardisez l’exécution des déploiements : répertoire de travail fixe, --env-file explicite,
    et une politique : les changements d’env exigent recréation ou redémarrage roulant.
  5. Cessez de stocker les secrets dans des .env occasionnels : migrez vers un mécanisme de secrets ou des montages de fichiers et
    adaptez les outils de diagnostic pour éviter de les divulguer pendant les incidents.

Docker Compose fonctionne. Ce qui ne va pas, ce sont les hypothèses non dites autour de .env.
Rendez les variables explicites, la config rendue observable, et l’environnement des conteneurs vérifiable.
Alors le prochain « mystère de régression » deviendra un diff de cinq minutes plutôt qu’un weekend.

Google Search Console « Page avec redirection » : quand c’est acceptable et quand ça pose problème

Vous ouvrez Google Search Console, cliquez sur Pages, et voilà : « Page avec redirection ».
Pas indexée. Pas éligible. Pas invitée à la fête. Pendant ce temps, votre chef de produit demande pourquoi le trafic a baissé,
l’équipe marketing actualise les tableaux de bord comme si c’était un sport, et vous regardez une redirection qui fonctionne parfaitement
dans le navigateur.

Voici le point de vue des systèmes de production : « Page avec redirection » est souvent normal et même souhaitable. Mais cela devient toxique
quand vous avez accidentellement appris à Google que vos URL préférées sont instables, contradictoires ou lentes à résoudre.
C’est la différence entre une couche de canonicalisation propre et un labyrinthe de redirections construit en comité.

Ce que signifie réellement « Page avec redirection »

Dans Search Console, « Page avec redirection » est un statut d’indexation, pas un jugement moral.
Cela signifie que Google a essayé de récupérer une URL et a reçu une réponse de redirection au lieu d’une réponse de contenu finale
(typiquement un 200). Google a ensuite décidé que l’URL de départ n’était pas l’URL canonique indexable. Il n’indexe donc pas
cette URL de départ ; il suit la redirection et (peut-être) indexe la destination.

C’est pourquoi ce rapport contient souvent beaucoup d’URL que vous ne voulez pas indexer intentionnellement : les variantes HTTP anciennes,
d’anciennes structures après une migration, variantes avec ou sans slash final, variantes majuscules/minuscules, et paramètres indésirables.

La question opérationnelle clé n’est pas « Comment faire disparaître ce statut ? » mais :
Les bonnes URL de destination sont-elles indexées, classées et servies de manière fiable ?

Ce que ce n’est pas

  • Ce n’est pas une erreur par défaut. Une redirection est une réponse valide.
  • Ce n’est pas la preuve que Google est confus. C’est la preuve que Google prête attention.
  • Ce n’est pas une garantie que la destination est indexée. La redirection peut conduire à une impasse.

Comment Google le traite en coulisses (version pratique)

Googlebot récupère l’URL A, reçoit une redirection vers l’URL B, et enregistre une relation.
Si les signaux sont cohérents (la redirection est stable, B renvoie 200 et est canonique, les liens internes pointent vers B, le sitemap
référence B, les hreflang pointent vers B, etc.), Google tend à consolider les signaux d’indexation sur B et à abandonner A.
Si les signaux sont incohérents, vous obtenez l’équivalent console de haussement d’épaules.

Une réalité d’ingénierie que les spécialistes SEO oublient parfois : les redirections ne sont pas gratuites. Elles consomment du budget de crawl,
ajoutent de la latence, peuvent être mises en cache de façon étrange, et créer des comportements limites à travers les CDN, navigateurs et bots.
En production, la redirection la plus simple est celle dont vous n’avez pas besoin.

Quand c’est acceptable (et que vous devez l’ignorer)

« Page avec redirection » est sain lorsqu’il représente une canonicalisation intentionnelle ou
un mouvement planifié, et que les URL de destination sont indexées et performantes.
Vous voulez ces redirections parce qu’elles compressent les variantes d’URL en une URL préférée.

Scénarios où c’est normal

  • HTTP → HTTPS. Vous voulez que les URL HTTP redirigent définitivement.
    GSC affichera souvent les URL HTTP comme « Page avec redirection. » C’est normal.
  • www → non-www (ou l’inverse). Là encore, l’hôte non préféré doit rediriger.
  • Normalisation du slash final. Choisissez une variante et redirigez l’autre.
  • Ancienne structure d’URL après une migration. Les anciens chemins redirigent vers les nouveaux.
  • Nettoyage des paramètres (certains paramètres de tracking, identifiants de session). Redirigez ou canonicalisez selon la sémantique.
  • Routage localisé ou régional lorsqu’il est réalisé soigneusement (et pas basé sur des heuristiques IP fragiles).

À quoi ressemble le « acceptable » dans les métriques

  • Les URL de destination apparaissent sous Indexées et montrent des impressions/clics.
  • Les redirections sont à saut unique (A → B), pas A → B → C → D.
  • Le type de redirection est majoritairement 301/308 pour les déplacements permanents (avec rares exceptions).
  • Les liens internes, les balises canonicals et les sitemaps pointent massivement vers les URL de destination.
  • Les logs serveur montrent que Googlebot récupère avec succès le contenu de destination (200).

Si ces conditions sont réunies, résistez à l’envie de « corriger » le rapport en essayant d’indexer les URL redirigées.
Indexer d’anciennes URL, c’est comme garder son ancien numéro de pager parce que certaines personnes l’ont encore.
Vous ne voulez pas de ça.

Quand ça pose problème (et comment cela se manifeste)

« Page avec redirection » nuit lorsqu’il masque un désalignement entre ce que vous voulez indexer et ce que
Google peut récupérer, rendre et considérer comme canonique de manière fiable. Cela pose aussi problème lorsque les redirections sont
utilisées comme rustine pour des problèmes plus profonds (contenu dupliqué, locales mal routées, liens internes cassés, protocole/hôte incohérent).

Modes de défaillance à fort impact

1) Chaînes de redirection et gaspillage de crawl

Les chaînes se produisent lorsque plusieurs règles de normalisation s’empilent : HTTP → HTTPS → www → ajout de slash final → réécriture vers nouveau chemin.
Chaque saut ajoute de la latence et une probabilité d’échec. Google suivra les chaînes, mais pas indéfiniment, et pas sans coût.
Les chaînes augmentent aussi le risque de créer une boucle par accident.

2) Redirection vers une destination non indexable

Rediriger vers une URL qui renvoie 404, 410, 5xx, bloquée par robots.txt, ou en « noindex » revient à supprimer silencieusement des pages.
GSC affichera l’URL de départ comme « Page avec redirection », mais votre vrai problème est que la destination ne peut pas être indexée.

3) 302/307 utilisées comme redirection « permanente »

Les redirections temporaires ne sont pas toujours mauvaises, mais elles sont faciles à mal utiliser. Si vous laissez un 302 en place pendant des mois, Google peut
éventuellement le traiter comme un 301, ou bien il peut garder l’ancienne URL dans l’index plus longtemps que prévu. Ce n’est pas une stratégie ;
c’est de l’indécision en forme HTTP.

4) Signaux mixtes : la redirection dit une chose, le canonical en dit une autre

Si l’URL A redirige vers B, mais que le canonical de B pointe vers A (ou vers C), vous avez créé un conflit de canonicalisation.
Google choisira un gagnant. Il se peut que ce ne soit pas votre préféré.

5) Redirections déclenchées par user-agent, géolocalisation, cookies ou JS

Les redirections conditionnelles sont la façon la plus rapide de créer un SEO « ça marche sur mon poste ». Googlebot n’est pas votre navigateur.
Votre edge CDN n’est pas votre origine. Votre origine n’est pas votre environnement de staging. Si la redirection dépend de conditions,
vous devez la tester comme Google la voit.

6) Sitemaps remplis d’URL qui redirigent

Un sitemap est supposé lister des URL canoniques indexables. Quand vous fournissez à Google des milliers d’URL redirigées dans le sitemap,
vous l’envoyez effectivement en mission. Il obéira pendant un moment, puis vous dépriorisera silencieusement.

Blague n°1 : Les chaînes de redirection sont comme les chaînes d’approbation en entreprise — personne ne sait qui a ajouté le dernier saut, mais tout le monde subit la latence.

À quoi ressemble le « qui fait mal » dans les résultats

  • Les URL préférées ne sont pas indexées, ou sont indexées de façon intermittente.
  • Le rapport de couverture montre des pics de « Dupliqué, Google a choisi une autre canonique » en même temps que des redirections.
  • Le rapport de performance montre une chute des impressions pour des pages migrées, sans récupération.
  • Les logs serveur montrent Googlebot frappant les URL redirigeantes à répétition au lieu des destinations canoniques.
  • Une grande partie de l’activité de crawl est consacrée aux redirections plutôt qu’aux URL de contenu.

Physique des redirections : 301/302/307/308, mise en cache et canonicalisation

Codes d’état, vus comme un opérateur les considère

  • 301 (Moved Permanently) : « Voici la nouvelle adresse. » Mis en cache agressivement par les clients et souvent par les intermédiaires. Bon pour les déplacements canoniques.
  • 302 (Found) : « Temporairement ici. » Historiquement traité comme temporaire. Les moteurs de recherche sont devenus plus flexibles, mais votre intention compte.
  • 307 (Temporary Redirect) : Comme 302 mais préserve la sémantique de la méthode plus strictement. Principalement pertinent pour les API.
  • 308 (Permanent Redirect) : Comme 301 mais préserve la sémantique de la méthode plus strictement. De plus en plus courant.

Balises canonical vs redirections : choisissez votre arme

Une redirection est une instruction côté serveur : « Ne restez pas ici. » Un canonical est un indice intégré à une page : « Indexez plutôt l’autre. »
Si vous pouvez rediriger en toute sécurité (sans impact utilisateur, sans raison fonctionnelle de garder l’ancienne URL), faites-le. C’est plus fort et plus propre.
Utilisez les canonicals pour les cas où le duplicata doit rester accessible (filtres, tris, paramètres de tracking, vues imprimables).

Mais ne les mélangez pas à la légère. Si vous redirigez A → B, alors le canonical de B devrait presque toujours être B. Vous voulez une histoire cohérente.

Latence et fiabilité : pourquoi les SRE se soucient des « simples redirections »

Chaque saut est une requête de plus qui peut échouer, une poignée de main TLS en plus, une recherche de cache en plus, un autre endroit où un en-tête mal configuré peut tout casser.
Multipliez par le taux de crawl de Googlebot et votre propre trafic utilisateur et vous avez un coût réel.

Une citation à épingler au tableau de bord : « L’espoir n’est pas une stratégie. » — Gene Kranz.
Les redirections laissées en attente dans l’espoir que les moteurs comprennent sont un incident au ralenti.

Comportement de cache qu’on ne peut pas ignorer

Les redirections permanentes peuvent être mises en cache longtemps. Si vous déployez accidentellement un mauvais 301, vous ne corrigez pas juste le serveur et passez à autre chose.
Navigateurs, CDN et bots peuvent continuer à suivre l’ancien chemin. C’est pourquoi les changements de redirection méritent une gestion des changements.

Mode d’emploi pour un diagnostic rapide

Quand « Page avec redirection » semble suspect, ne commencez pas par réécrire la moitié de vos règles de réécriture. Commencez par des preuves.
Cette séquence trouve rapidement le goulot d’étranglement, même lorsque plusieurs équipes ont touché la pile.

1) Confirmer la destination finale et le nombre de sauts

  • Prenez un échantillon d’URL affectées depuis GSC.
  • Suivez les redirections et enregistrez : nombre de sauts, codes d’état, URL finale, statut final.
  • Si le nombre de sauts > 1, vous avez déjà du travail concret à faire.

2) Valider que la destination est indexable

  • Le statut final doit être 200.
  • Pas de balise ou d’en-tête « noindex ».
  • Non bloquée par robots.txt.
  • Le canonical pointe vers elle-même (ou vers un canonical clairement intentionnel).

3) Vérifier si votre propre site vous sabote

  • Les liens internes doivent pointer vers les destinations finales, pas vers des URL redirigeantes.
  • Les sitemaps doivent lister les URL canoniques, pas celles qui redirigent.
  • Les hreflang (si utilisées) doivent référencer les destinations canoniques.

4) Examiner les logs serveur pour le comportement de Googlebot

  • Googlebot frappe-t-il à répétition les URL redirigeantes ? Cela suggère que les sources de découverte pointent encore vers elles.
  • Googlebot échoue-t-il sur la destination (timeouts, 5xx, bloqué) ? C’est un problème de fiabilité, pas un problème « SEO ».

5) Si c’est une migration : comparer l’ancienne et la nouvelle couverture

  • Les anciennes URL devraient être « Page avec redirection. » Les nouvelles URL devraient être indexées.
  • Si les nouvelles URL ne sont pas indexées, vous avez probablement l’un de ces problèmes : noindex, blocage robots, faible maillage interne, ou conflits de canonical.

Tâches pratiques : commandes, sorties, décisions (12+)

Voici les contrôles que j’exécute réellement. Chaque tâche inclut : la commande, ce que signifie une sortie typique, et quelle décision prendre ensuite.
Remplacez les domaines et chemins d’exemple par les vôtres. Les commandes supposent que vous pouvez atteindre le site depuis un shell.

Task 1: Follow redirects and count hops

cr0x@server:~$ curl -sSIL -o /dev/null -w "final=%{url_effective} code=%{http_code} redirects=%{num_redirects}\n" http://example.com/Old-Path
final=https://www.example.com/new-path/ code=200 redirects=2

Signification : Deux redirections ont eu lieu. Le final est 200.
Décision : Si redirects > 1, essayez de regrouper les règles pour que la première réponse pointe directement vers l’URL finale.

Task 2: Print the full redirect chain (see each Location)

cr0x@server:~$ curl -sSIL http://example.com/Old-Path | sed -n '1,120p'
HTTP/1.1 301 Moved Permanently
Location: https://example.com/Old-Path
HTTP/2 301
location: https://www.example.com/new-path/
HTTP/2 200
content-type: text/html; charset=UTF-8

Signification : HTTP→HTTPS puis normalisation hôte/chemin.
Décision : Changez la première redirection pour qu’elle aille directement vers l’hôte/chemin final, pas vers un intermédiaire.

Task 3: Detect redirect loops

cr0x@server:~$ curl -sSIL --max-redirs 10 https://www.example.com/loop-test/ | tail -n +1
curl: (47) Maximum (10) redirects followed

Signification : Boucle ou chaîne excessive.
Décision : Traitez-le comme un incident : identifiez l’ensemble de règles (CDN, load balancer, origine) créant la boucle et corrigez-le avant de penser à l’indexation.

Task 4: Verify canonical tag on destination

cr0x@server:~$ curl -sS https://www.example.com/new-path/ | grep -i -m1 'rel="canonical"'
<link rel="canonical" href="https://www.example.com/new-path/" />

Signification : Le canonical pointe vers lui-même. Bon.
Décision : Si le canonical pointe ailleurs de façon inattendue, corrigez les templates ou en-têtes ; sinon Google peut ignorer votre intention de redirection.

Task 5: Check for noindex on destination (meta tag)

cr0x@server:~$ curl -sS https://www.example.com/new-path/ | grep -i -m1 'noindex'

Signification : Aucune sortie signifie que la balise meta « noindex » n’a pas été trouvée dans la première correspondance.
Décision : Si vous voyez noindex, stoppez. C’est pourquoi cela ne s’indexe pas. Corrigez la configuration de release, les drapeaux CMS, ou la fuite d’environnement staging.

Task 6: Check X-Robots-Tag header for noindex

cr0x@server:~$ curl -sSIL https://www.example.com/new-path/ | grep -i '^x-robots-tag'
X-Robots-Tag: index, follow

Signification : Les en-têtes autorisent l’indexation.
Décision : Si vous voyez « noindex », corrigez-le à la source (app, règles CDN, ou middleware de sécurité). Les en-têtes priment sur les bonnes intentions.

Task 7: Confirm robots.txt isn’t blocking the destination

cr0x@server:~$ curl -sS https://www.example.com/robots.txt | sed -n '1,120p'
User-agent: *
Disallow: /private/

Signification : robots.txt basique affiché.
Décision : Si le chemin de destination est interdit, Google peut voir la redirection mais ne pas crawler le contenu. Mettez à jour robots.txt et retestez dans GSC.

Task 8: Check whether your sitemap lists redirecting URLs

cr0x@server:~$ curl -sS https://www.example.com/sitemap.xml | grep -n 'http://example.com' | head
42:  <loc>http://example.com/old-path</loc>

Signification : Le sitemap contient des URL non canoniques (hôte HTTP).
Décision : Régénérez les sitemaps pour lister uniquement les URL canoniques finales. C’est un nettoyage à faible risque et à fort rendement.

Task 9: Spot internal links that still point to redirecting URLs

cr0x@server:~$ curl -sS https://www.example.com/ | grep -oE 'href="http://example.com[^"]+"' | head
href="http://example.com/old-path"

Signification : La page d’accueil pointe encore vers l’ancienne URL HTTP.
Décision : Corrigez la génération de liens internes (templates, champs CMS). Les liens internes sont votre propre donation de budget de crawl au fonds des redirections.

Task 10: Check redirect type (301 vs 302) at the edge

cr0x@server:~$ curl -sSIL https://example.com/old-path | head -n 5
HTTP/2 302
location: https://www.example.com/new-path/

Signification : Redirection temporaire en place.
Décision : Si le déplacement est permanent, passez en 301/308. Si c’est vraiment temporaire (maintenance, A/B), assurez-vous que c’est limité dans le temps et surveillé.

Task 11: Confirm the destination returns 200 consistently (not 403/500 sometimes)

cr0x@server:~$ for i in {1..5}; do curl -sS -o /dev/null -w "%{http_code} %{time_total}\n" https://www.example.com/new-path/; done
200 0.142
200 0.151
200 0.139
500 0.312
200 0.145

Signification : 500 intermittent. C’est un bug de fiabilité.
Décision : Ne discutez pas avec GSC tant que l’origine n’est pas stable. Corrigez les erreurs en amont, puis demandez une réindexation.

Task 12: Inspect Nginx redirect rules for double normalization

cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -nE 'return 301|rewrite .* permanent' | head -n 20
123:    return 301 https://$host$request_uri;
287:    rewrite ^/Old-Path$ /new-path/ permanent;

Signification : Plusieurs directives de redirection peuvent s’empiler (protocole + chemin).
Décision : Combinez en une redirection de canonicalisation unique lorsque possible, ou assurez-vous que l’ordre empêche le multi-saut.

Task 13: Inspect Apache rewrite rules for unintended matches

cr0x@server:~$ sudo apachectl -S 2>/dev/null | sed -n '1,80p'
VirtualHost configuration:
*:443                  is a NameVirtualHost
         default server www.example.com (/etc/apache2/sites-enabled/000-default.conf:1)

Signification : Confirme quel vhost est par défaut ; un vhost par défaut incorrect peut créer des redirections d’hôte inattendues.
Décision : Assurez-vous que le vhost de l’hôte canonique est correct et que les hôtes non canoniques redirigent explicitement vers lui en un seul saut.

Task 14: Use access logs to see whether Googlebot is stuck on redirecting URLs

cr0x@server:~$ sudo awk '$9 ~ /^30/ && $0 ~ /Googlebot/ {print $4,$7,$9,$11}' /var/log/nginx/access.log | head
[27/Dec/2025:09:12:44 /old-path 301 "-" 
[27/Dec/2025:09:12:45 /old-path 301 "-" 

Signification : Googlebot demande à répétition l’URL redirigeante. Les sources de découverte pointent encore là.
Décision : Corrigez les liens internes et les sitemaps ; envisagez de mettre à jour les références externes si vous les contrôlez (profils, propriétés détenues).

Task 15: Check for inconsistent behavior by user-agent (dangerous “smart” redirects)

cr0x@server:~$ curl -sSIL -A "Mozilla/5.0" https://www.example.com/ | head -n 5
HTTP/2 200
content-type: text/html; charset=UTF-8
cr0x@server:~$ curl -sSIL -A "Googlebot/2.1" https://www.example.com/ | head -n 5
HTTP/2 302
location: https://www.example.com/bot-landing/

Signification : Réponses différentes pour Googlebot. C’est un signal d’alerte sauf si vous avez une raison très légitime.
Décision : Supprimez la logique de redirection basée sur le UA ; cela peut ressembler à du cloaking, et cela crée de l’instabilité d’indexation.

Task 16: Validate HSTS can’t be blamed for your redirect confusion

cr0x@server:~$ curl -sSIL https://www.example.com/ | grep -i '^strict-transport-security'
Strict-Transport-Security: max-age=31536000; includeSubDomains

Signification : HSTS est activé, donc les navigateurs forceront HTTPS après le premier contact.
Décision : Ne « déboguez » pas les redirections uniquement dans un navigateur ; utilisez curl depuis un environnement propre. HSTS peut cacher le comportement HTTP et vous faire courir après des fantômes.

Trois mini-histoires d’entreprise issues des tranchées des redirections

Incident : la mauvaise hypothèse (« Google comprendra de toute façon »)

Une entreprise SaaS de taille moyenne est passée d’un CMS legacy à un framework moderne. Le plan semblait propre :
les anciennes URL redirigeraient vers les nouvelles, et le nouveau site serait plus rapide. L’ingénierie a implémenté les redirections au niveau de l’application,
et la QA a vérifié dans un navigateur. Tout le monde est rentré chez soi.

La première semaine, Search Console a commencé à se remplir de « Page avec redirection » et les impressions ont chuté pour des pages à forte valeur.
L’équipe SEO a paniqué et a demandé « supprimez les redirections ». Ce serait avoir utilisé le mauvais extincteur.
Le SRE de garde a fait l’action peu glamour : a extrait les logs serveur pour Googlebot et a rejoué les requêtes avec curl.

L’hypothèse qui les a fait tomber : « Si ça marche dans le navigateur, Googlebot voit la même chose. »
Leur CDN avait des règles d’atténuation des bots qui traitaient différemment les user-agents inconnus lors de pics de trafic.
Quand l’origine était lente, l’edge renvoyait une redirection temporaire vers une page générique « veuillez réessayer » — acceptable pour les humains,
catastrophique pour l’indexation. Googlebot suivait la redirection et trouvait du contenu maigre.

La correction n’a pas été de la « magie SEO ». Ce fut de l’hygiène de production :
ils ont exempté les bots vérifiés de la redirection d’atténuation, amélioré le cache pour les nouvelles pages, et arrêté de rediriger vers un fallback générique.
Après cela, « Page avec redirection » est resté pour les anciennes URL (attendu), tandis que les nouvelles URL se sont stabilisées et réindexées.

Optimisation qui a mal tourné : écraser les paramètres par redirections

Une organisation e‑commerce avait un problème de paramètres : des URL infinies comme ?color=blue&sort=popular&ref=ads.
Les statistiques de crawl étaient mauvaises, et quelqu’un a proposé une solution « simple » : rediriger toute URL avec paramètres vers la page de catégorie sans paramètres.
Une règle de réécriture pour les gouverner tous.

Elle a été déployée rapidement. Trop vite. Les conversions ont baissé. Le trafic organique sur les variantes longue traîne de catégorie s’est effondré.
Search Console affichait beaucoup de « Page avec redirection », mais le vrai dommage était qu’ils redirigeaient l’intention utilisateur réelle.
Certaines combinaisons de paramètres représentaient des pages filtrées pertinentes que les utilisateurs recherchaient (et qui avaient un inventaire unique).

Pire, la règle de redirection déclenchait des chaînes : URL paramétrée → catégorie propre → redirection géo → catégorie localisée.
Googlebot passait plus de temps à rebondir qu’à crawler. La latence a augmenté. Le site semblait « instable ».

Le rollback a été inconfortable mais nécessaire. Ils ont remplacé la redirection brutale par une politique :
supprimer seulement les paramètres de tracking connus (utm/ref), conserver les filtres fonctionnels indexables uniquement lorsque le contenu le justifie,
et utiliser des balises canonical pour les duplicatas. Soudain, « Page avec redirection » s’est limité aux URL indésirables, pas aux pages génératrices de revenus.

Pratique ennuyeuse mais correcte : la propreté des sitemaps et des liens internes a sauvé la situation

Une plateforme de publication a consolidé des domaines : quatre sous-domaines vers un hôte canonique.
Ils ont implémenté des redirections 301 et s’attendaient à de la turbulence. La différence : ils l’ont traité comme un changement opérationnel, pas un souhait SEO.

Avant le lancement, ils ont généré une table de correspondance (ancien → nouveau), exécuté des tests automatiques de redirection, et mis à jour les liens internes dans les templates.
Pas seulement la navigation. Pied de page, modules d’articles liés, flux RSS, tout. Ils ont aussi régénéré les sitemaps pour n’inclure que les URL canoniques
et les ont déployés en même temps.

Après le lancement, Search Console s’est rempli de « Page avec redirection » pour les anciens hôtes (comme prévu), mais le nouvel hôte s’est indexé rapidement.
Les statistiques de crawl ont montré que Googlebot a quitté les anciennes URL plus vite que lors de leurs migrations précédentes.
Leur monitoring basé sur les logs a montré une forte diminution des hits sur les redirections au fil des semaines, signe que les sources de découverte étaient propres.

La leçon n’était pas glamour : le travail ennuyeux évite la panne excitante.
Les redirections sont un pont. Il faut quand même transférer le trafic, les liens et les signaux de l’autre côté.

Erreurs courantes : symptôme → cause racine → correctif

1) Symptom: « Page with redirect » spikes after a deploy

Cause racine : Une nouvelle règle a introduit des redirections multi-sauts ou des boucles (souvent slash final + locale + normalisation d’hôte).
Correctif : Exécutez des tests de chaîne de redirection sur un échantillon d’URL ; réduisez à un saut ; ajoutez des tests de régression en CI pour les URL canoniques.

2) Symptom: Old URLs show “Page with redirect,” but new URLs are “Crawled — currently not indexed”

Cause racine : Les pages de destination sont de faible qualité/maigres, bloquées, lentes, ou ont des contradictions canonical/noindex.
Correctif : Vérifiez que la destination renvoie 200, est indexable, a un canonical auto‑référencé, et que le contenu est substantiel. Corrigez les performances et les templates.

3) Symptom: GSC shows “Page with redirect” for URLs that should be final

Cause racine : Les liens internes ou le sitemap pointent vers des variantes non canoniques, donc Google continue de découvrir la mauvaise version en premier.
Correctif : Mettez à jour les liens internes, le sitemap, les hreflang et les données structurées pour référencer uniquement les destinations canoniques.

4) Symptom: Redirects work in browser, fail for Googlebot

Cause racine : Logique conditionnelle basée sur user-agent, cookies, géo, ou mitigation bots au niveau CDN/WAF.
Correctif : Testez avec l’UA Googlebot, comparez les en-têtes, retirez les redirections conditionnelles, et assurez que la même canonicalisation s’applique de façon cohérente.

5) Symptom: Pages disappear after you “cleaned up” parameters

Cause racine : La règle de redirection a écrasé des URL significatives en pages génériques, supprimant la pertinence longue traîne.
Correctif : Ne redirigez/supprimez que les paramètres de tracking ; gérez les filtres fonctionnels avec des canonicals, des règles noindex, ou autorisez l’indexation sélective.

6) Symptom: Redirecting URLs stay in the index for months

Cause racine : Redirections temporaires (302/307) utilisées pour des déplacements permanents, ou signaux canoniques incohérents.
Correctif : Utilisez 301/308 pour les déplacements permanents ; assurez-vous que la destination est canonique ; assurez-vous que les liens internes et le sitemap pointent vers la destination.

7) Symptom: Redirects cause intermittent 5xx and crawling drops

Cause racine : La gestion des redirections au niveau applicatif déclenche une logique coûteuse ; surcharge de l’origine ; manque de cache ; frais de handshake TLS à chaque saut.
Correctif : Déplacez les redirections vers l’edge/serveur web lorsque possible ; mettez en cache les redirections ; réduisez les sauts ; surveillez la p95 de latence sur les endpoints de redirection.

Blague n°2 : Le moyen le plus rapide de trouver une règle de redirection non documentée est de la supprimer et d’attendre qu’une personne importante le remarque.

Listes de contrôle / plan pas à pas

Checklist A: You see “Page with redirect” and want to know if you should care

  1. Choisissez 20 URL du rapport (mélange d’importantes et aléatoires).
  2. Exécutez curl avec comptage de redirections. Si >1 saut sur beaucoup, il faut s’en préoccuper.
  3. Confirmez que les URL finales renvoient 200 et sont indexables (noindex/robots/canonical).
  4. Vérifiez si les URL finales sont indexées et obtiennent des impressions.
  5. Si les URL finales sont saines, considérez « Page avec redirection » comme informationnel.

Checklist B: Redirect cleanup that won’t cause a new incident

  1. Inventoriez les règles de redirection actuelles à travers les couches : CDN/WAF, load balancer, serveur web, app.
  2. Définissez une politique d’URL canonique : protocole, hôte, slash final, minuscules, motifs de locale.
  3. Assurez un saut unique vers le canonique quand c’est possible.
  4. Mettez à jour les liens internes et les templates pour utiliser les URL canoniques.
  5. Régénérez les sitemaps pour nister que les URL canoniques.
  6. Déployez avec surveillance : taux de redirection, 4xx/5xx sur la destination, latence.
  7. Après déploiement, échantillonnez les logs Googlebot et vérifiez qu’il atteint des pages 200.

Checklist C: Migration-specific plan (domains or URL structures)

  1. Créez un fichier de mapping (ancien → nouveau) pour toutes les URL à forte valeur ; ne vous fiez pas uniquement aux regex.
  2. Implémentez des redirections 301/308 et testez les boucles et chaînes.
  3. Maintenez la parité de contenu : titres, headings, données structurées lorsque pertinent.
  4. Assurez que les nouvelles pages ont des canonicals auto-référencées.
  5. Basculer les sitemaps vers les nouvelles URL au lancement.
  6. Surveillez l’indexation : les nouvelles pages doivent monter pendant que les anciennes deviennent « Page avec redirection ».
  7. Gardez les redirections assez longtemps (mois à années selon l’écosystème), pas deux semaines parce que quelqu’un veut des « configs propres ».

Faits intéressants et contexte historique

  • Fait 1 : Les codes HTTP 301 et 302 datent des premières spécifications HTTP ; le web déplace des pages depuis quasiment toujours.
  • Fait 2 : 307 et 308 ont été introduits plus tard pour clarifier le comportement préservant la méthode ; ils importent davantage pour les API mais apparaissent dans les piles modernes.
  • Fait 3 : Les moteurs de recherche traitaient historiquement le 302 comme « ne pas transmettre les signaux », mais avec le temps ils se sont montrés plus flexibles quand la redirection persiste.
  • Fait 4 : HSTS peut rendre les redirections HTTP→HTTPS invisibles lors de tests dans le navigateur parce que le navigateur passe à HTTPS avant d’effectuer la requête.
  • Fait 5 : Les CDN implémentent souvent des redirections à l’edge ; c’est plus rapide, mais cela peut créer des interactions de règles cachées avec les redirections d’origine.
  • Fait 6 : Au début, la « canonicalisation » SEO se faisait souvent avec des redirections parce que les balises canonical n’existaient pas ; plus tard, les hints canonical sont devenus un outil standard.
  • Fait 7 : Les chaînes de redirection sont devenues plus courantes à mesure que les piles se sont empilées : CMS + framework + CDN + WAF + load balancer, chacun « aidant » à normaliser.
  • Fait 8 : Les bots ne se comportent pas comme les utilisateurs : ils peuvent crawler à grande échelle, réessayer agressivement, et amplifier de petites inefficacités en coûts d’infrastructure importants.

FAQ

1) Dois-je essayer de faire disparaître « Page avec redirection » dans Search Console ?

Pas comme objectif. Votre objectif est que les URL de destination soient indexées et performantes. Que les URL redirigées soient « non indexées » est attendu.
Nettoyez seulement lorsque le comportement de redirection est inefficace ou incohérent.

2) « Page avec redirection » est-ce une pénalité ?

Non. C’est une classification. La pénalité est ce que vous faites ensuite — par exemple garder des chaînes, rediriger vers des pages maigres, ou envoyer des signaux canoniques mixtes.

3) Combien de redirections sont trop ?

En pratique : visez un saut. Deux est survivable. Plus que ça sent la fiabilité douteuse, et ça peut ralentir le crawling et gaspiller du budget.
Si vous voyez 3+, corrigez sauf s’il existe une raison spécifique.

4) Un 302 nuit-il au SEO comparé à un 301 ?

Parfois. Si le déplacement est permanent, utilisez 301 ou 308. Un 302 durable peut fonctionner, mais il communique de l’incertitude et peut retarder la consolidation.
Ne basez pas votre stratégie d’indexation sur « Google traitera probablement ça comme un 301 finalement ».

5) Pourquoi mon sitemap montre des URL que GSC dit être « Page avec redirection » ?

Parce que votre générateur de sitemap utilise la mauvaise base d’URL (HTTP vs HTTPS, hôte erroné) ou qu’il émet des chemins legacy.
Corrigez le générateur pour que les sitemaps listent uniquement les URL finales canoniques. C’est l’une des victoires les plus faciles sur ce sujet.

6) Et si j’ai besoin des deux versions accessibles (comme des pages filtrées), mais que je ne veux pas qu’elles soient indexées ?

Ne les redirigez pas si elles sont nécessaires fonctionnellement. Gardez-les accessibles, puis utilisez des balises canonical ou des règles noindex de façon délibérée.
Les redirections servent « ceci ne doit pas exister en tant que page d’atterrissage ».

7) « Page avec redirection » peut-il être causé par des redirections JavaScript ?

Oui, mais c’est le mode difficile. Les redirections basées sur JS peuvent être plus lentes, moins fiables pour les bots, et paraître suspectes si elles sont abusées.
Préférez les redirections côté serveur sauf si vous avez une raison forte.

8) Combien de temps dois-je garder les redirections après une migration ?

Plus longtemps que vous ne le pensez. Des mois au minimum ; souvent un an ou plus pour des sites significatifs, surtout si les anciennes URL sont largement liées.
Supprimer les redirections trop tôt revient à transformer votre migration en une récolte permanente de 404.

9) Pourquoi les URL redirigées sont-elles encore beaucoup crawlées ?

Google continue de les trouver via des liens internes, des sitemaps ou des liens externes. Les sources internes sont sous votre contrôle ; corrigez-les en premier.
Les liens externes mettent du temps à décliner. L’objectif est d’arrêter d’alimenter le problème.

10) « Page avec redirection » peut-il cacher une mauvaise configuration de sécurité ou de WAF ?

Absolument. Les WAF redirigent parfois le trafic suspect, le trafic soumis à taux-limit, ou certains user agents. Si Googlebot reçoit ce traitement,
vous verrez de l’instabilité d’indexation. Confirmez le comportement avec des tests user-agent et les logs edge.

Conclusion : prochaines étapes pratiques

« Page avec redirection » n’est pas votre ennemi. C’est une lampe torche. Parfois elle éclaire des URL que vous avez intentionnellement retirées. Super.
Parfois elle expose la dette de redirection : chaînes, boucles, canonicals mixtes, et redirections « temporaires » devenues permanentes par paresse.

Prochaines étapes qui rapportent vite :

  1. Échantillonnez 20–50 URL du rapport et mesurez le nombre de sauts avec curl.
  2. Confirmez que les destinations sont indexables (200, pas de noindex, pas bloquées, canonical auto‑référencé).
  3. Corrigez les liens internes et les sitemaps pour pointer vers les URL canoniques finales.
  4. Réduisez les redirections à un saut et standardisez sur 301/308 pour les déplacements permanents.
  5. Surveillez les logs : Googlebot doit passer moins de temps sur les redirections et plus sur les pages réelles.

Si vous traitez les redirections comme une infrastructure de production — observable, testable et ennuyeuse — vous obtiendrez le meilleur résultat SEO possible :
Google consacrera son temps à votre contenu plutôt qu’à votre plomberie.

MariaDB vs SQLite pour les pics d’écriture : qui gère les pointes sans drame

Les pics d’écriture n’arrivent pas poliment. Ils débarquent comme un troupeau grondant : des runners de jobs qui se réveillent en même temps, un arriéré qui se vide après un déploiement, des clients mobiles qui se reconnectent après un tunnel, ou un rechargement « oups » que vous avez promis d’exécuter « doucement ». La question n’est pas de savoir si votre base peut écrire. La question est de savoir si elle peut écrire beaucoup, tout de suite, sans transformer votre rotation d’astreinte en hobby.

MariaDB et SQLite peuvent tous deux stocker vos données. Mais en cas de pointes, ils se comportent comme des espèces différentes. MariaDB est un serveur avec contrôles de concurrence, vidage en arrière-plan, buffer pools, et une longue histoire de charges de production qui lui crient dessus. SQLite est une bibliothèque qui vit dans votre processus, brutalement efficace et merveilleusement peu exigeante — jusqu’à ce que vous lui demandiez quelque chose qui ressemble à une tempête de multi-écrivains.

La vraie question : que signifie « pic » pour votre système

« Pic d’écriture » est une expression vague qui provoque des malentendus coûteux. Il existe au moins quatre bêtes différentes que les gens appellent un pic :

  1. Pic court, forte concurrence : 500 requêtes arrivent en même temps, chacune effectuant un petit INSERT.
  2. Sursaut soutenu : taux d’écriture multiplié par 10 pendant 10–30 minutes (jobs batch, backfills).
  3. Explosion de latence en queue longue : le débit moyen semble correct, mais toutes les 20 secondes les commits se figent pendant 300–2000 ms.
  4. Falaise I/O : le disque ou le système de stockage atteint un mur de flush (comportement fsync/vidage de cache), et tout se met en file d’attente derrière.

MariaDB vs SQLite face aux « pics » concerne principalement leur comportement sous concurrence et la façon dont ils paient la durabilité. Si vous n’avez qu’un seul écrivain et que vous pouvez tolérer un certain enfilement, SQLite peut être ridiculement bon. Si vous avez de nombreux écrivains, plusieurs processus, ou devez continuer à servir des lectures pendant que les écritures s’affolent, MariaDB est généralement l’adulte dans la pièce.

Mais il y a des pièges des deux côtés. Le piège de SQLite est le verrouillage. Le piège de MariaDB est de penser que le serveur de base de données est le goulet d’étranglement alors qu’en réalité c’est le sous-système de stockage (ou votre politique de commit).

Quelques faits et historiques qui comptent vraiment

Quelques points de contexte, courts, concrets et étonnamment prédictifs du comportement en cas de pic :

  • SQLite est une bibliothèque, pas un serveur. Il n’y a pas de démon séparé ; votre application la lie et lit/écrit directement le fichier DB. C’est une superpuissance de performance et une contrainte opérationnelle.
  • La conception originale de SQLite est optimisée pour les systèmes embarqués. Elle est devenue populaire sur desktop/mobile parce que c’est « juste un fichier » et qu’elle ne nécessite pas un DBA pour la surveiller.
  • Le mode WAL dans SQLite a été introduit pour améliorer la concurrence. Il sépare lectures et écritures en ajoutant un write-ahead log, permettant aux lecteurs de continuer pendant des écritures — jusqu’à un certain point.
  • SQLite a toujours la règle d’un seul écrivain au niveau de la base. WAL aide les lecteurs, mais plusieurs écrivains concurrents se sérialisent toujours sur le verrou d’écriture.
  • MariaDB est un fork de MySQL. Le fork est survenu après l’acquisition d’Oracle ; MariaDB est devenu le choix « ami de la communauté » pour de nombreuses organisations.
  • InnoDB est devenu le moteur par défaut pour une raison. Il repose sur MVCC, des redo logs, des flush en arrière-plan et la récupération après crash — des fonctionnalités importantes quand les pics arrivent.
  • La performance de MariaDB pendant les pics dépend fortement du comportement des fsync. Votre politique de vidage des redo logs peut déplacer la douleur de « chaque commit consiste en un stall » à « certains commits subissent des stalls mais le débit s’améliore ». C’est un compromis, pas de l’argent gratuit.
  • La plupart des incidents « la base est lente » pendant des pics d’écriture sont en réalité « le stockage est lent ». La base est juste la première à l’admettre en se bloquant sur fsync.

Anatomie du chemin d’écriture : MariaDB/InnoDB vs SQLite

SQLite : un fichier, un écrivain, très peu de cérémonial

SQLite écrit dans un seul fichier de base de données (plus, en mode WAL, un fichier WAL et un fichier d’index en mémoire partagée). Votre processus envoie du SQL ; SQLite le traduit en mises à jour de pages. Lors d’un commit de transaction, SQLite doit garantir la durabilité selon vos paramètres pragmas. Cela signifie généralement forcer les données sur un stockage stable à l’aide d’appels de type fsync, selon la plateforme et le système de fichiers.

En cas de pics, le détail critique de SQLite est la rapidité avec laquelle il peut enchaîner « acquérir le verrou d’écriture → écrire les pages/WAL → politique de sync → libérer le verrou ». Si les commits sont fréquents et petits, le coût est dominé par les appels de synchronisation et les transferts de verrou. Si les commits sont groupés, SQLite peut voler.

Le mode WAL change la forme : les écrivains ajoutent au WAL et les lecteurs peuvent continuer à lire le snapshot principal. Mais il n’y a toujours qu’un seul écrivain à la fois, et les checkpoints peuvent devenir un second type de pic (plus bas).

MariaDB/InnoDB : concurrence, mise en cache et I/O en arrière-plan

MariaDB est un processus serveur avec plusieurs threads workers. InnoDB maintient un buffer pool (cache) pour les pages, un redo log (write-ahead), et souvent un undo log pour MVCC. Quand vous committez, InnoDB écrit des enregistrements redo et — selon la configuration — les vide sur disque. Les pages modifiées sont vidées en arrière-plan.

En cas de pics, la superpuissance d’InnoDB est qu’il peut accepter de nombreux écrivains concurrents, mettre le travail en file, et lisser la charge avec du flushing en arrière-plan — à condition d’avoir dimensionné correctement et que votre I/O tienne la charge. Sa faiblesse est qu’il peut quand même atteindre un mur dur où le redo log ou le flushing des pages modifiées devient urgent, et alors les pics de latence ressemblent à un effondrement synchronisé.

Il y a une idée paraphrasée de Werner Vogels (CTO d’Amazon) que les ops répètent parce qu’elle reste vraie : tout échoue, donc concevez pour la récupération et minimisez le rayon d’impact (idée paraphrasée). En zone de pics, cela signifie souvent : attendez-vous à l’amplification d’écriture et attendez-vous à ce que le disque soit le premier à se plaindre.

Qui gère les pointes mieux (et quand)

Si vous voulez une règle honnête et simple : SQLite gère les pics sans drame lorsque vous pouvez transformer la charge d’écriture en moins de transactions et que vous n’avez pas beaucoup d’écrivains entre processus. MariaDB gère les pics sans drame lorsque vous avez de nombreux écrivains concurrents, plusieurs instances applicatives, et que vous avez besoin d’un comportement prévisible sous contention — à condition que votre stockage et votre configuration ne vous sabotent pas.

SQLite gagne quand

  • Processus unique ou écrivains contrôlés : un thread écrivain, une file d’attente, ou un processus écrivain dédié.
  • Transactions courtes, commits groupés : vous pouvez commit par N enregistrements ou toutes les T millisecondes.
  • Disque local, fsync basse latence : NVMe, pas un filesystem réseau instable.
  • Vous voulez de la simplicité : pas de serveur, moins de pièces mobiles, moins de personnes réveillées à 3 h du matin.
  • Lecture majoritaire avec pics occasionnels : le mode WAL peut garder les lectures rapides pendant les écritures.

SQLite perd (bruyamment) quand

  • Beaucoup d’écrivains concurrents : ils se serialisent et vos threads applicatifs s’accumulent derrière « database is locked ».
  • Plusieurs processus écrivent en même temps : en particulier sur des hôtes ou conteneurs très chargés sans coordination.
  • Le checkpoint devient un pic : le WAL grandit, le checkpoint se déclenche, et soudain vous avez une tempête d’écriture dans votre tempête d’écriture.
  • Le stockage a des sémantiques fsync bizarres : certains stockages virtualisés ou réseau rendent la durabilité extrêmement coûteuse ou inconsistante.

MariaDB gagne quand

  • Vous avez une vraie concurrence : plusieurs instances applicatives écrivent en même temps.
  • Vous avez besoin d’outils opérationnels : réplication, sauvegardes, migrations en ligne, outils d’observabilité.
  • Vous devez isoler la charge : le buffer pool absorbe les pics, les thread pools et la mise en file évitent l’effondrement complet.
  • Vous avez besoin de sémantiques d’isolation prévisibles : MVCC avec lectures cohérentes sous charge d’écriture.

MariaDB perd quand

  • Votre disque ne peut pas flush assez vite : les flushs du redo log bloquent le monde ; la latence explose.
  • Vous avez mal dimensionné le buffer pool : trop petit et il thrash ; trop grand et l’OS cache + swapping deviennent problématiques.
  • Vous « tunez » la durabilité à l’aveugle : vous achetez du débit en vendant à votre futur vous-même un incident de perte de données.
  • Votre schéma crée des points chauds : compteurs sur une seule ligne, mauvais index, ou insert monotone qui se battent sur les mêmes structures.

Blague #1 : SQLite est l’ami toujours à l’heure — sauf si vous invitez trois autres amis à parler en même temps, alors il verrouille simplement la porte.

Les réglages de durabilité : ce que vous achetez réellement avec fsync

Les pics d’écriture sont l’endroit où les paramètres de durabilité cessent d’être théoriques. Ils deviennent une facture que votre stockage doit payer, immédiatement, en espèces.

Leviers de durabilité de SQLite

SQLite expose la durabilité via des pragmas. Les principaux pour les pics :

  • journal_mode=WAL : généralement recommandé pour les lectures concurrentes et des performances d’écriture constantes.
  • synchronous : contrôle la manière dont SQLite synchronise les données sur disque. Une durabilité supérieure signifie généralement un coût fsync plus élevé.
  • busy_timeout : n’améliore pas le débit, mais évite des échecs inutiles en attendant les verrous.
  • wal_autocheckpoint : contrôle quand SQLite tente de checkpoint (déplacer le contenu du WAL vers le fichier DB principal).

Voici la subtilité : en mode WAL, le système peut sembler excellent jusqu’à ce que le WAL grossisse et que le checkpointing devienne inévitable. Cette « taxe de checkpoint » apparaît souvent comme des pics de latence périodiques qui ressemblent à un hoquet de la base. Si vous journalisez ou insérez du time-series, cela peut mordre sévèrement.

Leviers de durabilité MariaDB/InnoDB

Dans InnoDB, les réglages critiques pour les pics concernent le flush du redo log et la rapidité d’écriture des pages modifiées :

  • innodb_flush_log_at_trx_commit : le classique compromis durabilité/débit. La valeur 1 est la plus sûre (flush à chaque commit), 2 échange un peu de durabilité pour de la vitesse, 0 est plus rapide mais plus risqué.
  • sync_binlog : si vous utilisez les binlogs pour la réplication, c’est un coût fsync additionnel.
  • innodb_redo_log_capacity (ou le dimensionnement des logs anciens) : trop petit et vous subissez des checkpoints fréquents ; trop grand et le temps de récupération change. Les pics révèlent souvent des logs sous-dimensionnés.
  • innodb_io_capacity / innodb_io_capacity_max : indique à InnoDB à quel point être agressif avec le flushing en arrière-plan.

Pour tolérer les pics, vous voulez que la base absorbe la pointe et vide progressivement plutôt que de paniquer et vider tout d’un coup. Le flushing panique, c’est là où la latence devient « intéressante ».

Schémas courants de pics et ce qui casse en premier

Schéma : petites transactions à haut QPS

C’est la classique boucle « insert une ligne et commit », multipliée par la concurrence. C’est une tempête de commits.

  • SQLite : contention de verrous + coût des fsync. Vous verrez « database is locked » ou de longs temps d’attente à moins de mettre en file les écritures et de regrouper les commits.
  • MariaDB : peut gérer la concurrence, mais le fsync par commit peut dominer la latence. Vous verrez beaucoup de commits de trx, des attentes de log flush, et une saturation I/O.

Schéma : backfill avec index lourds

Vous ajoutez des colonnes, backfillez et mettez à jour des index secondaires. Maintenant chaque écriture se déploie en plusieurs mises à jour de B-tree.

  • SQLite : l’écrivain unique le rend prévisible mais lent ; la fenêtre de verrou est plus longue, donc tout le monde attend plus longtemps.
  • MariaDB : le débit dépend du buffer pool et de l’I/O. Les index chauds peuvent provoquer de la contention sur les latches ; trop de threads peut empirer la situation.

Schéma : le pic coïncide avec le cycle de checkpoint/flush

C’est le scénario « ça va, ça va, ça va… pourquoi tout brûle toutes les 30 secondes ? ».

  • SQLite WAL checkpoint : les cycles longs de checkpoint peuvent bloquer ou ralentir les écritures, selon le mode et les conditions.
  • InnoDB checkpoint : le redo log se remplit, les pages modifiées doivent être vidées, et le travail au premier plan commence à attendre l’I/O en arrière-plan.

Schéma : jitter de latence du stockage

Tout est normal jusqu’à ce que le disque fasse une pause. Volumes cloud, flushs de cache RAID, voisin bruyant, commits du journal du système de fichiers — choisissez votre méchant.

  • SQLite : votre thread applicatif est la base ; il se bloque. Les pics de latence se répercutent directement sur la latence des requêtes.
  • MariaDB : peut mettre en file et paralléliser, mais finalement les threads du serveur se bloquent aussi. La différence est que vous pouvez le voir depuis l’intérieur du moteur via des compteurs d’état et des logs.

Blague #2 : « Nous allons juste rendre ça synchrone et rapide » est l’équivalent base de données de « Je resterai calme et à l’heure pendant la file de sécurité à l’aéroport. »

Trois mini-récits d’entreprise depuis le terrain

Incident causé par une mauvaise hypothèse : « SQLite peut gérer quelques écrivains, non ? »

Une équipe produit de taille moyenne a livré un nouveau service d’ingestion. Chaque conteneur prenait des événements depuis une queue et les écrivait dans un fichier SQLite local pour « buffer temporaire », puis un autre job envoyait le fichier vers un stockage d’objets. L’hypothèse était que « c’est du disque local, donc ce sera rapide ». Et ça l’était — durant la démo en conditions idéales.

Puis la production est arrivée. L’autoscaling a lancé plusieurs conteneurs sur le même nœud, tous écrivant dans le même fichier SQLite via un hostPath partagé. Au moment où le trafic a monté, les écrivains se sont percutés. SQLite a fait ce pour quoi il est conçu : sérialiser les écritures. L’application a fait ce pour quoi elle est conçue : paniquer.

Les symptômes étaient désordonnés : timeouts de requêtes, erreurs « database is locked », et une boucle de retry qui a multiplié le pic. L’hôte lui-même semblait peu utilisé en CPU, ce qui a encouragé exactement le mauvais instinct de debugging : « ça ne peut pas être la base ; le CPU est inactif. »

La correction fut embarrassante par sa simplicité et adulte opérationnellement : un écrivain par fichier de base. Ils ont basculé vers des fichiers SQLite par conteneur, et introduit une file d’écriture explicite en processus. Quand ils ont eu besoin d’écritures cross-container, ils ont déplacé la couche de buffer vers MariaDB avec pooling de connexions et batching des transactions.

La leçon : SQLite est incroyable quand vous contrôlez volontairement la sérialisation des écritures. C’est le chaos quand vous découvrez la sérialisation par accident.

Optimisation qui s’est retournée contre eux : « Détendons le fsync et augmentons les threads »

Une plateforme admin interne tournait sur MariaDB. Pendant un import trimestriel, ils ont observé des pics de latence sur les commits. Quelqu’un (fatigué mais bien intentionné) a changé innodb_flush_log_at_trx_commit de 1 à 2 et a augmenté la concurrence de l’importateur de 16 à 128 threads. Ils voulaient « pousser le batch plus vite » et réduire la fenêtre de douleur.

Le débit s’est amélioré pendant environ cinq minutes. Puis le système a heurté un autre mur : churn du buffer pool plus amplification d’écriture des index secondaires. Les pages modifiées se sont accumulées plus vite que le flushing ne pouvait suivre. InnoDB a commencé un flushing agressif. La latence est passée de picotante à constamment mauvaise, et le primaire a commencé à accumuler du retard de réplication parce que le pattern de fsync du binlog a changé sous charge.

Ils n’ont pas perdu de données, mais ils ont perdu du temps : l’import a pris plus longtemps au final parce que le système oscillait entre des périodes de progrès et de longues pauses. Entre-temps, le trafic utilisateur a souffert car la base ne pouvait pas garder des temps de réponse stables.

La solution finale n’a pas été « plus de tuning ». Ce fut un modelage discipliné de la charge : limiter l’importateur, grouper les commits, et planifier le job avec une limite de débit prévisible. Ils ont gardé les paramètres de durabilité conservateurs et corrigé le vrai problème : l’importateur n’avait pas à se comporter comme un test DDoS.

La leçon : tourner des réglages sans contrôler la concurrence, c’est échanger un mode de défaillance contre un autre plus déroutant.

Pratique ennuyeuse mais correcte qui a sauvé la journée : « mesurer fsync, garder de la marge, répéter les restaurations »

Un service adjacent au paiement (du genre où vous ne pouvez pas être créatif sur la durabilité) utilisait MariaDB avec InnoDB. Toutes les quelques semaines ils avaient un pic : jobs de réconciliation plus une hausse de trafic. Cela n’a jamais causé d’incident, et personne n’a célébré cela. C’était le but.

Ils avaient une routine ennuyeuse. Ils mesuraient la latence disque (y compris fsync) en continu, pas seulement les IOPS. Ils gardaient une marge dans la capacité du redo log et dimensionnaient le buffer pool pour éviter le thrash pendant les pics. Ils répétaient aussi les restaurations selon un calendrier pour que personne n’apprenne le comportement des backups pendant un incident.

Un jour, le jitter de latence de stockage a doublé à cause d’un voisin bruyant sur l’infra sous-jacente. Le service ne s’est pas effondré. Il est devenu plus lent, les alertes se sont déclenchées tôt, et l’équipe a appliqué une mitigation connue : limiter temporairement les jobs batch et mettre en pause les écrivains non critiques. Le trafic utilisateur est resté dans le SLO.

Plus tard, quand ils ont migré vers un autre stockage, ils avaient déjà des baselines prouvant que le stockage était en cause. Les réunions d’achat sont beaucoup plus faciles quand vous montrez des graphes au lieu d’émotions.

La leçon : la pratique « ennuyeuse » de mesurer les bonnes choses et de garder de la marge est l’assurance la moins chère contre les pics.

Méthode de diagnostic rapide

Quand un pic d’écriture frappe et que tout devient bizarre, vous n’avez pas le temps de philosopher. Vous avez besoin d’un arbre de décision rapide : sommes-nous bloqués par les verrous, le CPU ou l’I/O ?

Premier point : confirmer la forme de la douleur (latence vs débit)

  • Si le débit reste élevé mais que la latence p95/p99 explose : cherchez des stalls liés à fsync/journal/checkpoint.
  • Si le débit s’effondre : cherchez la contention de verrous, l’épuisement des threads, ou la saturation du stockage.

Deuxième point : décider si c’est spécifique à SQLite ou MariaDB

  • SQLite : erreurs du type « database is locked », longues attentes, croissance du fichier WAL, ou stalls de checkpoint.
  • MariaDB : threads en attente de log flush, flushing de pages modifiées, waits de verrous de ligne, ou retard de réplication qui amplifie la pression.

Troisième point : prouver ou éliminer le stockage comme goulet

  • Vérifiez la latence disque, la profondeur de file et le comportement fsync sous charge.
  • Si le stockage est instable, presque toute base de données aura l’air coupable.

Quatrième point : arrêter d’empirer la situation

  • Limiter la source du pic (job batch, importateur, boucle de retry).
  • Grouper les commits. Réduire la concurrence. Désactiver les « retries infinis sans jitter ».
  • Capturer des preuves avant de redémarrer les services. Les redémarrages effacent les indices et ne règlent rarement la physique.

Tâches pratiques : commandes, sorties et décisions

Voici ce que vous pouvez exécuter pendant un incident ou une session de tuning. Chaque élément inclut : commande, ce que la sortie signifie, et la décision à prendre.

1) Voir si le système est saturé en I/O (Linux)

cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (db01)  12/30/2025  _x86_64_ (8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.1    0.0    4.3    31.7     0.0    51.9

Device            r/s     w/s   rkB/s   wkB/s  rrqm/s  wrqm/s  %util  await  svctm
nvme0n1          20.0  900.0   640.0 42000.0    0.0    50.0   98.7   22.4   0.8

Signification : un %util proche de 100% plus un await élevé indique que le périphérique est le limiteur ; iowait est aussi élevé.

Décision : Arrêtez de « tuner le SQL » en premier. Limitez les écrivains, réduisez la fréquence des commits, ou migrez vers un stockage plus rapide. Si c’est un volume partagé, enquêtez sur un voisin bruyant.

2) Identifier quel processus génère le plus d’I/O d’écriture

cr0x@server:~$ pidstat -d 1 3
Linux 6.5.0 (db01)  12/30/2025  _x86_64_  (8 CPU)

# Time   UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s  Command
12:01:01  999     2145      0.00  52000.00     0.00  mariadbd
12:01:01 1000     8812      0.00   1200.00     0.00  importer

Signification : MariaDB effectue la majorité des écritures ; l’importer contribue mais n’est pas dominant.

Décision : Concentrez-vous sur le comportement de flushing/redo d’InnoDB et la latence du stockage, pas seulement sur le batching applicatif.

3) Vérifier le système de fichiers et les options de montage (surprises de durabilité)

cr0x@server:~$ mount | grep -E " /var/lib/mysql | /data "
/dev/nvme0n1p2 on /var/lib/mysql type ext4 (rw,relatime,commit=5)
/dev/nvme1n1p1 on /data type xfs (rw,relatime,attr2,inode64,logbufs=8)

Signification : l’intervalle commit d’ext4 et le comportement de journalisation peuvent affecter des pics périodiques de latence.

Décision : Si vous observez des stalls périodiques alignés sur les commits du journal, considérez un tuning du filesystem ou déplacer les fichiers DB sur un volume avec de meilleures caractéristiques de latence.

4) Mesurer la latence brute de type fsync avec un test simple

cr0x@server:~$ dd if=/dev/zero of=/var/lib/mysql/sync-test.bin bs=4k count=4096 oflag=dsync
4096+0 records in
4096+0 records out
16777216 bytes (17 MB, 16 MiB) copied, 3.91 s, 4.3 MB/s

Signification : oflag=dsync force la synchronisation par bloc ; un faible débit implique un coût de sync élevé. Ce n’est pas un modèle parfait, mais cela révèle que « le stockage ment ».

Décision : Si cela paraît terrible même sur des disques « rapides », arrêtez et corrigez le stockage ou la configuration de virtualisation avant de blâmer la base.

5) MariaDB : confirmer la politique de flush d’InnoDB et la taille des redo

cr0x@server:~$ mariadb -e "SHOW VARIABLES WHERE Variable_name IN ('innodb_flush_log_at_trx_commit','sync_binlog','innodb_redo_log_capacity','innodb_io_capacity','innodb_io_capacity_max');"
+------------------------------+-----------+
| Variable_name                | Value     |
+------------------------------+-----------+
| innodb_flush_log_at_trx_commit | 1       |
| sync_binlog                   | 1        |
| innodb_redo_log_capacity      | 1073741824|
| innodb_io_capacity            | 200      |
| innodb_io_capacity_max        | 2000     |
+------------------------------+-----------+

Signification : Durabilité complète sur redo et binlog (coûteuse pendant les pics). La capacité redo peut être petite selon la charge.

Décision : Si le p99 meurt et que vous pouvez tolérer de petits compromis de durabilité, envisagez d’ajuster ces paramètres — mais seulement avec un accord commercial clair. Sinon augmentez la performance du stockage et songez au batching des commits.

6) MariaDB : vérifier si vous attendez le flush du log

cr0x@server:~$ mariadb -e "SHOW GLOBAL STATUS LIKE 'Innodb_log_waits'; SHOW GLOBAL STATUS LIKE 'Innodb_os_log_fsyncs';"
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Innodb_log_waits | 1834  |
+------------------+-------+
+----------------------+--------+
| Variable_name        | Value  |
+----------------------+--------+
| Innodb_os_log_fsyncs | 920044 |
+----------------------+--------+

Signification : Log waits signifie que des transactions ont dû attendre le flush du redo log. Pics + latence fsync = douleur.

Décision : Réduisez la fréquence des commits (batch), réduisez la concurrence, ou améliorez la latence des fsync. N’ajoutez pas juste du CPU.

7) MariaDB : vérifier la pression des pages modifiées (dette de flush)

cr0x@server:~$ mariadb -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_total';"
+--------------------------------+--------+
| Variable_name                  | Value  |
+--------------------------------+--------+
| Innodb_buffer_pool_pages_dirty | 412345 |
+--------------------------------+--------+
+--------------------------------+--------+
| Variable_name                  | Value  |
+--------------------------------+--------+
| Innodb_buffer_pool_pages_total | 524288 |
+--------------------------------+--------+

Signification : Un ratio très élevé de pages dirty suggère que le système est en retard sur le flushing ; les checkpoints peuvent forcer des stalls.

Décision : Augmentez prudemment les paramètres d’I/O capacity, assurez-vous que le stockage peut soutenir les écritures, et réduisez le taux d’écriture entrant jusqu’à stabilisation des pages dirty.

8) MariaDB : identifier les waits de verrou et les tables chaudes

cr0x@server:~$ mariadb -e "SELECT * FROM information_schema.innodb_lock_waits\G"
*************************** 1. row ***************************
requesting_trx_id: 123456
blocking_trx_id: 123455
blocked_table: `app`.`events`
blocked_lock_type: RECORD
blocking_lock_type: RECORD

Signification : Vous avez de la contention sur une table/index spécifique.

Décision : Corrigez le point chaud : ajoutez un index, changez le pattern d’accès, évitez les compteurs mono-ligne, ou sharder par clé/temps. Mettre plus de threads sur de la contention de verrou aggrave le problème.

9) MariaDB : inspecter les états actuels des threads (sur quoi attendent-ils ?)

cr0x@server:~$ mariadb -e "SHOW PROCESSLIST;"
+-----+------+-----------+------+---------+------+------------------------+------------------------------+
| Id  | User | Host      | db   | Command | Time | State                  | Info                         |
+-----+------+-----------+------+---------+------+------------------------+------------------------------+
| 101 | app  | 10.0.0.12 | app  | Query   |   12 | Waiting for handler commit | INSERT INTO events ...     |
| 102 | app  | 10.0.0.13 | app  | Query   |   11 | Waiting for handler commit | INSERT INTO events ...     |
| 103 | app  | 10.0.0.14 | app  | Sleep   |    0 |                        | NULL                         |
+-----+------+-----------+------+---------+------+------------------------+------------------------------+

Signification : « Waiting for handler commit » se corrèle souvent avec la pression sur commit/fsync.

Décision : Enquêtez sur les réglages redo/binlog et la latence disque ; envisagez le batching d’écriture.

10) SQLite : vérifier le journal_mode et les réglages synchronous

cr0x@server:~$ sqlite3 /data/app.db "PRAGMA journal_mode; PRAGMA synchronous; PRAGMA wal_autocheckpoint;"
wal
2
1000

Signification : Le mode WAL est activé ; synchronous=2 est FULL (durable, plus lent) ; autocheckpoint à 1000 pages.

Décision : Si vous subissez des pics et des stalls, demandez-vous si FULL est vraiment nécessaire. Planifiez aussi la stratégie de checkpoint (manuelle/contrôlée) plutôt que de laisser autocheckpoint vous surprendre.

11) SQLite : détecter la contention de verrou avec un test d’écriture contrôlé

cr0x@server:~$ sqlite3 /data/app.db "PRAGMA busy_timeout=2000; BEGIN IMMEDIATE; INSERT INTO events(ts, payload) VALUES(strftime('%s','now'),'x'); COMMIT;"

Signification : Si cela échoue de façon intermittente avec « database is locked », vous avez des écrivains concurrents ou des transactions longues.

Décision : Introduisez une file d’écriture single-writer, raccourcissez les transactions, et assurez-vous que les lecteurs n’ont pas de verrous trop longs (par ex. SELECT de longue durée dans une transaction).

12) SQLite : surveiller la croissance du WAL et l’état des checkpoints

cr0x@server:~$ ls -lh /data/app.db /data/app.db-wal /data/app.db-shm
-rw-r--r-- 1 app app 1.2G Dec 30 12:05 /data/app.db
-rw-r--r-- 1 app app 3.8G Dec 30 12:05 /data/app.db-wal
-rw-r--r-- 1 app app  32K Dec 30 12:05 /data/app.db-shm

Signification : Le WAL est plus gros que la DB principale. Ce n’est pas automatiquement fatal, mais c’est un signe que le checkpointing ne suit pas.

Décision : Lancez un checkpoint contrôlé pendant une fenêtre calme, ou ajustez votre charge pour que les checkpoints se produisent de façon prévisible. Enquêtez sur des lecteurs longuement vivants empêchant le progrès du checkpoint.

13) SQLite : vérifier si des lecteurs bloquent les checkpoints (base occupée)

cr0x@server:~$ sqlite3 /data/app.db "PRAGMA wal_checkpoint(TRUNCATE);"
0|0|0

Signification : Les trois nombres sont (busy, log, checkpointed). Des zéros après TRUNCATE suggèrent que le checkpoint a réussi rapidement et que le WAL a été tronqué.

Décision : Si « busy » n’est pas zéro ou si le WAL ne se tronque pas, chassez les transactions longues en lecture et corrigez-les (raccourcir les lectures, éviter de garder des transactions ouvertes).

14) MariaDB : confirmer le dimensionnement du buffer pool et la pression

cr0x@server:~$ mariadb -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';"
+-------------------------+------------+
| Variable_name           | Value      |
+-------------------------+------------+
| innodb_buffer_pool_size | 8589934592 |
+-------------------------+------------+
+-------------------------+----------+
| Variable_name           | Value    |
+-------------------------+----------+
| Innodb_buffer_pool_reads| 18403921 |
+-------------------------+----------+

Signification : Si les lectures du buffer pool augmentent rapidement pendant le pic, vous manquez le cache et effectuez plus d’I/O physique que prévu.

Décision : Augmentez le buffer pool (si la RAM le permet), réduisez l’ensemble de travail (index, patterns de requête), ou shardez la charge. N’ignorez pas l’OS ; le swapping vous ruinera la journée.

15) Suspicion de stockage en réseau : vérifier rapidement la distribution de la latence

cr0x@server:~$ ioping -c 10 -W 2000 /var/lib/mysql
4 KiB <<< /var/lib/mysql (ext4 /dev/nvme0n1p2): request=1 time=0.8 ms
4 KiB <<< /var/lib/mysql (ext4 /dev/nvme0n1p2): request=2 time=1.1 ms
4 KiB <<< /var/lib/mysql (ext4 /dev/nvme0n1p2): request=3 time=47.9 ms
...
--- /var/lib/mysql ioping statistics ---
10 requests completed in 12.3 s, min/avg/max = 0.7/6.4/47.9 ms

Signification : Ce pic de latence max est exactement ce à quoi ressemble la latence de commit quand le disque fait un hoquet.

Décision : Si vous voyez ce genre de jitter, arrêtez de chasser les micro-optimisations SQL. Réparez la QoS du stockage, migrez les volumes, ou ajoutez du buffering/batching.

16) Trouver des tempêtes de retry dans les logs applicatifs (la « montée en charge auto-amplifiée »)

cr0x@server:~$ journalctl -u app-ingester --since "10 min ago" | grep -E "database is locked|retrying" | tail -n 5
Dec 30 12:00:41 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=7
Dec 30 12:00:41 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=8
Dec 30 12:00:42 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=9
Dec 30 12:00:42 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=10
Dec 30 12:00:42 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=11

Signification : Vous n’êtes pas seulement en train d’expérimenter de la contention ; vous la multipliez avec des retries.

Décision : Ajoutez un backoff exponentiel avec jitter, plafonnez les retries, et considérez une file d’écriture single-writer. Retenter agressivement transforme un pic en incident.

Erreurs courantes (symptômes → cause racine → correction)

1) Symptom : erreurs « database is locked » pendant les pics (SQLite)

Cause racine : Plusieurs écrivains concurrents ou transactions longuement ouvertes tenant des verrous ; la réalité d’un seul écrivain se heurte à une charge multi-écrivains.

Correction : Sérialisez explicitement les écritures (un seul thread/processus écrivain), utilisez le mode WAL, définissez un busy_timeout raisonnable, et regroupez les commits. Évitez de garder des transactions de lecture ouvertes pendant les écritures.

2) Symptom : stalls périodiques de 200–2000 ms toutes les N secondes (SQLite)

Cause racine : Cycles de checkpoint WAL ou commits du journal du filesystem créant un comportement sync en rafales.

Correction : Contrôlez les checkpoints (manuels pendant les fenêtres calmes), ajustez wal_autocheckpoint, réduisez le niveau synchronous seulement si les exigences de durabilité le permettent, et validez le jitter de latence du stockage.

3) Symptom : p99 de MariaDB qui explose alors que le CPU est bas

Cause racine : Commits liés à l’I/O : la latence des fsync redo/binlog domine ; les threads attendent le log flush ou handler commit.

Correction : Groupez les transactions, réduisez la concurrence, révisez innodb_flush_log_at_trx_commit et sync_binlog avec approbation métier, et améliorez la latence du stockage.

4) Symptom : le débit s’effondre quand vous « ajoutez plus de workers » (MariaDB)

Cause racine : Contention de verrous/latches ou pression de flush amplifiée par le thrash des threads ; plus de concurrence augmente les context switches et la contention.

Correction : Limitez la concurrence, utilisez du pooling de connexions, corrigez les index/tables chauds, et tunez le flushing en arrière-plan d’InnoDB plutôt que d’ajouter des threads.

5) Symptom : le fichier WAL grossit indéfiniment (SQLite)

Cause racine : Lecteurs longuement ouverts empêchent le checkpoint de se terminer ; ou wal_autocheckpoint ne correspond pas à la charge.

Correction : Assurez-vous que les lecteurs ne gardent pas des transactions ouvertes, lancez wal_checkpoint pendant des fenêtres contrôlées, et envisagez de scinder la charge sur plusieurs fichiers DB si la contention est structurelle.

6) Symptom : pics de lag de réplication MariaDB pendant les imports

Cause racine : Fsync du binlog et flush redo sous forte écriture ; l’application single-threaded (selon la configuration) ne peut pas suivre.

Correction : Groupez les écritures, planifiez les imports, révisez les paramètres de durabilité du binlog, et assurez-vous que la configuration d’application du replica correspond à la charge. Ne considérez pas la réplication comme « gratuite ».

7) Symptom : « C’est rapide sur mon laptop, lent en prod » (les deux)

Cause racine : Les sémantiques de stockage diffèrent : NVMe sur laptop vs volume cloud partagé ; fsync et jitter de latence sont des univers différents.

Correction : Benchmarquez sur un stockage proche de la production, mesurez la distribution de latence, et fixez des SLO autour de la latence de commit p99 — pas seulement du débit moyen.

Listes de vérification / plan étape par étape

Si vous choisissez entre MariaDB et SQLite pour des écritures en rafales

  1. Comptez les écrivains, pas les requêtes. Combien de processus/hôtes peuvent écrire simultanément ?
  2. Décidez si vous pouvez imposer un écrivain unique. Si oui, SQLite reste une option.
  3. Définissez clairement les exigences de durabilité. « Nous pouvons perdre 1 seconde de données » est une exigence réelle ; « doit être durable » ne suffit pas.
  4. Mesurez la latence fsync du stockage. Si elle est instable, les deux bases auront l’air fragiles lors des pics.
  5. Planifiez les backfills. Si vous importerez ou retraiterez régulièrement des données, concevez le throttling et le batching dès le départ.

Plan de durcissement pour SQLite (pratique)

  1. Activez le mode WAL et vérifiez qu’il reste activé.
  2. Fixez busy_timeout à une valeur non triviale (des centaines à milliers de ms), et gérez SQLITE_BUSY avec backoff + jitter.
  3. Batcher les commits : commit tous les N enregistrements ou toutes les T millisecondes.
  4. Introduisez une file d’écriture avec un thread écrivain. Si plusieurs processus existent, introduisez un processus écrivain unique.
  5. Contrôlez les checkpoints : lancez wal_checkpoint pendant les périodes calmes ; ajustez wal_autocheckpoint.
  6. Surveillez la taille du WAL et le succès des checkpoints comme métriques de premier plan.

Plan de durcissement pour MariaDB (pratique)

  1. Confirmez que vous êtes sur InnoDB pour les tables à écritures rafales.
  2. Dimensionnez le buffer pool pour que l’ensemble de travail tienne autant que raisonnable sans swapping.
  3. Vérifiez la capacité du redo log ; évitez un redo trop petit qui force des checkpoints fréquents.
  4. Alignez innodb_io_capacity sur la capacité réelle du stockage (pas sur l’optimisme).
  5. Plafonnez la concurrence applicative ; utilisez du pooling de connexions ; évitez les tempêtes de threads.
  6. Batcher les écritures et utilisez des INSERT multi-row quand c’est sûr.
  7. Mesurez et alertez sur les log waits, les indicateurs de latence fsync, et le ratio de pages dirty.

Quand migrer de SQLite vers MariaDB (ou vice versa)

  • Migrer SQLite → MariaDB quand vous ne pouvez pas imposer un écrivain unique, vous avez besoin d’écritures multi-hôtes, ou les outils opérationnels (réplication/sauvegardes en ligne) comptent.
  • Migrer MariaDB → SQLite quand la charge est locale, single-writer, et que vous payez un overhead opérationnel inutile pour un petit jeu de données embarqué.

FAQ

1) SQLite peut-il gérer un débit d’écriture élevé ?

Oui — si vous batcher les transactions et gardez les écrivains sérialisés. SQLite peut être extrêmement rapide par cœur car il évite les sauts réseau et le overhead serveur.

2) Pourquoi SQLite dit-il « database is locked » au lieu de mettre les écrivains en file ?

Le modèle de verrouillage de SQLite est simple et intentionnel. Il attend que l’application contrôle la concurrence (busy_timeout, retries, et idéalement un écrivain unique). Si vous voulez que la base gère une forte concurrence multi-écrivains, vous décrivez une base de données serveur.

3) Le mode WAL est-il toujours le bon choix pour SQLite sous pics ?

Souvent, mais pas toujours. WAL aide les lectures concurrentes pendant les écritures et peut lisser une charge d’écriture constante. Il introduit aussi un comportement de checkpoint que vous devez gérer. Si vous ignorez les checkpoints, vous aurez des stalls périodiques et des fichiers WAL gigantesques.

4) Pour MariaDB, quel paramètre affecte le plus le comportement en cas de pic ?

innodb_flush_log_at_trx_commit et (si le binlog est utilisé) sync_binlog. Ils déterminent directement la fréquence à laquelle vous payez le coût fsync. Les changer modifie la durabilité, donc traitez-les comme une décision métier.

5) Pourquoi les pics d’écriture semblent parfois pire après avoir ajouté des index ?

Les index augmentent l’amplification d’écriture. Un insert devient plusieurs mises à jour de B-tree et plus de pages modifiées. Sous pic, la différence entre « une écriture » et « cinq écritures » n’est pas théorique ; c’est votre p99.

6) Dois-je mettre SQLite sur un stockage réseau ?

D’habitude non. SQLite dépend d’un verrouillage et de sémantiques de sync corrects et de faible latence. Les systèmes de fichiers réseau et certains volumes distribués peuvent rendre le verrouillage imprévisible et fsync douloureusement lent. Si vous devez le faire, testez précisément l’implémentation de stockage sous charge.

7) Si MariaDB est lent pendant les pics, dois-je simplement augmenter le CPU ?

Seulement après avoir prouvé que vous êtes CPU-bound. La plupart des douleurs lors des pics sont liées à la latence I/O ou à la contention. Ajouter du CPU à un goulet fsync est comme ajouter plus de caissiers alors que le magasin n’a qu’une seule caisse.

8) Quelle est la façon la plus simple de faire tenir mieux l’une ou l’autre base aux pics ?

Batcher les commits et limiter la concurrence. Les pics sont souvent auto-infligés par des « workers illimités » et des « commit par ligne ». Corrigez cela en premier.

9) Laquelle est la plus sûre pour la durabilité pendant les pics ?

Les deux peuvent être sûres ; les deux peuvent être configurées de manière dangereuse. Les valeurs par défaut de MariaDB ont tendance à être conservatrices pour les charges serveur. SQLite peut aussi être totalement durable, mais le coût en performance pendant les pics est plus visible car il est dans votre chemin de requête.

10) Comment savoir si je suis limité par le checkpointing ?

SQLite : le WAL grandit et les checkpoints rapportent « busy » ou ne se tronquent pas. MariaDB : les log waits augmentent, les pages dirty montent, et vous voyez des stalls liés au flushing. Dans les deux cas, corrélez avec des pics de latence disque.

Conclusion : étapes pratiques suivantes

Si vous avez des écritures en rafales et que vous hésitez entre MariaDB et SQLite, ne commencez pas par l’idéologie. Commencez par le modèle d’écriture.

  • Si vous pouvez imposer un seul écrivain, batcher les commits, et garder la base sur un stockage local à faible latence, SQLite gérera les pics discrètement et à moindre coût.
  • Si vous avez beaucoup d’écrivains répartis sur processus/hôtes et que vous avez besoin d’outils opérationnels comme la réplication et une observabilité robuste, MariaDB est le pari le plus sûr — à condition de respecter la physique des fsync et de tuner avec soin.

Puis faites le travail peu glamour qui évite le drame : mesurez la latence fsync, plafonnez la concurrence, batcher les écritures, et faites des checkpoints/flushs une partie contrôlée du système plutôt qu’une surprise. Votre futur vous sera toujours fatigué, mais au moins il sera ennuyé. C’est le but.

Dovecot : maildir vs mdbox — choisissez un stockage qui ne vous hantera pas plus tard

Vous ne remarquez pas le format de votre boîte aux lettres lorsque tout va bien. Vous le remarquez lorsque l’iPhone du PDG affiche « Cannot Get Mail », que vos disques sont à 70 % d’inactivité et pourtant chaque connexion IMAP donne l’impression de négocier avec un classeur rempli de confettis.

Le stockage du courrier est une de ces décisions d’infrastructure qui reste ennuyeuse—jusqu’à ce qu’elle devienne la seule chose dont tout le monde veut parler. Gardons-la ennuyeuse, volontairement.

La décision qui compte vraiment

« Maildir vs mdbox » ressemble à un débat de formats. Ce n’en est pas un. C’est un débat de philosophie opérationnelle :

  • Maildir mise sur les sémantiques du système de fichiers : chaque message est un fichier distinct ; les renommages atomiques sont vos amis ; la corruption tend à être localisée ; et vous avez beaucoup d’inodes.
  • mdbox mise sur l’agrégation gérée par Dovecot : les messages vivent dans de plus gros fichiers conteneurs avec des métadonnées Dovecot ; vous réduisez la pression sur les inodes ; certaines opérations peuvent être plus rapides selon les patterns d’E/S ; et quand vous faites une erreur, l’impact peut être plus vaste.

Si vous gérez un petit serveur avec un système de fichiers sain et que vous voulez un débogage simple et une restauration partielle facile, Maildir est le choix par défaut qui vieillit bien. Si vous opérez à grande échelle où le nombre d’inodes, le scan de répertoires et le coût des petits fichiers vous tuent, mdbox peut être la bonne douleur—mais seulement si vous êtes disciplinés concernant les sauvegardes, la maintenance et les outils opérationnels.

Une citation à garder sur un post-it, parce qu’elle s’applique directement aux formats de boîte aux lettres : « L’espoir n’est pas une stratégie. » — Gene Kranz.

Maildir et mdbox en un coup d’œil

Maildir : qu’est-ce que c’est

Maildir stocke chaque message comme un fichier séparé dans une structure de répertoires—typiquement cur/, new/ et tmp/ par boîte aux lettres. Les flags sont souvent encodés dans le nom de fichier. La livraison et les déplacements reposent sur le comportement de renommage atomique.

Ambiance opérationnelle : Quand quelque chose casse, vous pouvez souvent ouvrir un répertoire et voir les messages. Vous pouvez récupérer un seul utilisateur sans avoir l’impression de désamorcer une bombe.

mdbox : qu’est-ce que c’est

mdbox stocke les messages à l’intérieur de fichiers « box » gérés par Dovecot (avec des métadonnées d’index et de map associées). Pensez à Dovecot contrôlant davantage la couche de stockage : moins de fichiers, plus de structure, et plus de dépendance aux garanties de cohérence de Dovecot.

Ambiance opérationnelle : Quand c’est rapide, c’est agréable. Quand il faut réparer, vous voulez vos outils prêts et vos sauvegardes vérifiées.

Ce que vous devriez choisir (opinion)

  • Choisissez Maildir si : vous êtes une petite à moyenne structure, vous valorisez la simplicité de récupération, vous avez des SSD décents, vous utilisez des snapshots/sauvegardes, et vous voulez des domaines de panne prévisibles.
  • Choisissez mdbox si : vous avez beaucoup d’utilisateurs, beaucoup de messages, la pression sur les inodes est réelle, le coût de listing des répertoires est pénalisant, ou vous avez besoin de fonctionnalités de stockage comme un comptage de fichiers efficace—et vous êtes prêt à industrialiser la maintenance Dovecot et les drills de sauvegarde/restauration.
  • Évitez « ça n’a pas d’importance » comme décision. Ça compte le jour où vous devez restaurer une seule boîte à 3 h du matin et que votre processus de restauration se résume à « tout restaurer et prier ».

Blague #1 : Les décisions de stockage de mail sont comme les tatouages : ça semble amusant jusqu’à ce que vous essayiez de les enlever pendant la revue trimestrielle des incidents.

Faits et historique qui expliquent les compromis actuels

Un peu de contexte rend les compromis moins arbitraires. Voici des points concrets qui montrent pourquoi ces formats existent et pourquoi ils se comportent comme ils le font :

  1. Maildir a été conçu pour éviter les problèmes de verrouillage d’mbox. Le mbox traditionnel stocke une boîte entière dans un seul fichier ; l’accès concurrent causait historiquement des douleurs de verrouillage et un risque de corruption.
  2. Le « renommage atomique » de Maildir dépend des garanties du système de fichiers. Le schéma tmp→new/cur repose sur l’atomicité au sein du même filesystem.
  3. L’IMAP a multiplié les besoins en « métadonnées » de boîte. L’indexation, les flags et le suivi des UID sont devenus critiques pour la performance ; les fichiers d’index de Dovecot sont une réponse à cette réalité.
  4. Le coût des petits fichiers est devenu plus important à mesure que la rétention a grandi. Des millions de petits fichiers sollicitent les inodes, les recherches de répertoire et les outils de sauvegarde ; cette pression est une des raisons des formats agrégés.
  5. Dovecot a introduit des formats « box » pour réduire l’agitation du système de fichiers. mdbox et des designs similaires déplacent le travail du système de fichiers vers des structures gérées par Dovecot.
  6. L’évolution des systèmes de fichiers compte. Ext4, XFS, ZFS et btrfs gèrent différemment les répertoires et les métadonnées ; le même format peut être « acceptable » sur l’un et pénible sur l’autre.
  7. Les snapshots copy-on-write ont changé les attentes de sauvegarde. Avec les snapshots ZFS/btrfs, le « point dans le temps cohérent » est plus facile—mais seulement si votre modèle d’indexation et de verrouillage se comporte bien avec les snapshots.
  8. Les clients mail sont devenus plus agressifs. Les clients mobiles font des synchronisations fréquentes ; la recherche côté serveur/FTS est devenue attendue ; les formats de boîte qui amplifient les E/S de métadonnées peuvent sembler pires aujourd’hui qu’en 2008.

Comment chaque format échoue en production

Modes d’échec de Maildir

1) « Trop de fichiers » devient une vraie panne. Vous atteignez l’épuisement des inodes, les sauvegardes ralentissent, ou les scans de répertoire deviennent votre plancher de latence. Maildir ne vous prévient pas poliment ; il devient juste lent puis soudainement impossible.

2) La corruption partielle est survivable—mais coûteuse. Quelques fichiers message peuvent être corrompus par des problèmes de disque ou des transferts cassés. En général vous pouvez sauver le reste. Mais si vos fichiers d’index deviennent bizarres, les clients verront des messages manquants ou dupliqués jusqu’à ce que vous reconstruisiez les index.

3) Les sauvegardes mentent si vous ne faites pas de snapshot. Une sauvegarde fichier par fichier pendant que des livraisons sont en cours peut capturer des états incohérents (messages dans tmp/, renommages partiels). Ça peut toujours fonctionner, mais vous devez comprendre ce que « cohérent » signifie pour maildir.

Modes d’échec de mdbox

1) La cohérence des métadonnées devient votre vie. mdbox s’appuie sur les métadonnées Dovecot (indexes, fichiers map). Si celles-ci sont désynchronisées ou corrompues, la boîte peut sembler vide ou brouillée même si les fichiers box sous-jacents existent.

2) Rayon d’impact plus large par fichier. Un fichier conteneur corrompu peut affecter plus de messages. Les outils Dovecot peuvent réparer dans de nombreux cas, mais l’isolation « fichier par message » de Maildir n’est pas la valeur par défaut ici.

3) La complexité de restauration augmente. Restaurer une seule boîte peut être simple si vous avez des répertoires par utilisateur et de bons outils. Ça peut aussi être un cauchemar si vous avez fait « un volume géant et on verra bien ». La conception compte.

Blague #2 : Le meilleur format de boîte aux lettres est celui que vous pouvez restaurer pendant que votre café est encore buvable.

Modèle de performance : ce que vous payez vraiment

La taxe cachée de Maildir : métadonnées et opérations sur les répertoires

La performance de Maildir est dominée par les métadonnées du système de fichiers : création, renommage, stat, listing de répertoires et mise à jour des timestamps. Si vous avez des SSD et un système de fichiers qui gère bien les répertoires, maildir peut être très rapide. Si vous avez des disques rotatifs ou des chemins de métadonnées surchargés, maildir peut donner l’impression de tout faire sauf servir le courrier.

Quand les utilisateurs ont des centaines de milliers de messages dans un seul dossier, Maildir peut se dégrader fortement parce que le serveur effectue beaucoup d’opérations de répertoire juste pour répondre à « quoi de neuf ? » Les index Dovecot aident, mais le nombre de fichiers sous-jacent vous poursuit encore dans les sauvegardes, le temps de fsck et l’utilisation des inodes.

La taxe cachée de mdbox : structures gérées par Dovecot et flux de réparation

mdbox tend à réduire la pression sur le nombre de fichiers, ce qui peut diminuer le coût des répertoires et des inodes. Mais vous payez dans une autre devise : vous devez faire confiance et maintenir les structures de métadonnées de Dovecot. Cela signifie que vous vous souciez de l’intégrité des index, de la santé des fichiers map et de la façon dont vos sauvegardes/restaurations interagissent avec ces fichiers.

Sur les systèmes chargés, mdbox peut être plus amical au filesystem, mais il peut aussi amplifier les conséquences des réglages « astucieux » ou des pratiques de sauvegarde non sûres.

Latence vs débit : choisissez ce que vos utilisateurs ressentent

La plupart des pannes mail ne sont pas « le serveur ne peut pas gérer le débit ». Elles sont « la connexion est lente », « l’ouverture de INBOX est lente », « la recherche est lente », « la mise à jour des flags est lente ». C’est de la latence. La latence vient des allers-retours de stockage et de la contention sur les métadonnées.

Règle empirique : Si votre douleur est axée sur les métadonnées (nombre de fichiers, scans de répertoire, crawling des sauvegardes), mdbox commence à sembler meilleur. Si votre douleur est la simplicité de réparation et de récupération, Maildir est difficile à battre.

Sauvegardes, restaurations, et pourquoi « ce n’est que des fichiers » est un piège

Sauvegardes Maildir : trompeusement simples

Maildir ressemble à « juste des fichiers », ce qui détend les gens. Ne le soyez pas. Maildir en production a des états transitoires (tmp/), des renommages et des mises à jour d’index. Si vous le sauvegardez sans snapshots, vous pouvez capturer une boîte en plein vol.

Ce qui fonctionne bien :

  • Snapshots de filesystem (ZFS, btrfs, LVM thin snapshots) en lisant la sauvegarde depuis le snapshot.
  • Sauvegardes qui préservent permissions, propriété et timestamps (la livraison de mail et Dovecot y sont sensibles).
  • Pratiques régulières de reconstruction d’index lors des tests de restauration.

Sauvegardes mdbox : vous avez besoin de cohérence, pas de simples copies

mdbox exige que vous capturiez à la fois les fichiers box et les fichiers de métadonnées/index à un point dans le temps cohérent. Les sauvegardes par snapshot sont la base saine. Si vous comptez sur une copie fichier par fichier sans snapshots, vous risquez de capturer des états désaccordés—le fichier box dit une chose, l’index une autre, la map une troisième.

Restaurations : planifiez pour « un utilisateur, un dossier, un message »

La restauration qui compte n’est pas « restaurer tout le serveur ». C’est « restaurer un dossier de boîte pour un cadre parce qu’un client a synchronisé et tout supprimé ». Cette restauration devrait être un runbook, pas de l’improvisation héroïque.

Si vous ne pouvez pas restaurer une seule boîte sans tout restaurer, vous n’avez pas choisi un format de stockage ; vous avez choisi un incident futur.

Réalité de la réplication/HA

Le format de boîte ne remplace pas la stratégie de réplication. Il change juste les modes d’échec et l’ergonomie opérationnelle.

Réplication Dovecot et choix de format

Dovecot peut répliquer des boîtes au niveau applicatif. Cela peut lisser certaines différences de filesystem. Mais la réplication ne corrige pas :

  • La mauvaise planification de capacité (l’épuisement des inodes arrive toujours, juste sur deux machines).
  • Le stockage lent (vous avez maintenant du stockage lent plus la surcharge de réplication).
  • Les pratiques de sauvegarde non sûres (la réplication répliquera volontiers des suppressions et certaines formes de corruption).

Les snapshots ne sont pas la réplication, la réplication n’est pas une sauvegarde

Les snapshots offrent une récupération à un point dans le temps ; la réplication offre de la disponibilité. Vous voulez les deux si le système mail compte. Si vous ne pouvez en permettre qu’un, choisissez des sauvegardes que vous avez testées. La disponibilité sans récupération n’est qu’un moyen plus rapide de rester en panne.

Tâches pratiques : commandes, sorties et ce que vous décidez

Voici les vérifications que j’exécute réellement quand quelqu’un dit « IMAP est lent », « des messages ont disparu » ou « le disque est OK, mais le mail meurt ». Chaque tâche inclut ce que la sortie signifie et la décision que vous prenez.

1) Confirmer le format de boîte actuel

cr0x@server:~$ doveconf -n | egrep '^(mail_location|mail_attachment_dir|mail_plugins|namespace|mail_fsync)'
mail_location = maildir:~/Maildir
mail_plugins = $mail_plugins quota
mail_fsync = optimized

Sens : Ce serveur utilise maildir dans le répertoire personnel de chaque utilisateur. Si vous attendiez mdbox, vos « hypothèses de performance » sont déjà fausses.

Décision : Alignez le dépannage selon le format : la pression sur les inodes et les opérations de répertoire comptent plus avec Maildir ; l’intégrité des métadonnées et des map/index compte plus avec mdbox.

2) Vérifier la version de Dovecot (les fonctionnalités et bugs comptent)

cr0x@server:~$ dovecot --version
2.3.19.1 (9b53102964)

Sens : Vous êtes sur une 2.3.x moderne. Le comportement diffère selon les versions majeures/minores, notamment autour de la gestion des index et des valeurs par défaut de fsync.

Décision : Si vous êtes sur quelque chose d’ancien, envisagez une mise à jour avant des réglages « astucieux ». Les vieux bugs de stockage mail ne sont pas charmants.

3) Mesurer l’utilisation des inodes (le tueur silencieux de Maildir)

cr0x@server:~$ df -ih /var/vmail
Filesystem      Inodes IUsed   IFree IUse% Mounted on
/dev/sdb1         50M   41M     9M   83% /var/vmail

Sens : 83 % d’utilisation des inodes. Ce n’est pas « acceptable ». C’est « une importation ratée loin d’un arrêt ».

Décision : Si l’utilisation des inodes augmente rapidement, soit : (a) migrez vers un système de fichiers avec plus d’inodes / une stratégie d’allocation différente, (b) appliquez la rétention, (c) migrez les utilisateurs à fort volume vers mdbox, ou (d) repensez le découpage des dossiers/archivage.

4) Compter les fichiers messages dans un dossier chaud

cr0x@server:~$ find /var/vmail/acme.example/jane/Maildir/cur -type f | wc -l
287641

Sens : 287k fichiers dans un seul répertoire. Beaucoup de systèmes de fichiers gèrent mal cela sous churn, même si les lectures sont en cache.

Décision : Envisagez un partitionnement des dossiers (archives par année), des changements de politique côté client, ou la migration de cet utilisateur vers mdbox si le coût opérationnel est récurrent.

5) Identifier si Dovecot passe du temps en IO wait

cr0x@server:~$ iostat -x 1 3
Linux 6.1.0-18-amd64 (server) 	01/03/2026 	_x86_64_	(8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           3.21    0.00    1.10   24.50    0.00   71.19

Device            r/s     w/s   rkB/s   wkB/s  avgrq-sz avgqu-sz   await  r_await  w_await  svctm  %util
nvme0n1         85.00  220.00  7400.0 14800.0     95.0     3.20   10.50     5.10    12.60   0.35  10.70

Sens : Le iowait CPU est élevé (24,5 %). Le stockage n’est pas saturé (%util ~10 %), mais la latence (await) n’est pas bonne. Cela pointe souvent vers des patterns riches en sync ou une contention des métadonnées plutôt que des limites de débit brut.

Décision : Vérifiez les paramètres fsync, les processus Dovecot provoquant des vagues de sync, et les options de montage du système de fichiers. Pour Maildir, les patterns d’écriture des métadonnées peuvent provoquer cela même sur SSD.

6) Voir quels processus causent la pression IO

cr0x@server:~$ pidstat -d 1 5
Linux 6.1.0-18-amd64 (server) 	01/03/2026 	_x86_64_	(8 CPU)

# Time        UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s  Command
12:10:01     1001     23142      0.00   9200.00      0.00  dovecot
12:10:01     1001     23188      0.00   5100.00      0.00  dovecot
12:10:01        0     11422      0.00   1400.00      0.00  rsync

Sens : Dovecot écrit beaucoup (probablement des mises à jour d’index, des changements de flags, des livraisons). Il y a aussi un rsync en cours—classique « sauvegarde qui concurrence les E/S live ».

Décision : Si vous n’avez pas de snapshots, arrêtez les sauvegardes qui parcourent les fichiers pendant les pics. Passez aux snapshots pour les sauvegardes ou planifiez rsync en dehors des heures de pointe, ou limitez son débit.

7) Vérifier la santé du service Dovecot et la concurrence

cr0x@server:~$ doveadm service status
auth: client connections: 12, server connections: 12
imap: client connections: 380, server connections: 380
lmtp: client connections: 0, server connections: 0
indexer-worker: client connections: 8, server connections: 8

Sens : IMAP a 380 connexions actives. Les workers d’indexation sont actifs. Si vous êtes sous-approvisionné en indexers, les recherches et l’ouverture de boîtes peuvent traîner.

Décision : Ajustez les limites de processus avec responsabilité. Si les indexers sont saturés, augmentez le nombre de workers ou corrigez la cause (par ex. reconstruction d’index constante due à des permissions incorrectes ou caches cassés).

8) Mesurer la latence d’ouverture de boîte et de status (depuis Dovecot)

cr0x@server:~$ doveadm -v mailbox status -u jane@example.com messages recent uidnext unseen INBOX
INBOX messages=142003 recent=0 uidnext=412887 unseen=12

Sens : Cette commande doit revenir rapidement. Si elle bloque, vous avez de la latence IO, des problèmes d’index ou de la contention de verrouillage.

Décision : Si c’est lent : vérifiez la corruption d’index, des scans de filesystem coûteux, ou un stockage bloqué. Pour Maildir avec un INBOX énorme, encouragez l’archivage.

9) Identifier et reconstruire les fichiers d’index cassés en toute sécurité

cr0x@server:~$ doveadm -Dv index -u jane@example.com INBOX
doveadm(jane@example.com): Debug: Loading modules from directory: /usr/lib/dovecot/modules
doveadm(jane@example.com): Debug: Added plugin: quota
doveadm(jane@example.com): Debug: Finished indexing INBOX

Sens : Dovecot peut reconstruire les index. La sortie de debug confirme le chargement des modules et la fin de l’indexation.

Décision : Si cela corrige les messages manquants chez les clients, vous aviez une incohérence d’index plutôt qu’une perte de message. Ajoutez une maintenance d’index périodique ou réglez la cause racine (permissions, erreurs disque, resets forcés).

10) Chercher des erreurs au niveau du filesystem (le sérum de vérité ennuyeux)

cr0x@server:~$ dmesg -T | egrep -i 'ext4|xfs|btrfs|zfs|nvme|i/o error|reset|abort' | tail -n 10
[Fri Jan  3 11:58:41 2026] nvme nvme0: I/O 123 QID 6 timeout, completion polled
[Fri Jan  3 11:58:41 2026] nvme nvme0: resetting controller
[Fri Jan  3 11:58:43 2026] EXT4-fs warning (device sdb1): ext4_dx_add_entry: Directory index full, reach max htree level

Sens : Timeouts NVMe et un avertissement d’indexation de répertoire ext4. Ce n’est pas un problème Dovecot. C’est le stockage et le comportement du filesystem sous stress.

Décision : Corrigez le matériel/firmware, vérifiez la santé NVMe, et envisagez un tuning du système de fichiers ou de déplacer le stockage mail vers un filesystem mieux adapté aux gros répertoires.

11) Confirmer la santé réelle du disque (avant d’accuser le format de mail)

cr0x@server:~$ smartctl -a /dev/nvme0 | egrep -i 'critical_warning|media_errors|num_err_log_entries|temperature'
Critical Warning:                   0x00
Temperature:                       41 Celsius
Media and Data Integrity Errors:    0
Error Information Log Entries:      2

Sens : Pas d’erreurs média, mais il y a des entrées de log d’erreur. Combiné aux resets NVMe, vous pouvez avoir des problèmes intermittents de contrôleur/firmware.

Décision : Planifiez de la maintenance : mises à jour de firmware, vérifications du contrôleur, et considérez la redondance. Les formats de mail ne vous sauveront pas d’un matériel instable.

12) Vérifier la distribution des répertoires et fichiers (repérer des layouts pathologiques)

cr0x@server:~$ du -sh /var/vmail/acme.example/jane/Maildir
96G	/var/vmail/acme.example/jane/Maildir

Sens : 96 GB pour un seul utilisateur. Le volume est acceptable, mais grand + beaucoup de petits fichiers change tout.

Décision : Si une poignée d’utilisateurs domine le stockage et la performance, traitez-les spécialement : tier de stockage séparé, mdbox, ou volume dédié.

13) Vérifier les options de montage du filesystem (les tueurs de latence se cachent ici)

cr0x@server:~$ findmnt -no TARGET,SOURCE,FSTYPE,OPTIONS /var/vmail
/var/vmail /dev/sdb1 ext4 rw,relatime,errors=remount-ro

Sens : Options standard. Si vous voyez sync ou des réglages de journalisation extrêmement agressifs, vous pourriez vous être infligé de la latence.

Décision : Évitez les changements d’options de montage par mimétisme. Modifiez uniquement avec des améliorations de latence mesurées et un plan de retour arrière.

14) Pour mdbox : localiser les métadonnées clés et valider des signes d’intégrité basiques

cr0x@server:~$ ls -la /var/vmail/acme.example/jane/mdbox/ | head
total 64
drwx------  5 vmail vmail 4096 Jan  3 12:01 .
drwx------ 12 vmail vmail 4096 Jan  3 12:01 ..
-rw-------  1 vmail vmail 8192 Jan  3 11:59 dovecot.index
-rw-------  1 vmail vmail 4096 Jan  3 11:59 dovecot.index.log
-rw-------  1 vmail vmail 2048 Jan  3 12:00 dovecot.map.index
-rw-------  1 vmail vmail 4096 Jan  3 11:58 storage

Sens : La présence d’index et de fichiers map est attendue. Des fichiers manquants ou de taille zéro en fonctionnement normal peuvent indiquer une corruption ou des problèmes de permissions.

Décision : Si ces fichiers manquent ou sont illisibles, corrigez d’abord permissions/ownership ; si la corruption est suspectée, passez à une restauration depuis snapshot ou aux workflows de réparation Dovecot.

15) Observer la contention de lock active sur les fichiers de mailbox

cr0x@server:~$ lsof +D /var/vmail/acme.example/jane/Maildir 2>/dev/null | head -n 10
COMMAND   PID  USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
dovecot 23142 vmail   15r  REG  8,17     12456 918273 /var/vmail/acme.example/jane/Maildir/dovecot.index
dovecot 23142 vmail   16u  REG  8,17     40960 918274 /var/vmail/acme.example/jane/Maildir/dovecot.index.log
dovecot 23188 vmail   18r  REG  8,17     53248 918275 /var/vmail/acme.example/jane/Maildir/cur/1735891023.M1234P23188.server,S=53248:2,S

Sens : Vous pouvez voir quels fichiers sont chauds. Si de nombreux processus se disputent les fichiers d’index/log, vous avez peut-être une charge qui tourne constamment les flags ou force des réécritures d’index.

Décision : Si la contention est constante, évaluez les paramètres d’index, la latence du stockage et le comportement des clients (par ex. clients qui resynchronisent tout constamment).

Guide de diagnostic rapide

Ceci est l’ordre qui trouve rapidement les goulots sans se transformer en un projet d’archéologie d’une semaine.

Premier : prouver si c’est la latence stockage, le CPU, ou la concurrence Dovecot

  • IO wait et latence : iostat -x 1 3 et pidstat -d 1 5. Un iowait élevé ou un await élevé pointe vers le stockage ou des patterns de sync.
  • Saturation CPU : top ou pidstat -u 1 5. Si le CPU est saturé, vous ne choisissez pas entre Maildir et mdbox—vous choisissez entre scaler et réécrire.
  • Pression de connexions : doveadm service status. Si les connexions IMAP explosent et que les processus sont affamés, corrigez les limites de processus et le comportement client.

Deuxième : déterminer si le problème est « métadonnées du filesystem » ou « métadonnées Dovecot »

  • Suspects Maildir : utilisation des inodes (df -ih), nombres de fichiers énormes dans un dossier (find ... | wc -l), avertissements ext4 de dmesg.
  • Suspects mdbox : dovecot.map.index manquant/ invalide et churn des logs d’index, requêtes status lentes malgré des métriques de stockage raisonnables.

Troisième : valider si le problème est localisé ou systémique

  • Exécutez doveadm mailbox status sur un utilisateur « chaud » et un utilisateur « normal ».
  • Si seuls quelques utilisateurs sont lents, traitez-les comme des cas spéciaux (archivage, séparation de boîte, format différent, volume différent).
  • Si tout le monde est lent, suspectez le stockage, les indexeurs globaux, les sauvegardes ou un changement récent de montage/options/kernel/firmware.

Quatrième : choisir l’action corrective la moins risquée

  • Reconstruire les index (sûr et réversible).
  • Arrêter les E/S concurrentes (sauvegardes, scans antivirus, shipping de logs agressif).
  • Corriger les erreurs filesystem/matériel avant de tuner Dovecot.
  • Ce n’est qu’ensuite que vous envisagerez une migration entre formats.

Erreurs fréquentes : symptômes → cause → correction

1) « La connexion IMAP est lente, mais l’utilisation disque est faible »

Symptômes : Les utilisateurs signalent des délais à l’ouverture des dossiers ; la surveillance montre un %util disque faible.

Cause : Haute latence par opération (E/S de métadonnées, écritures sync, recherches de répertoire). Faible utilisation ne signifie pas faible latence.

Correction : Mesurez await avec iostat -x. Si la latence est élevée, réduisez les comportements lourds en sync, déplacez les sauvegardes hors du FS live, et envisagez mdbox si les inodes/overhead de répertoire dominent.

2) « Les sauvegardes sont cohérentes parce qu’on utilise rsync la nuit »

Symptômes : Les restaurations produisent des boîtes bizarres : messages récents manquants, doublons, ou tempêtes de resync côté client.

Cause : La copie fichier par fichier a capturé Maildir/mdbox en cours de mise à jour ; les index et les messages ne sont pas du même point dans le temps.

Correction : Sauvegardes snapshot-first. Restaurer depuis snapshot. Reconstruire les index après restauration via doveadm index ou en supprimant prudemment les fichiers d’index obsolètes.

3) « On mettra tout dans un INBOX massif »

Symptômes : Un ou deux utilisateurs sont toujours lents ; les fenêtres de sauvegarde explosent ; des avertissements filesystem apparaissent.

Cause : Les dossiers énormes amplifient le coût des listings et des métadonnées (Maildir) ou la surcharge d’indexation (tout format).

Correction : Appliquez des politiques d’archivage et de dossier. Envisagez des règles Sieve côté serveur. Scindez les boîtes chaudes sur des tiers de stockage.

4) « Nous avons migré les formats et n’avons pas planifié la transition des index »

Symptômes : Après migration, les clients voient des mails manquants jusqu’à resync ; la charge serveur monte en flèche.

Cause : Les index n’ont pas été reconstruits proprement ou ont été restaurés de façon incohérente ; les clients déclenchent un resync lourd.

Correction : Reconstruction d’index post-migration, reconnexion client par étapes et contrôle de la concurrence. Communiquez le comportement attendu de resync au support.

5) « Nous avons désactivé fsync pour la perf »

Symptômes : La performance s’est améliorée… jusqu’à un crash ou une coupure ; ensuite les utilisateurs perdent des mises à jour de flags, des livraisons récentes, ou voient des états corrompus.

Cause : Réglages de durabilité non sûrs. Le stockage mail est riche en métadonnées d’écriture ; perdre quelques secondes peut créer des incohérences confuses.

Correction : Conservez une durabilité raisonnable. Si vous voulez de la vitesse, achetez du meilleur stockage ou repensez l’architecture ; ne jouez pas à la roulette avec l’intégrité sauf si vous tolérez la perte.

6) « L’antivirus scanne tout le store mail chaque heure »

Symptômes : Pics périodiques de latence ; beaucoup de misses de cache ; pics IO ; plaintes utilisateurs par vagues.

Cause : Les nombreux fichiers de Maildir punissent les scans complets ; même mdbox souffre si les scans déstabilisent les caches.

Correction : Scannez à l’ingestion (pipeline LMTP/SMTP) ou utilisez des scans ciblés. Excluez les index et répertoires transitoires des scans larges quand c’est approprié.

7) « On a supposé que le filesystem n’avait pas d’importance »

Symptômes : La même configuration se comporte différemment selon les hôtes ; des mises à jour « changent la perf au hasard ».

Cause : L’indexation de répertoire, le comportement de l’allocateur et la journalisation diffèrent selon le FS et la version du kernel.

Correction : Standardisez le choix du filesystem et les options de montage pour les volumes mail. Faites des benchmarks d’opérations de boîte, pas seulement du débit séquentiel.

Trois mini-histoires d’entreprise (anonymisées, plausibles et instructives)

Mini-histoire 1 : La panne causée par une mauvaise hypothèse

Ils géraient une plateforme mail corporate de taille moyenne. Rien d’exotique : Dovecot, Postfix, un cluster de VM, et un volume de stockage « temporaire » devenu permanent. Un nouveau membre a demandé quel format de boîte ils utilisaient. La réponse fut confiante et fausse : « C’est Maildir, donc ce ne sont que des fichiers. Les sauvegardes sont faciles. »

Le job de sauvegarde était une copie par parcours de fichiers la nuit. Pas de snapshots. Le job tournait pendant des livraisons et parfois pendant les pics de sync mobile. Il « fonctionnait » dans le sens où il produisait un tas de fichiers. Il capturait aussi des états transitoires : messages dans tmp, renommages à moitié faits, logs d’index en cours de mise à jour.

Des mois plus tard, une défaillance de stockage a forcé une restauration. La restauration s’est terminée rapidement—la direction a applaudi. Puis la file d’attente du support s’est transformée en attaque par déni de service. Les utilisateurs voyaient des messages manquants, des fils dupliqués et des dossiers qui semblaient vides jusqu’à ce qu’ils cliquent et attendent. Certains clients « ont réparé » en retéléchargeant tout, ce qui a alourdi le serveur, qui a fait que les clients retentaient encore plus.

La cause racine n’était pas Maildir. C’était l’hypothèse que la sauvegarde au niveau fichier équivaut à une sauvegarde cohérente. Ils sont passés aux sauvegardes basées sur snapshot et ont ajouté un drill de restauration incluant la reconstruction d’index et la reconnexion client par étapes. La restauration suivante était ennuyeuse. Tout le monde l’a moins détestée. C’est comme ça qu’on sait que ça a marché.

Mini-histoire 2 : L’optimisation qui a mal tourné

Une autre entreprise avait une forte pression d’inodes. Ils sont passés à mdbox pour réduire le nombre de fichiers et diminuer le temps de scan des sauvegardes. Bonne idée. Puis ils ont décidé de pousser la perf en ajustant la durabilité : réduire les syncs et pousser le cache d’écriture. Les résultats des benchs s’amélioraient. Les graphiques étaient beaux.

Puis un événement d’alimentation est survenu dans un rack. Pas un désastre, juste quelques minutes de chaos. Les systèmes ont redémarré. La plupart des services se sont remis correctement. Le mail non. Les utilisateurs pouvaient se connecter, mais les nouveaux mails apparaissaient de façon incohérente, et les flags se comportaient comme des suggestions. Certains mails existaient dans les fichiers box mais n’apparaissaient pas en vue IMAP avant la réparation des index. Certaines réparations ont fonctionné ; d’autres ont nécessité la restauration des métadonnées depuis des snapshots.

La revue post-incident n’a pas été agréable. L’« optimisation » avait réduit le coût de chaque écriture en échange de propriétés de fiabilité qu’ils utilisaient implicitement. Avec un stockage agrégé et des structures de métadonnées, de petites incohérences peuvent cascader en états visibles par les utilisateurs, difficiles à expliquer et plus difficiles à supporter.

Ils ont annulé les réglages risqués, investi dans une protection d’alimentation correcte pour le stockage, et standardisé une approche snapshot+réplication testée. La performance était un peu moins bonne que dans le fantasme des benchs. La disponibilité était meilleure que la réalité de la panne. Choisissez la réalité.

Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Une entreprise régulée exploitait Dovecot à grande échelle. Leur stockage mail était suffisamment grand pour que « tout restaurer » ne soit pas un plan ; c’était une lettre de démission. Ils utilisaient Maildir pour la plupart des utilisateurs et mdbox pour un sous-ensemble de comptes à fort volume. La clé n’était pas les formats. C’était la discipline.

Ils pratiquaient des restaurations trimestrielles. Pas « on a testé les sauvegardes ». De vraies restaurations : choisir une boîte au hasard, la restaurer sur une machine isolée, reconstruire les index, valider l’accès IMAP et vérifier les comptes de messages et les livraisons récentes. Ils pratiquaient aussi une politique : snapshot toutes les quelques minutes, conserver une courte rétention localement, répliquer les snapshots hors site, et tester le chemin de restauration de bout en bout.

Quand un contrôleur de stockage a commencé à glitcher, ils l’ont vu tôt dans dmesg et SMART. Avant que cela ne devienne une perte de données, ils ont basculé le service mail vers la réplique et ont continué à servir les utilisateurs. Ensuite ils ont restauré quelques boîtes affectées depuis le dernier snapshot sain et les ont validées avec les outils Dovecot avant de les réintroduire.

Pas d’héroïsme. Pas de mystère. Juste le genre d’hygiène opérationnelle ennuyeuse qui semble chère jusqu’au moment où c’est la chose la moins coûteuse que vous ayez jamais faite.

Listes de contrôle / plan étape par étape

Choisir un format : checklist de décision

  1. Combien de messages par utilisateur ? Si beaucoup d’utilisateurs dépassent 200k messages dans un dossier, Maildir punira votre filesystem à moins que vous ne gériez le fractionnement des dossiers.
  2. Êtes-vous contraint par les inodes ? Si df -ih montre une tendance au-dessus de 70 % et en croissance, traitez-le comme un problème d’échelle, pas une simple étiquette d’avertissement.
  3. Avez-vous des sauvegardes basées sur snapshot ? Si non, corrigez cela avant de changer de format. Sinon vous ne ferez que modifier la forme de l’incohérence de sauvegarde.
  4. Avez-vous besoin d’une récupération partielle facile ? Maildir est généralement plus adapté pour des récupérations chirurgicales. mdbox peut convenir, mais vous aurez besoin d’outils et de processus rodés.
  5. Quel est votre système de fichiers ? Faites des benchmarks d’opérations de boîte sur le filesystem et le kernel réels que vous allez utiliser. Les charges mail sont riches en métadonnées et étranges.
  6. Avez-vous du temps de personnel pour la maintenance ? Si non, choisissez la voie avec les opérations day-2 les plus simples : Maildir plus un bon snapshoting.

Plan de migration : Maildir → mdbox (séquence sûre-ish)

  1. Inventoriez les utilisateurs et identifiez les boîtes chaudes (dossiers volumineux, churn élevé).
  2. Mettez en place des sauvegardes snapshot et effectuez un drill de restauration avant la migration.
  3. Installez un serveur de staging avec la version et la configuration Dovecot cible.
  4. Migrez d’abord un petit groupe pilote. Surveillez la latence IMAP, le temps de reconstruction d’index et les problèmes visibles par les utilisateurs.
  5. Planifiez les migrations hors-peak. Limitez la concurrence. Ne migrez pas tout le monde en même temps sauf si vous aimez vivre dangereusement.
  6. Après chaque lot : reconstruisez les index, validez les comptes de messages, et surveillez les tempêtes de resync côté client.
  7. Conservez la capacité de rollback via snapshots et un marqueur de cutover clair.

Checklist opérationnelle de base (quel que soit le format)

  • Sauvegardes basées sur snapshot avec restaurations testées.
  • Surveillance de l’utilisation des inodes, de la croissance des répertoires et du volume de mail.
  • Surveillance de la latence de stockage (await) et des erreurs kernel liées au stockage.
  • Procédures définies pour la reconstruction d’index et la réparation de boîtes.
  • Contrôle du comportement des clients quand c’est possible (les patterns de sync agressifs peuvent vous faire un DOS).

FAQ

1) Maildir est-il toujours plus sûr que mdbox ?

Non. Maildir a souvent un rayon d’impact plus petit pour une corruption d’un seul fichier, mais il peut échouer de façon spectaculaire via l’épuisement des inodes et l’overhead des métadonnées. « Plus sûr » dépend de ce que vous êtes susceptible de mal gérer.

2) mdbox est-il toujours plus rapide ?

Non. mdbox peut réduire le coût des petits fichiers, mais si votre goulot est le churn d’index, les réglages de sync ou la latence de stockage lente, il ne corrigera pas magiquement cela. Il peut aussi ajouter de la complexité pendant les réparations et restaurations.

3) Quelle est la principale raison pour laquelle les systèmes Maildir deviennent lents avec le temps ?

La croissance du nombre de fichiers plus les opérations sur les métadonnées. Le système ne ralentit pas linéairement ; il se dégrade quand les répertoires deviennent énormes, que les sauvegardes commencent à ramper et que l’utilisation des inodes approche du précipice.

4) Quelle est la principale raison pour laquelle les systèmes mdbox deviennent pénibles ?

La dépendance opérationnelle aux métadonnées gérées par Dovecot et le besoin de sauvegardes cohérentes. Si vous ne snapshottez pas, vous pouvez restaurer une boîte qui existe mais qui « n’a pas de sens » pour Dovecot jusqu’à réparation.

5) Dois-je stocker le mail sur NFS ?

Seulement si vous comprenez la sémantique de locking client/serveur NFS, les caractéristiques de latence et le comportement en cas de panne sous charge. Le stockage réseau est riche en métadonnées et sensible aux pics de latence. Beaucoup de problèmes « mystérieux » de mail ne sont que le stockage réseau qui fait son travail.

6) Puis-je mélanger les formats sur le même système ?

Oui, et parfois c’est la réponse pragmatique : gardez la plupart des utilisateurs sur Maildir et migrez les comptes à fort volume et à fort usage d’inodes vers mdbox. Assurez-vous simplement que vos outils opérationnels couvrent les deux.

7) Les snapshots remplacent-ils la réplication Dovecot ?

Non. Les snapshots vous aident à revenir en arrière et à restaurer. La réplication vous aide à rester disponible. Ils résolvent des problèmes différents et échouent différemment.

8) Comment savoir si c’est une corruption d’index ou une vraie perte de message ?

Comparez la réalité du filesystem à la vue IMAP. Si les messages existent sur disque (fichiers maildir ou storage mdbox) mais n’apparaissent pas, reconstruisez les index. S’ils n’existent pas sur disque, c’est une perte et vous devez restaurer.

9) Quelle est la maintenance minimale viable pour une couche de stockage Dovecot saine ?

Sauvegardes basées sur snapshot, drills de restauration périodiques, surveillance de l’utilisation des inodes et de la latence de stockage, et un runbook pour la reconstruction/réparation d’index. Le reste n’est que optimisation.

Prochaines étapes que vous pouvez faire cette semaine

  1. Exécutez les bases : doveconf -n, df -ih, iostat -x, et doveadm mailbox status sur un utilisateur chaud. Notez ce qui est réellement vrai.
  2. Vérifiez la cohérence des sauvegardes : Si vous ne faites pas de snapshots, considérez vos sauvegardes comme des « copies best-effort », pas des restaurations fiables pour votre poste.
  3. Faites un drill de restauration : Choisissez une boîte, restaurez-la dans un environnement isolé, reconstruisez les index, validez IMAP. Chronométrez et documentez.
  4. Identifiez la falaise de croissance : Inodes, dossiers énormes et latence stockage sont les trois gros facteurs. Mettez des alertes dessus.
  5. Décidez avec intention : Si votre douleur vient des inodes et de l’overhead des métadonnées, mdbox peut être la solution. Si votre douleur est la récupérabilité et la simplicité opérationnelle, restez sur Maildir et adaptez le filesystem et les sauvegardes.

Choisissez le format qui correspond à votre budget de défaillance et à votre réalité de restauration. Votre futur vous sera celui qui sera en astreinte. Ne lui faites pas une mauvaise blague.