Ubuntu 24.04 « Échec de démarrage … » : le flux de triage systemd le plus rapide (cas n°2)

Cet article vous a aidé ?

Vous redémarrez une machine de production, et systemd vous accueille avec la phrase la moins utile de l’histoire de Linux :
« Failed to start … ». Aucun contexte. Aucun indice sur la couche en faute. Juste le nom triste d’une unité et un horodatage déjà passé.

Ceci est le cas n°2 : pas une démonstration toy, pas une situation « réinstallez simplement ». C’est le workflow reproductible que j’utilise quand un service
échoue sur Ubuntu 24.04 et que j’ai besoin d’une réponse rapide : qu’est-ce qui a cassé, où cela a-t-il cassé, et quelle décision prendre ensuite.
La vitesse compte, mais la justesse compte davantage — car le deuxième redémarrage est quand la véritable panne commence.

Playbook de diagnostic rapide (5 premières minutes)

Quand une unité échoue, vous ne « déboguez pas un service ». Vous déboguez une transaction que systemd a tenté d’exécuter :
dépendances, ordonnancement, environnement, privilèges, système de fichiers, réseau, et le binaire lui‑même.
Le chemin le plus rapide est d’arrêter de deviner et de forcer systemd à vous indiquer quelle phase a échoué.

1) Confirmez l’unité exacte en échec et le mode d’échec

  • Obtenez le nom de l’unité, pas « le nom de l’application ».
  • Obtenez le résultat et l’étape (EXEC, SIGNAL, TIMEOUT, etc.).
  • Obtenez les derniers journaux pour cette unité seulement.

2) Identifiez la couche goulot d’étranglement

Le goulot est presque toujours l’un de ceux-ci :
ExecStart ne peut pas s’exécuter (binaire manquant, permissions, SELinux/AppArmor),
le processus démarre mais se termine (config, ports, secrets),
il ne devient jamais « prêt » (Type=notify, probe de readiness, PIDFile),
ou il attend des dépendances (network-online, mount, db, stockage distant).

3) Décidez si vous avez besoin d’un correctif sûr au redémarrage ou d’un contournement d’urgence

Il existe deux types de correctifs :
appropriés (override d’unité, correction de dépendance, correction de config),
et de triage (masking temporaire, dépendance réduite, démarrage manuel).
Si l’hôte est bloqué au démarrage, le contournement d’urgence est légitime — mais documentez‑le et retirez‑le ensuite.

4) En cas de doute, suivez la chaîne, pas le symptôme

Un service d’application en échec n’est souvent pas cassé. Il attend un montage, le DNS ou une route réseau qui n’arrive jamais.
Traitez‑le donc comme un système distribué : trouvez la première divergence significative dans la chaîne.

Une citation que j’utilise encore en postmortems, de W. Edwards Deming : « A bad system will beat a good person every time. »
Si vous regardez « Failed to start », assumez que le système (dépendances, ordonnancement, environnement) est coupable jusqu’à preuve du contraire.

Neuf faits qui changent votre façon de déboguer systemd

  1. systemd a remplacé Upstart sur Ubuntu il y a des années, mais le grand changement n’était pas un « nouvel init » — c’était l’ordonnancement par graphe de dépendances. Le débogage est maintenant de la traversée de graphe.
  2. journald n’est pas « juste des logs » ; il stocke des champs structurés (unit, PID, cgroup, chemin exécutable). Vous pouvez découper les journaux chirurgicalement sans le grep spaghetti.
  3. « Failed to start » est un résumé d’interface, pas une cause racine. La vraie cause se trouve typiquement dans un événement plus petit : « failed at step EXEC » ou « start request repeated too quickly ».
  4. Les unités ont deux relations orthogonales : l’ordre (After=) et le besoin (Requires=/Wants=). Beaucoup de pannes surviennent parce que les gens les confondent.
  5. Les targets sont des points de synchronisation, pas des « runlevels avec un nouveau nom ». Déboguer des problèmes de démarrage signifie souvent comprendre quelle target a tiré l’unité en faute.
  6. Type=notify est courant dans les démons modernes. Si le service n’envoie pas la readiness, systemd peut déclarer un timeout alors même que le processus est vivant.
  7. StartLimitBurst/Interval sont des disjoncteurs. Un service en clignotement peut « échouer » même si la dernière tentative aurait réussi.
  8. Les drop‑in overrides sont de première classe. Les fournisseurs livrent des unités ; les opérateurs remplacent. Éditer les fichiers dans /lib/systemd/system est la recette pour vous faire du tort plus tard.
  9. systemd-run et les unités transitoires existent. Quand vous devez reproduire des problèmes d’environnement, une unité transitoire peut répliquer les contraintes de cgroup et de sandboxing mieux que votre shell.

Blague n°1 : systemd ne vous déteste pas personnellement. Il déteste tout le monde de la même façon — puis l’écrit dans le journal avec une précision milliseconde.

Modèle mental : d’où vient vraiment « Failed to start »

Le rôle de systemd est de prendre une unité, calculer une transaction de dépendances, l’exécuter, et suivre l’état.
L’échec que vous voyez est la fin d’une chaîne de transitions d’état, et vous voulez la première divergence significative.

Ce que « démarrer » signifie (en termes systemd)

Le « démarrage » d’un service inclut :

  • Chargement de l’unité (parsing du fichier d’unité, drop‑ins, sortie des generators)
  • Résolution des dépendances (ce qui doit exister avant que cette unité puisse s’exécuter)
  • Exécution (ExecStartPre, ExecStart, éventuellement plusieurs processus)
  • Readiness (Type=simple est immédiat ; Type=notify requiert un signal ; Type=forking nécessite un PID)
  • Supervision (politiques Restart=, watchdogs, échecs)

Classes d’échecs communes (utilisez‑les comme seaux)

  • L’unité ne peut pas exécuter : binaire manquant, mauvaises permissions, mauvais utilisateur, interpréteur manquant, mauvais répertoire de travail.
  • Le processus se termine non‑zéro : erreurs de parsing de config, port déjà utilisé, dépendance inaccessible.
  • Délai d’attente : readiness non signalée, blocage sur le réseau, blocage sur un montage, disque lent, manque d’entropie (rare aujourd’hui).
  • Deadlock de dépendance : mauvais ordonnancement, After= incorrect, boucle de dépendance.
  • La politique bloque : dénis AppArmor, sandboxing systemd (ProtectSystem, PrivateTmp), restrictions de capacités.
  • Limitation de débit : StartLimitHit, boucles de redémarrage.

Votre travail en triage est de mapper « Failed to start » sur l’un de ces seaux en moins de cinq minutes.
Tout le reste est de l’ingénierie, pas du tirage au sort.

Tâches pratiques de triage (commandes + signification + décisions)

Ci‑dessous les tâches que j’exécute réellement. Chacune inclut : la commande, ce que signifie la sortie, et la décision à prendre.
Je suppose une unité en échec nommée acme-api.service. Remplacez par la vôtre.

Task 1: Confirmer l’état, le résultat et le dernier résumé d’erreur

cr0x@server:~$ systemctl status acme-api.service --no-pager
● acme-api.service - Acme API
     Loaded: loaded (/etc/systemd/system/acme-api.service; enabled; preset: enabled)
     Active: failed (Result: timeout) since Mon 2025-12-30 10:12:48 UTC; 32s ago
   Duration: 1min 30.012s
    Process: 1842 ExecStart=/usr/local/bin/acme-api --config /etc/acme/api.yaml (code=killed, signal=TERM)
   Main PID: 1842 (code=killed, signal=TERM)
        CPU: 1.012s

Dec 30 10:11:18 server systemd[1]: Starting acme-api.service - Acme API...
Dec 30 10:12:48 server systemd[1]: acme-api.service: start operation timed out. Terminating.
Dec 30 10:12:48 server systemd[1]: acme-api.service: Failed with result 'timeout'.
Dec 30 10:12:48 server systemd[1]: Failed to start acme-api.service - Acme API.

Signification : systemd a tué le processus après un timeout de démarrage. Ce n’est pas « il a planté ». Il peut fonctionner mais ne jamais être « prêt » aux yeux de systemd.

Décision : Ensuite : déterminer s’il s’agit d’un problème de readiness/Type, ou s’il est bloqué en attendant quelque chose (réseau, montage, DB).

Task 2: Récupérer les logs scoped à l’unité avec contexte (et arrêter de scroller)

cr0x@server:~$ journalctl -u acme-api.service -b --no-pager -n 200
Dec 30 10:11:18 server systemd[1]: Starting acme-api.service - Acme API...
Dec 30 10:11:19 server acme-api[1842]: loading config from /etc/acme/api.yaml
Dec 30 10:11:19 server acme-api[1842]: connecting to postgres at 10.20.0.15:5432
Dec 30 10:11:49 server acme-api[1842]: still waiting for postgres...
Dec 30 10:12:19 server acme-api[1842]: still waiting for postgres...
Dec 30 10:12:48 server systemd[1]: acme-api.service: start operation timed out. Terminating.
Dec 30 10:12:48 server systemd[1]: acme-api.service: Failed with result 'timeout'.

Signification : l’application est bloquée sur Postgres, et le timeout de systemd est le messager.

Décision : Enquêter sur l’atteignabilité réseau/DNS/routage, et vérifier la chaîne de dépendances du service (avons‑nous besoin de After=network-online.target ? le voulons‑nous ?).

Task 3: Lire le fichier d’unité que systemd utilise réellement

cr0x@server:~$ systemctl cat acme-api.service
# /etc/systemd/system/acme-api.service
[Unit]
Description=Acme API
After=network.target
Wants=network.target

[Service]
Type=notify
ExecStart=/usr/local/bin/acme-api --config /etc/acme/api.yaml
User=acme
Group=acme
Restart=on-failure
TimeoutStartSec=90

[Install]
WantedBy=multi-user.target

Signification : c’est Type=notify, donc systemd attend une readiness. Si le démon ne la signale pas, systemd expirera même s’il est « ok ».
Aussi, il ne attend que network.target, ce qui n’est pas « le réseau est opérationnel ».

Décision : Confirmer si le binaire supporte réellement sd_notify. Sinon, passer à Type=simple ou corriger le démon. Évaluer aussi si network-online.target est approprié (souvent non).

Task 4: Vérifier si systemd attend des dépendances au lieu de votre service

cr0x@server:~$ systemctl list-dependencies --reverse acme-api.service
acme-api.service
● multi-user.target
● graphical.target

Signification : rien de surprenant ne le tire. Mais ceci n’affiche pas les délais d’ordonnancement ; ça montre qui le veut.

Décision : Utiliser critical chain pour voir ce qui a bloqué le boot, et inspecter network-online/mount units si présentes.

Task 5: Utiliser critical chain pour trouver la première unité lente ou cassée

cr0x@server:~$ systemd-analyze critical-chain acme-api.service
acme-api.service +1min 30.012s
└─network.target @8.412s
  └─systemd-networkd.service @6.901s +1.201s
    └─systemd-udevd.service @3.112s +3.654s
      └─systemd-tmpfiles-setup-dev-early.service @2.811s +201ms
        └─kmod-static-nodes.service @2.603s +155ms

Signification : la chaîne indique que votre service a passé 90 secondes à « démarrer », pas que le réseau était lent.
Cela pointe vers un attente de readiness, pas un ordonnancement.

Décision : Confirmer si systemd a jamais reçu READY=1, ou si l’app est juste bloquée en attendant Postgres.

Task 6: Confirmer si le processus était vivant durant le « timeout »

cr0x@server:~$ systemctl show acme-api.service -p MainPID -p ExecMainStatus -p ExecMainCode -p TimeoutStartUSec -p Type
MainPID=0
ExecMainStatus=0
ExecMainCode=0
TimeoutStartUSec=1min 30s
Type=notify

Signification : MainPID vaut 0 maintenant parce qu’il est mort (systemd l’a tué). La ligne clé est Type=notify et un timeout fini.

Décision : Soit le démon n’a jamais notifié la readiness, soit il n’a pas atteint la readiness car il attendait Postgres. Résoudre la dépendance ou changer le comportement de démarrage.

Task 7: Vérifier StartLimitHit (l’échec « il a échoué parce qu’il a échoué »)

cr0x@server:~$ systemctl status acme-api.service --no-pager | sed -n '1,18p'
● acme-api.service - Acme API
     Loaded: loaded (/etc/systemd/system/acme-api.service; enabled; preset: enabled)
     Active: failed (Result: start-limit-hit) since Mon 2025-12-30 10:13:30 UTC; 5s ago
    Process: 1912 ExecStart=/usr/local/bin/acme-api --config /etc/acme/api.yaml (code=exited, status=1/FAILURE)

Signification : systemd a arrêté d’essayer parce qu’il a redémarré trop souvent.

Décision : Réinitialisez le compteur d’échecs seulement après avoir changé quelque chose de significatif ; sinon vous n’accélérez qu’une boucle de crash.

Task 8: Réinitialiser une start-limit et réessayer intentionnellement

cr0x@server:~$ sudo systemctl reset-failed acme-api.service
cr0x@server:~$ sudo systemctl start acme-api.service
cr0x@server:~$ systemctl status acme-api.service --no-pager -n 20
● acme-api.service - Acme API
     Loaded: loaded (/etc/systemd/system/acme-api.service; enabled; preset: enabled)
     Active: activating (start) since Mon 2025-12-30 10:14:01 UTC; 3s ago

Signification : il est à nouveau en activating — maintenant vous surveillez les logs.

Décision : Si ça se répète, arrêtez et corrigez la cause racine (config, dépendance, readiness), ne continuez pas à marteler start.

Task 9: Valider le fichier d’unité pour des pièges évidents

cr0x@server:~$ systemd-analyze verify /etc/systemd/system/acme-api.service
/etc/systemd/system/acme-api.service:6: Unknown lvalue 'Wants' in section 'Unit'

Signification : Dans la vraie vie, les fautes de frappe arrivent. Ici il a signalé une clé invalide (exemple). Systemd peut ignorer ce que vous pensiez critique.

Décision : Corriger la syntaxe de l’unité ; recharger le daemon ; réessayer. Si vous avez de la chance, vous venez de trouver la panne.

Task 10: Vérifier si une dépendance échoue (les montages et le stockage sont souvent en cause)

cr0x@server:~$ systemctl --failed --no-pager
  UNIT                          LOAD   ACTIVE SUB    DESCRIPTION
● mnt-data.mount                loaded failed failed /mnt/data
● acme-api.service              loaded failed failed Acme API

LOAD   = Reflects whether the unit definition was properly loaded.
ACTIVE = The high-level unit activation state.
SUB    = The low-level unit activation state.

Signification : Si le stockage n’a pas monté, votre app peut être dommage collatéral.

Décision : Corriger d’abord le montage. Démarrer l’app avant que son chemin de données n’existe est la façon d’obtenir une perte de données déguisée en « disponibilité ».

Task 11: Inspecter rapidement une unité de montage en échec

cr0x@server:~$ systemctl status mnt-data.mount --no-pager -n 80
● mnt-data.mount - /mnt/data
     Loaded: loaded (/proc/self/mountinfo; generated)
     Active: failed (Result: exit-code) since Mon 2025-12-30 10:10:02 UTC; 4min ago
      Where: /mnt/data
       What: UUID=aa1b2c3d-4e5f-6789-a012-b345c678d901
    Process: 1222 ExecMount=/usr/bin/mount UUID=aa1b2c3d-4e5f-6789-a012-b345c678d901 /mnt/data (code=exited, status=32)
     Status: "Mounting failed."

Dec 30 10:10:02 server mount[1222]: mount: /mnt/data: wrong fs type, bad option, bad superblock on /dev/sdb1, missing codepage or helper program, or other error.
Dec 30 10:10:02 server systemd[1]: mnt-data.mount: Mount process exited, code=exited, status=32/n/a

Signification : échec classique de montage. Peut être UUID erroné, corruption FS, module noyau manquant, ou disque changé.

Décision : Confirmer le device bloc, exécuter blkid, vérifier dmesg, puis seulement toucher aux outils de réparation de FS.

Task 12: Confirmer si AppArmor a bloqué le service (Ubuntu adore AppArmor)

cr0x@server:~$ journalctl -k -b --no-pager | grep -i apparmor | tail -n 5
Dec 30 10:11:19 server kernel: audit: type=1400 audit(1735553479.112:88): apparmor="DENIED" operation="open" class="file" profile="/usr/local/bin/acme-api" name="/etc/acme/secret.key" pid=1842 comm="acme-api" requested_mask="r" denied_mask="r" fsuid=1001 ouid=0

Signification : Votre service peut être correct ; la politique ne l’est pas.

Décision : Mettre à jour le profil AppArmor (ou cesser de confiner ce binaire si vous ne pouvez pas le maintenir). Ne faites pas chmod 777 sur des secrets « juste pour voir ».

Task 13: Reproduire l’exécution sous des contraintes similaires à systemd (unité transitoire)

cr0x@server:~$ sudo systemd-run --unit=acme-api-debug --property=User=acme --property=Group=acme /usr/local/bin/acme-api --config /etc/acme/api.yaml
Running as unit: acme-api-debug.service
cr0x@server:~$ systemctl status acme-api-debug.service --no-pager -n 30
● acme-api-debug.service - /usr/local/bin/acme-api --config /etc/acme/api.yaml
     Loaded: loaded (/run/systemd/transient/acme-api-debug.service; transient)
     Active: failed (Result: exit-code) since Mon 2025-12-30 10:15:22 UTC; 2s ago
    Process: 2044 ExecStart=/usr/local/bin/acme-api --config /etc/acme/api.yaml (code=exited, status=1/FAILURE)

Signification : Cela isole « marche dans mon shell » de « marche sous systemd user/cgroup ».

Décision : Lire les logs de l’unité transitoire. Si elle échoue de la même façon, c’est probablement la config/dépendance, pas le câblage de l’unité.

Task 14: Afficher l’interprétation exacte du statut de sortie

cr0x@server:~$ systemctl show acme-api.service -p ExecMainStatus -p ExecMainCode -p Result
ExecMainStatus=1
ExecMainCode=exited
Result=exit-code

Signification : échec par code de sortie, pas timeout, pas signal, pas watchdog.

Décision : Se concentrer sur stderr de l’application et la configuration. Ne perdez pas de temps sur les dépendances sauf si les logs pointent vers elles.

Task 15: Vérifier si des sockets/ports étaient le véritable problème

cr0x@server:~$ ss -ltnp | grep -E ':8080\b'
LISTEN 0      4096         0.0.0.0:8080      0.0.0.0:*    users:(("nginx",pid=912,fd=12))

Signification : Quelque chose d’autre occupe le port. Beaucoup de services loggent cela, mais parfois ils ne le font pas avant d’exit.

Décision : Résoudre le conflit de port (changer la config, arrêter l’autre service, ou utiliser correctement l’activation par socket).

Task 16: Quand le boot est impliqué, vérifier aussi le boot précédent

cr0x@server:~$ journalctl -u acme-api.service -b -1 --no-pager -n 80
Dec 30 09:03:12 server systemd[1]: Starting acme-api.service - Acme API...
Dec 30 09:03:13 server acme-api[701]: connecting to postgres at 10.20.0.15:5432
Dec 30 09:03:14 server acme-api[701]: ready
Dec 30 09:03:14 server systemd[1]: Started acme-api.service - Acme API.

Signification : Hier, ça fonctionnait. C’est cadeau : quelque chose a changé (réseau, secrets, montages, politique, dépendance distante).

Décision : Chercher les changements entre les boots : mises à jour de paquets, déploiement de config, DNS, pare‑feu, routes, changements de stockage.

Cas n°2 : chaîne de dépendances + délai d’attente + une vérification « verte » trompeuse

Voici la situation qui apparaît dans de vraies flottes : un service ne démarre pas après un redémarrage routinier, et l’erreur paraît locale.
Ce n’est pas le cas. C’est un problème de dépendance/ordonnancement déguisé en timeout.

La configuration

Vous avez un service API qui lit la configuration, puis se connecte à Postgres. L’unité est en Type=notify.
Elle est configurée avec After=network.target et Wants=network.target.
Sur Ubuntu 24.04, le réseau est géré par systemd-networkd ou NetworkManager selon votre build.

L’échec commence ainsi :

cr0x@server:~$ systemctl status acme-api.service --no-pager -n 30
● acme-api.service - Acme API
     Loaded: loaded (/etc/systemd/system/acme-api.service; enabled; preset: enabled)
     Active: failed (Result: timeout) since Mon 2025-12-30 10:12:48 UTC; 7min ago
Dec 30 10:12:48 server systemd[1]: acme-api.service: start operation timed out. Terminating.
Dec 30 10:12:48 server systemd[1]: Failed to start acme-api.service - Acme API.

Les gens sautent souvent directement à « augmentez TimeoutStartSec ». Parfois c’est correct. Souvent c’est paresseux.
Première question : pourquoi ça a pris trop de temps ?

Étape 1 : le service n’est pas « down », il n’est pas « ready »

Le journal montre qu’il attend Postgres. C’est le premier indice : le processus a démarré et travaille.
Le second indice est Type=notify. Cela signifie que systemd attend la readiness, et le timeout est la limite temporelle.

La vérification la plus simple : pouvons‑nous atteindre Postgres ?

cr0x@server:~$ ping -c 2 10.20.0.15
PING 10.20.0.15 (10.20.0.15) 56(84) bytes of data.

--- 10.20.0.15 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1003ms

Signification : Cet hôte ne peut pas atteindre l’IP de la base. Ce n’est pas un bug applicatif.

Décision : Déboguer le routage, les VLANs, le pare‑feu, ou l’état du service réseau. Ne touchez pas à l’app pour l’instant.

Étape 2 : le mensonge « le réseau est up » (network.target)

network.target signifie surtout « la pile de gestion réseau est en cours » et non « vous avez des routes et de la connectivité ».
Si vous voulez « IP configurée », vous vous préoccupez généralement de network-online.target — mais traitez‑le comme du piment :
un peu aide ; trop gâche le plat.

Vérifiez ce qui fournit la readiness en ligne :

cr0x@server:~$ systemctl status systemd-networkd-wait-online.service --no-pager -n 40
● systemd-networkd-wait-online.service - Wait for Network to be Configured
     Loaded: loaded (/usr/lib/systemd/system/systemd-networkd-wait-online.service; enabled; preset: enabled)
     Active: failed (Result: timeout) since Mon 2025-12-30 10:10:55 UTC; 9min ago
    Process: 876 ExecStart=/usr/lib/systemd/systemd-networkd-wait-online (code=exited, status=1/FAILURE)
Dec 30 10:09:25 server systemd[1]: Starting systemd-networkd-wait-online.service - Wait for Network to be Configured...
Dec 30 10:10:55 server systemd[1]: systemd-networkd-wait-online.service: start operation timed out. Terminating.
Dec 30 10:10:55 server systemd[1]: systemd-networkd-wait-online.service: Failed with result 'timeout'.

Signification : le réseau n’est pas devenu « online » dans le délai imparti. Votre API n’a jamais eu la chance si elle a besoin de la BD.

Décision : Ne « fixez » pas l’API. Réparez pourquoi l’hôte n’a jamais atteint network-online. Habituellement : netplan erroné, VLAN manquant, lien down, échec DHCP, ou interface renommée.

Étape 3 : trouver la véritable panne réseau (netplan et état de l’interface)

cr0x@server:~$ ip -br link
lo               UNKNOWN        00:00:00:00:00:00
enp0s31f6        DOWN           3c:52:82:aa:bb:cc

Signification : l’interface est down. C’est le lien physique, le driver, ou un problème de switch.

Décision : Si c’est une VM, vérifier la vNIC de l’hyperviseur. Si physique, vérifier câblage/port de switch, et dmesg pour des problèmes de pilote.

cr0x@server:~$ journalctl -u systemd-networkd -b --no-pager -n 120
Dec 30 10:09:03 server systemd-networkd[612]: enp0s31f6: Link DOWN
Dec 30 10:09:04 server systemd-networkd[612]: enp0s31f6: Lost carrier
Dec 30 10:09:05 server systemd-networkd[612]: enp0s31f6: DHCPv4 client: No carrier

Signification : pas de carrier ; DHCP n’a jamais tourné. Le message « Failed to start » est innocent — votre NIC ne l’est pas.

Décision : Restaurer le lien. Si le lien est intentionnellement absent (réseau isolé), alors le design du service est erroné : il ne devrait pas bloquer le boot.

Étape 4 : la vérification « verte » trompeuse

Dans de nombreuses organisations, quelqu’un exécutera une vérification qui dit « réseau OK » parce qu’il peut résoudre localhost ou atteindre une passerelle locale.
Ce n’est pas la connectivité vers votre dépendance. C’est une vérité partielle.

Voici la vérification qui semble verte mais ne l’est pas :

cr0x@server:~$ getent hosts postgres.internal
10.20.0.15      postgres.internal

Signification : la résolution DNS (ou /etc/hosts) fonctionne. Cela ne dit rien sur le routage, le pare‑feu, ou le lien.

Décision : Associez toujours les vérifications de nom à des vérifications d’atteignabilité : ip route get, ping, nc, ss.

cr0x@server:~$ ip route get 10.20.0.15
RTNETLINK answers: Network is unreachable

Signification : le noyau n’a pas de route. C’est plus bas niveau que l’app et plus fiable que « ça devrait marcher ».

Décision : Corriger netplan/routes. Ne touchez pas à TimeoutStartSec tant que l’hôte ne peut pas router vers ses dépendances.

Étape 5 : la correction réelle (et le non‑correctif)

Le non‑correctif : augmenter TimeoutStartSec à cinq minutes. Cela ne fait que donner plus de temps à votre panne pour rester mystérieuse.

La correction est de restaurer le réseau, puis de resserrer le comportement du service :
si Postgres est down, l’hôte doit‑il booter ? Généralement oui. Le service doit‑il continuer à réessayer ? Généralement oui.
Doit‑il bloquer pendant 90 secondes puis mourir ? Cela dépend si vous voulez l’unité « active » sans BD.

En pratique, vous choisissez une des options :

  • Rendre résilient : le laisser démarrer et fournir une fonctionnalité partielle, ou continuer à réessayer sans échouer la readiness.
  • Rendre explicite : échouer rapidement, mais ajouter des logs clairs, et rendre l’orchestration consciente de cet état.

Si le démon ne supporte pas sd_notify, basculer vers Type=simple évite souvent des timeouts faux :

cr0x@server:~$ sudo systemctl edit acme-api.service
# (editor opens)
cr0x@server:~$ cat /etc/systemd/system/acme-api.service.d/override.conf
[Service]
Type=simple
TimeoutStartSec=30
cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl restart acme-api.service

Signification : Vous alignez les attentes de readiness de systemd avec la réalité. De plus, vous raccourcissez la fenêtre de « blocage mystérieux ».

Décision : Ne faites cela que si le service ne notifie vraiment pas. S’il notifie, gardez Type=notify et corrigez le chemin de readiness.

Blague n°2 : Augmenter TimeoutStartSec, c’est comme éloigner l’alarme incendie. La cuisine brûle toujours ; vous l’entendez juste moins bien.

Trois mini-histoires d’entreprise (et la leçon utile)

Mini‑histoire n°1 : La panne causée par une mauvaise hypothèse

Une PME SaaS avait un service worker en arrière-plan qui traitait des événements de facturation. Il était « simple » :
lire une file, écrire dans Postgres, émettre des métriques. Il tournait depuis des mois sans drame.

Pendant une campagne de durcissement de sécurité, quelqu’un a mis à jour le fichier d’unité :
After=network.target et Wants=network.target, parce que ça semblait « correct » et que d’autres unités faisaient pareil.
L’hypothèse était que network.target voulait dire « le réseau est opérationnel ».

Une semaine plus tard, l’entreprise a migré un VLAN et introduit une attente DHCP plus longue sur un sous‑ensemble de nœuds.
Ces nœuds ont booté, ont démarré le worker immédiatement, et le worker a essayé de se connecter avant que la route existe.
Le worker avait un timeout de démarrage de 60 secondes et est sorti avec un code non‑zéro. systemd l’a redémarré agressivement. StartLimitHit s’est déclenché.

La partie laide : les tableaux de bord montraient le worker « en cours » sur la plupart des nœuds. Sur les nœuds impactés, il était « failed », mais personne n’avait d’alertes sur l’état de l’unité.
La facturation a pris du retard. La finance l’a remarqué avant l’ingénierie, ce qui est une sorte de honte particulière.

Ils l’ont corrigé en rendant la connectivité explicite : soit attendre une dépendance spécifique (un endpoint BD atteignable via un script),
soit faire démarrer le worker et le laisser réessayer sans échouer la readiness de systemd. Ils ont aussi arrêté d’utiliser network.target comme doudoune.

Leçon : N’encodez pas le folklore dans les fichiers d’unité. Si vous dépendez de la connectivité, définissez ce que « prêt » signifie et mesurez‑le.

Mini‑histoire n°2 : L’optimisation qui s’est retournée

Une équipe IT d’entreprise voulait des temps de boot plus rapides sur une flotte de serveurs Ubuntu. Quelqu’un a remarqué quelques secondes passées dans « wait online »
et a décidé que c’était inutile. Ils ont désactivé systemd-networkd-wait-online.service pour gagner du temps de démarrage.

Ça a marché — le démarrage était plus rapide. Puis un service dépendant du stockage a commencé à échouer après des reboots. Pas toujours. Juste assez pour coûter cher.
Le service montait un LUN iSCSI, puis démarravait une base de données dessus. Avec wait-online désactivé, la connexion iSCSI entrait en course avec la configuration réseau.
Parfois elle gagnait. Parfois elle plantait.

La première réponse de l’équipe a été d’augmenter les timeouts de démarrage de la base. Cela a fait « démarrer » la base plus souvent,
mais a créé un pire mode d’échec : la base démarrait sur un répertoire vide si le montage n’était pas présent, initialisait un nouveau cluster,
et servait joyeusement des données absurdes jusqu’à ce que quelqu’un le remarque.

La correction finale n’a pas été « réactiver wait-online globalement ». Ils ont rendu l’ordonnancement et les exigences précis :
l’unité de base requérait l’unité de montage ; l’unité de montage requérait une route réseau ; et ils ont utilisé des vérifications de dépendance par unité.
Le temps de boot est resté rapide sur les machines qui n’avaient pas besoin du réseau. Les machines qui en avaient besoin sont devenues correctes.

Leçon : Les optimisations globales de boot doivent être traitées comme une modification d’une bibliothèque partagée. Si vous ne pouvez pas décrire le graphe de dépendances, ne « l’accélérez » pas.

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

Une équipe plateforme gérait un ensemble de services internes avec des fichiers d’unité fournis par des vendeurs. Ils n’ont jamais édité les fichiers dans /usr/lib/systemd/system.
Jamais. À la place, chaque changement allait dans des drop‑ins dans /etc/systemd/system/*.d/.

Cela paraissait bureaucratique à certains ingénieurs — jusqu’à ce qu’une mise à jour de paquet arrive un vendredi soir.
La mise à jour a remplacé l’unité du fournisseur et changé un flag ExecStart. Sur les hôtes où des gens avaient édité directement les unités fournisseur,
leurs changements ont été silencieusement écrasés et les services ont planté.

Sur les hôtes gérés de la manière ennuyeuse, les drop‑ins s’appliquaient proprement. Après la mise à jour, les services redémarraient avec les nouveaux défauts du fournisseur
plus les overrides de l’équipe. Le seul travail requis a été de valider le comportement — pas de récupérer une dérive de config.

La revue post‑incident a été calme. L’équipe n’a pas gagné de points d’héroïsme. Ils ont évité l’héroïsme. C’est tout le travail.

Leçon : Les pratiques ennuyeuses (drop‑ins, verify, reload, logs cohérents) sont celles qui survivent aux mises à jour.

Erreurs courantes : symptômes → cause racine → correction

1) Symptom: « Failed with result ‘timeout’ » pendant le démarrage

Cause racine : inadéquation de readiness (Type=notify sans sd_notify), ou le service bloque sur une dépendance (DB, DNS, montage), ou TimeoutStart trop court pour un vrai travail.

Correction : Déterminez si le processus effectue un travail utile. Si pas de support sd_notify, passez à Type=simple.
Si il attend une dépendance, corrigez la dépendance ou changez le comportement du service pour réessayer après le démarrage plutôt que de bloquer la readiness.

2) Symptom: « failed at step EXEC »

Cause racine : chemin ExecStart incorrect, binaire manquant, exécutable sans bit d’exécution, permissions utilisateur erronées, interpréteur manquant (par ex. script avec shebang incorrect), ou système de fichiers non monté.

Correction : Vérifier systemctl status et le fichier d’unité ; valider que le fichier existe et est exécutable ; vérifier les unités de montage ; inspecter les dénis AppArmor.

3) Symptom: « start request repeated too quickly » / start-limit-hit

Cause racine : boucle de crash ou échec rapide, exacerbée par Restart=always/on-failure. Le limiteur de débit de systemd se déclenche.

Correction : Lire les logs pour le premier échec, pas le dernier. Corriger la config/port/secrets sous‑jacente. Puis systemctl reset-failed.
Optionnellement ajuster StartLimit, mais ne les utilisez pas pour cacher une boucle.

4) Symptom: le service fonctionne manuellement, échoue sous systemd

Cause racine : environnement différent (PATH, répertoire de travail, ulimits), utilisateur différent, flags de sandboxing, permissions manquantes.

Correction : Utiliser systemd-run avec User/Group pour reproduire ; vérifier WorkingDirectory, EnvironmentFile, et les flags de durcissement de l’unité.

5) Symptom: le service échoue seulement après un reboot, pas après un redémarrage manuel

Cause racine : problème d’ordre au boot : montage pas prêt, réseau non configuré, secrets non disponibles, temps non synchronisé, service dépendant non démarré encore.

Correction : Utiliser systemd-analyze critical-chain et inspecter les dépendances. Remplacer After=network.target vague par un ordonnancement correct et des exigences explicites (unité de montage, unité de dépendance spécifique, ou une vérification légère préalable).

6) Symptom: le service « démarre » mais sort immédiatement et systemd rapporte un succès

Cause racine : mauvais Type de service (par ex. forking vs simple), ou ExecStart lance un wrapper qui se termine tandis que le démon continue (ou ne continue jamais).

Correction : Vérifier le modèle du démon. Utiliser Type=forking avec PIDFile uniquement quand le démon fork réellement. Préférer Type=simple ou notify pour les démons modernes.

7) Symptom: tout « a l’air ok », mais le service ne peut pas accéder à un fichier ou socket

Cause racine : déni AppArmor, sandboxing systemd (ProtectSystem, ReadOnlyPaths, PrivateTmp), ou mismatch d’ownership après déploiement.

Correction : Cherchez les dénis dans le journal noyau ; ajustez la politique ou le sandboxing ; assurez les ownerships et permissions corrects.

Checklists / plan étape par étape

Checklist A : Le triage en 90 secondes (service unique en échec)

  1. Exécuter systemctl status UNIT --no-pager. Capturer le Result (timeout/exit-code/start-limit-hit) et toute « step » (EXEC).
  2. Exécuter journalctl -u UNIT -b -n 200 --no-pager. Chercher la dernière ligne d’application significative.
  3. Exécuter systemctl cat UNIT. Noter Type=, TimeoutStartSec=, User=, et le chemin ExecStart.
  4. Classer : problème EXEC, exit-code, timeout/attente, dépendance, politique, ou limitation de débit.
  5. Choisir l’outil suivant en fonction du seau (ne pas improviser).

Checklist B : Workflow « le boot est bloqué »

  1. systemctl --failed --no-pager pour lister les premiers échecs évidents (montages, réseau, résolution de noms).
  2. systemd-analyze critical-chain pour identifier ce qui a retardé l’atteinte de la target par défaut.
  3. Corriger d’abord la dépendance cassée la plus tôt (montage/réseau) avant de redémarrer les services en aval.
  4. Si vous avez besoin de l’hôte tout de suite : maskez temporairement l’unité non critique en échec, bootez en multi-user, et revenez sur le mask après déploiement du correctif.

Checklist C : Discipline de réparation sûre au reboot (éviter l’auto‑sabotage)

  1. Ne jamais éditer les fichiers fournisseur dans /usr/lib/systemd/system ou /lib/systemd/system. Utiliser systemctl edit pour des drop‑ins.
  2. Après modifications d’unité : systemctl daemon-reload, puis redémarrer l’unité.
  3. Utiliser systemd-analyze verify sur les unités modifiées.
  4. Consigner ce que vous avez changé et pourquoi. Future‑vous est aussi un SRE, et il est fatigué.

Checklist D : Démarrages de services conscients du stockage (les pannes de stockage se déguisent en pannes de service)

  1. Si un service utilise un chemin comme /mnt/data, assurez‑vous qu’il existe une unité de montage et que le service la requiert.
  2. Vérifier les montages en échec tôt avec systemctl --failed.
  3. Ne laissez jamais une base de données s’initialiser sur un répertoire non monté. Ajouter des vérifications explicites ou des exigences.
  4. Si le montage est distant (NFS/iSCSI), traitez‑le comme une dépendance réseau et concevez pour la défaillance partielle.

FAQ

1) Pourquoi systemd dit « Failed to start » alors que le binaire a clairement tourné ?

Parce que « start » inclut la readiness. Si Type=notify est utilisé, systemd attend un signal de readiness.
Si l’app se bloque sur une dépendance, systemd peut expirer et la tuer même si elle « faisait quelque chose ».

2) Dois‑je toujours ajouter After=network-online.target ?

Non. Beaucoup de services n’ont pas besoin d’une connectivité complète au démarrage, et attendre « online » peut ralentir le boot ou introduire de nouveaux modes de panne.
Ajoutez‑le seulement quand le service a vraiment besoin de la connectivité réseau pour être fonctionnel, et préférez des dépendances plus spécifiques quand possible.

3) Quelle est la différence entre After= et Requires= ?

After= est uniquement d’ordre : « démarre après ». Requires= est une exigence : « si ça échoue, j’échoue aussi ».
Les confondre crée soit des deadlocks au boot (trop d’exigences), soit des conditions de course (ordonnancement sans garantir l’existence de la dépendance).

4) Que signifie habituellement « failed at step EXEC » ?

systemd n’a pas pu exécuter la commande configurée. Causes communes : chemin erroné, binaire manquant, pas de bit exécutable, permissions utilisateur,
interpréteur manquant pour les scripts, ou un chemin de système de fichiers requis non monté.

5) Comment savoir si AppArmor bloque mon service ?

Cherchez les dénis dans le journal noyau : journalctl -k -b | grep -i apparmor.
Si vous voyez des entrées DENIED correspondant au binaire de votre service, vous avez un problème de politique, pas d’application.

6) Quand devrais‑je augmenter TimeoutStartSec ?

Seulement quand le service a légitimement besoin de plus de temps pour devenir prêt et que vous comprenez pourquoi.
S’il attend une dépendance instable, un timeout plus long peut rendre la récupération plus lente et les pannes plus difficiles à détecter.

7) Pourquoi une boucle de redémarrage devient‑t‑elle start-limit-hit ?

systemd limite les redémarrages pour éviter le thrashing. Si une unité échoue à répétition en peu de temps, il arrête d’essayer.
Corrigez le problème sous‑jacent, puis effacez le compteur avec systemctl reset-failed.

8) Le service fonctionne quand je l’exécute manuellement. Pourquoi pas via systemd ?

systemd l’exécute en tant qu’utilisateur configuré avec un environnement spécifique, répertoire de travail, ulimits, contraintes de cgroup, et éventuellement du sandboxing.
Reproduisez avec systemd-run en utilisant le même User/Group pour réduire l’écart.

9) Comment savoir si le problème est en amont (dépendance) plutôt que le service ?

Si les logs de l’unité montrent « waiting for … » (DB, DNS, montage), ou si vous voyez des unités mount/network en échec dans systemctl --failed,
traitez‑le comme une panne en amont jusqu’à preuve du contraire. Corrigez la première panne dans la chaîne.

Prochaines étapes à réaliser dès aujourd’hui

Si vous voulez que les incidents « Failed to start » arrêtent de vous bouffer les après‑midi, faites ces actions dans l’ordre :

  1. Standardiser les commandes de triage au sein de votre équipe : systemctl status, journalctl -u, systemctl cat, systemd-analyze critical-chain, systemctl --failed.
  2. Rendre la readiness des unités honnête : n’utilisez pas Type=notify sauf si le démon notifie vraiment ; ne cachez pas des attentes de dépendance derrière de longs timeouts.
  3. Raccorder les dépendances explicitement : si vous avez besoin d’un montage, requérez le montage. Si vous avez besoin d’une route, prouvez‑la (ou concevez pour réessayer sans bloquer le boot).
  4. Utiliser des drop‑in overrides pour chaque changement opérationnel. Gardez les unités fournisseur intactes pour que les mises à jour ne vous surprennent pas.
  5. Ajouter des alertes sur l’échec d’unité pour les services importants. Le journal est excellent, mais il ne vous appelle pas.

Le flux de triage le plus rapide n’est pas un sac de commandes. C’est une habitude : identifier la classe d’échec, lire l’unité, lire les logs,
suivre la chaîne de dépendances, et réparer la première chose cassée. Tout le reste, c’est du théâtre.

← Précédent
Bridge et VLAN sur Ubuntu 24.04 pour la virtualisation : réparer « la VM n’a pas d’internet » correctement
Suivant →
Mars Climate Orbiter : le décalage d’unités qui a fait perdre un vaisseau spatial

Laisser un commentaire