Pourquoi systemd ne redémarre pas votre service
Restart= surveille uniquement le processus principal : un enfant mort dans le même cgroup reste invisible. Comprenez Type=, les limites et le journal.
La réponse courte : les politiques de redémarrage de systemd surveillent un seul processus
Les politiques de redémarrage de systemd surveillent un seul processus par unité : le processus principal. Restart= lit le code de sortie de ce processus, et de lui seul. Le cgroup d’une unité peut contenir vingt processus. L’un d’eux peut s’arrêter, tandis que l’unité reste active (running) parce que le processus principal est toujours présent. Pour systemd, rien n’a échoué. Aucun redémarrage n’est donc déclenché.
systemd connaît bien les autres processus. Il les tue lorsque l’unité s’arrête, comptabilise leur mémoire dans les limites de l’unité, leur applique le quota CPU de l’unité et les affiche dans systemctl status. Il ne lit simplement jamais leur code de sortie. La logique de redémarrage et le cgroup sont deux mécanismes distincts. La majeure partie de ce guide porte sur l’écart entre les deux.
Ce que contient le cgroup et ce que lit la logique de redémarrage
Un cgroup (control group) est un objet du kernel qui regroupe un ensemble de processus. Chaque unité de service en possède un, nommé d’après l’unité. Un processus ne peut pas le quitter. Les processus enfants héritent du cgroup de leur parent, et un processus non privilégié ne peut pas se déplacer ailleurs. C’est pourquoi systemd peut nettoyer un daemon qui effectue deux fork, ce que les anciens scripts init ne pouvaient jamais faire de manière fiable.
Examinez ces deux informations côte à côte :
systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Restart -p RestartUSec myapp.servicesystemd-cgls liste tous les processus de l’unité. MainPID est le nombre unique lu par la politique de redémarrage. Lorsque ces deux informations ne correspondent pas à votre modèle mental, c’est ce décalage qui constitue le bug. MainPID=0 est plus grave qu’un PID incorrect : cela signifie que systemd ne suit absolument rien, et qu’aucune valeur Restart= ne peut donc jamais déclencher d’action.
Il existe une véritable exception à la règle du processus principal. Si l’out-of-memory killer du kernel tue un processus dans le cgroup de l’unité, systemd le détecte, car il surveille le fichier memory.events du cgroup. OOMPolicy= détermine ce qui se passe ensuite, et sa valeur par défaut est stop : l’unité entière est arrêtée, le résultat est enregistré comme oom-kill, et cela compte comme un échec ; Restart=on-failure se déclenche donc. Le journal l’indique clairement.
myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Failed with result 'oom-kill'.Un processus enfant tué pour cause de manque de mémoire arrête donc bien l’unité, tandis que le même processus enfant qui se termine à la suite d’un segmentation fault ne l’arrête pas. Si vous définissez des limites mémoire pour une unité, consultez le fonctionnement de MemoryMax et CPUQuota sur le cgroup d’une unité avant d’ajuster la politique de redémarrage, car ces deux fonctionnalités interagissent ici, et nulle part ailleurs.
Comment Type= détermine le processus principal
Type= dans la section [Service] ne concerne pas uniquement l’ordre de démarrage. Il détermine quel PID (process ID) devient MainPID, ce qui revient à déterminer ce que Restart= peut observer.
Type=simpleest la valeur par défaut. Le processus que systemd fork depuisExecStart=est le processus principal. systemd marque l’unité comme démarrée immédiatement, avant de savoir siexeca réellement fonctionné. Une faute de frappe dans le chemin du binaire produit un job de démarrage qui réussit, puisMain process exited, code=exited, status=203/EXECéchoue un instant plus tard.Type=execse comporte commesimple, mais le job de démarrage attend queexecait réussi. La faute de frappe précédente devient ainsi un échec de démarrage explicite. Cette valeur nécessite systemd 240 ou une version plus récente, ce que proposent toutes les distributions prises en charge. Préférez-la àsimple.Type=forkingattend le processus issu deExecStart=fork un daemon en arrière-plan, puis se termine. systemd attend la fin du processus parent, puis recherche le daemon réel. Indiquez-luiPIDFile=. Sans cette information,GuessMainPID=(activé par défaut) ne fonctionne que lorsqu’il reste exactement un processus dans le cgroup. Si deux processus restent actifs,MainPIDreste0.Type=notifysignifie que le service appellesd_notify(3)et envoieREADY=1lorsqu’il peut traiter du trafic. Il peut également envoyerMAINPID=pour transmettre à systemd un autre processus à suivre.NotifyAccess=vaut par défautmain. Une notification envoyée par un processus enfant est donc ignorée, et le journal indique le PID à l’origine de cette notification.Type=oneshotn’a pas de processus principal durable. L’unité devient inactive dès queExecStart=se termine, sauf si vous définissezRemainAfterExit=yes.Restart=alwaysetRestart=on-successsont refusés ici, avec le messageService has Restart= set to either always or on-success, which isn't allowed for Type=oneshot services. Refusing.. Les autres valeurs, y comprison-failure, sont acceptées.
Deux erreurs Type=forking méritent d’être retenues, car chacune produit une unité qui semble défaillante sans raison visible :
myapp.service: Can't open PID file /run/myapp.pid (yet?) after start: No such file or directory
myapp.service: New main PID 4711 does not belong to service, and PID file is not owned by root. Refusing.La première signifie que le daemon écrit son fichier PID ailleurs ou l’écrit après que systemd l’a recherché. La seconde signifie que le fichier PID désigne un processus situé en dehors du cgroup de l’unité. systemd refuse de l’adopter, car un fichier PID inscriptible pourrait sinon permettre de faire envoyer par systemd des signaux à n’importe quel processus de la machine.
Pourquoi un script wrapper masque la mort de ses processus enfants
Voici la structure qui est à l’origine de la question posée dans le titre.
#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
waitL’unité est Type=simple : le processus principal est donc le shell. wait sans argument ne retourne qu’après la fin de tous les processus enfants. Tuez le worker : le shell continue d’attendre le processus web. Le shell ne se termine donc pas, MainPID ne se termine pas non plus, et Restart= n’est jamais consulté. Le cgroup contient alors un processus de moins, systemctl status affiche l’arborescence raccourcie, et l’unité est toujours active (running). systemd ne surveille pas les changements dans cette arborescence.
Une deuxième version de la même erreur est plus discrète :
ExecStart=/bin/sh -c 'export APP_ENV=production; /usr/local/bin/myapp'Le processus principal est le shell, pas myapp. Lors de systemctl stop, systemd envoie SIGTERM au processus principal. Un shell qui attend un processus enfant au premier plan ne relaie pas le signal. L’arrêt dure alors toute la valeur de TimeoutStopSec, soit 90 secondes par défaut, puis se termine ainsi :
myapp.service: State 'stop-sigterm' timed out. Killing.
myapp.service: Killing process 4711 (myapp) with signal SIGKILL.La correction consiste à utiliser exec. Écrivez exec /usr/local/bin/myapp : le shell est alors remplacé par le programme. MainPID est donc le programme, et les signaux lui parviennent. Mieux encore, supprimez le shell et utilisez Environment= ou EnvironmentFile= dans l’unité. Ce bug reste invisible lorsque la chaîne -c contient une seule commande, car bash et dash optimisent tous deux ce cas en un exec direct. Ajoutez une deuxième commande à la chaîne : le shell reste alors actif devant votre programme.
Reproduisez le problème sur un VPS de test en deux minutes
Enregistrez le wrapper ci-dessus sous /usr/local/bin/two-children.sh, rendez-le exécutable avec chmod +x, puis remplacez les deux chemins de programme par sleep 3600. Faites pointer une unité vers ce wrapper avec Type=simple et Restart=on-failure, puis systemctl daemon-reload et démarrez-la. Exécutez systemd-cgls --unit two-children.service et relevez les trois PID : celui du shell et ceux de ses deux processus enfants. Tuez l’un des processus enfants avec sudo kill <pid>. Vérifiez à nouveau l’unité. L’arborescence contient un processus de moins, l’état est toujours active (running) et le journal ne contient aucun nouveau message. Exécutez maintenant sudo kill -9 <shell pid>. L’unité échoue, le processus enfant restant est nettoyé, car KillMode=control-group est la valeur par défaut, et le journal affiche Scheduled restart job, restart counter is at 1.
Le vocabulaire complet de Restart= et les cas où on préfère on-failure à always
Restart= accepte l’une de sept valeurs. La distinction entre ces valeurs repose sur la définition d’une sortie propre. systemd considère comme propre le code de sortie 0, tout code indiqué dans SuccessExitStatus=, ainsi que les signaux SIGHUP, SIGINT, SIGTERM et SIGPIPE. Tous les autres cas sont considérés comme une sortie incorrecte, y compris SIGKILL et SIGSEGV.
noest la valeur par défaut. L’unité ne redémarre jamais d’elle-même. Une unité sans ligneRestart=s’arrête donc au premier crash et reste arrêtée.on-successredémarre l’unité uniquement après une sortie propre.on-failureredémarre l’unité après un code de sortie différent de zéro, un signal indiquant une sortie incorrecte, un dépassement du délai de démarrage ou d’arrêt, ou l’expiration du watchdog.on-abnormalredémarre l’unité après un signal indiquant une sortie incorrecte, un dépassement de délai ou l’expiration du watchdog, mais jamais après un simple code de sortie différent de zéro.on-abortredémarre l’unité uniquement après un signal indiquant une sortie incorrecte, c’est-à-dire après un crash.on-watchdogredémarre l’unité uniquement lorsqueWatchdogSec=arrive à expiration.alwaysredémarre l’unité dans tous les cas précédents, y compris après une sortie propre avec le statut 0.
on-failure est la valeur par défaut adaptée à un daemon qui doit fonctionner en continu. Elle relance le service après un crash, mais laisse tranquille un exit 0 volontaire. always convient à un programme qui se termine proprement pour une raison indépendante de son contrôle, par exemple un client de tunnel qui renvoie 0 lorsque l’extrémité distante se déconnecte. always peut masquer les bugs. Un service qui démarre, lit un fichier de configuration incorrect, journalise l’erreur et se termine avec le code 0 redémarrera indéfiniment. Le seul indice sera un compteur de redémarrages qui augmente.
SuccessExitStatus= modifie la limite entre sortie propre et sortie incorrecte. Borg renvoie 1 pour les avertissements et 2 pour les erreurs. Une unité de sauvegarde sans SuccessExitStatus=1 est donc marquée comme ayant échoué chaque fois qu’elle ignore un fichier illisible. RestartPreventExitStatus= indique les codes qui empêchent un redémarrage même avec always. C’est la manière propre pour un programme d’indiquer qu’il ne doit pas redémarrer. RestartForceExitStatus= fait l’inverse. Une tâche de sauvegarde doit être exécutée dans une unité Type=oneshot déclenchée par un timer, plutôt que dans une boucle de redémarrage. la paire service et timer qui exécute une tâche selon un calendrier constitue le modèle à reprendre.
Attention lors des tests. Tuer votre service avec un simple kill <pid> envoie SIGTERM, qui figure dans la liste des sorties propres. Restart=on-failure ne fait donc rien, et vous pouvez conclure à tort que votre configuration est incorrecte. Utilisez plutôt kill -9 <pid> ou systemctl kill -s SIGKILL myapp.service. Notez également qu’aucune valeur de Restart= ne se déclenche après systemctl stop, ni lorsque l’unité a été arrêtée parce qu’une dépendance BindsTo= ou PartOf= a disparu. Une tâche d’arrêt n’est pas un échec.
RestartSec et la valeur par défaut de 100 millisecondes
RestartSec= correspond à la pause entre l’arrêt de l’unité et son redémarrage par systemd. La valeur par défaut est de 100 millisecondes. Vérifiez la valeur réellement chargée par votre unité :
systemctl show -p RestartUSec -p StartLimitIntervalUSec -p StartLimitBurst myapp.serviceUne unité qui ne définit pas ce paramètre affiche RestartUSec=100ms. Cette valeur par défaut convient à un service qui plante une fois avant de redémarrer. Elle ne convient pas à un service qui ne peut pas démarrer du tout : cinq redémarrages ont alors lieu en moins d’une demi-seconde, ce qui déclenche précisément la limitation de débit décrite ensuite. Pour tout service qui attend une base de données, un montage ou une route réseau, définissez RestartSec=5s ou une valeur supérieure.
En août 2026, systemd 254 et les versions ultérieures proposent également RestartSteps= et RestartMaxDelaySec=. Ces paramètres augmentent le délai à partir de RestartSec= jusqu’à atteindre une limite maximale sur le nombre de tentatives indiqué. Ubuntu 24.04 fournit systemd 255 et prend en charge ces paramètres. Debian 12 fournit systemd 252 et ne les prend pas en charge. Les délais croissants sont la bonne solution lorsqu’une dépendance peut rester indisponible pendant longtemps.
Ce que signifie réellement « start request repeated too quickly »
C’est l’état que les utilisateurs attribuent à tort à un abandon arbitraire de systemd. Il s’agit d’un compteur. La règle est la suivante : si une unité est démarrée plus de StartLimitBurst= fois pendant StartLimitIntervalSec=, systemd refuse de la démarrer de nouveau et la place dans l’état failed. Les valeurs par défaut sont de 5 démarrages en 10 secondes.
Le journal affiche la séquence :
myapp.service: Scheduled restart job, restart counter is at 5.
myapp.service: Start request repeated too quickly.
myapp.service: Failed with result 'start-limit-hit'.
Failed to start myapp.service - My application.et systemctl start fournit directement la correction :
Job for myapp.service failed because start of the service was attempted too often. See "systemctl status myapp.service" and "journalctl -xeu myapp.service" for details. To force a start use "systemctl reset-failed myapp.service" followed by "systemctl start myapp.service" again.systemctl reset-failed myapp.service réinitialise le compteur et l’état failed. Rien d’autre ne le fait. Un simple systemctl start continue donc d’être refusé jusqu’à l’exécution de cette commande. Les démarrages manuels sont également comptabilisés dans la limite. Quelques exécutions impatientes de systemctl restart pendant la modification d’un fichier de configuration peuvent donc déclencher cette limite, même sans plantage.
Le point trompeur est le suivant : start-limit-hit n’indique jamais pourquoi le service échouait. Il indique seulement qu’il a échoué plusieurs fois rapidement. La véritable cause se trouve dans les lignes précédentes du journal.
Les deux paramètres doivent se trouver dans la section [Unit]. Vous trouverez des exemples qui les placent dans [Service], une configuration acceptée par les anciennes versions de systemd. C’est à cette différence que remonte la confusion. Écrivez-les dans [Unit], puis demandez à systemd ce qu’il a chargé avec systemctl show, car seule la valeur chargée est prise en compte.
[Unit]
Description=My application
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=exec
ExecStart=/usr/local/bin/myapp
Restart=on-failure
RestartSec=10sL’unité dispose ainsi de cinq tentatives sur une période de cinq minutes avant que systemd abandonne. StartLimitIntervalSec=0 désactive complètement la limite. Vous devez donc connaître les conséquences de ce choix : un service qui ne peut jamais démarrer réessaiera indéfiniment et écrira un message dans le journal à chaque tentative. Les valeurs par défaut à l’échelle de la machine se trouvent dans /etc/systemd/system.conf sous les noms DefaultStartLimitIntervalSec= et DefaultStartLimitBurst=.
Un paramètre voisin mérite une mise en garde. StartLimitAction= détermine ce qui se passe lorsque la limite est atteinte. Il accepte notamment les valeurs reboot, reboot-force et poweroff. La valeur par défaut est none : l’unité passe à l’état failed, sans modifier le reste de la machine. Sur un VPS distant, poweroff signifie que la machine reste éteinte jusqu’à ce que vous ouvriez la console du fournisseur.
Corrigez le problème : un processus par unité
C’est la solution dans presque tous les cas. Si deux programmes doivent s’exécuter, écrivez deux unités. Chacune dispose alors d’un véritable processus principal, d’un véritable code de sortie et de sa propre stratégie de redémarrage. Vous obtenez également des journaux séparés, des limites de ressources distinctes et des compteurs de redémarrage indépendants, ce qui est utile à trois heures du matin.
Décrivez la relation entre les unités dans les fichiers d’unité, pas dans un script shell.
After=ordonne uniquement le démarrage. Il ne dit rien sur les échecs.Requires=démarre l’autre unité en parallèle de celle-ci et arrête celle-ci si l’autre est arrêtée explicitement.BindsTo=correspond àRequires=, avec le comportement dont vous avez besoin : cette unité s’arrête lorsque l’autre s’arrête pour quelque raison que ce soit, y compris après un crash. Associez-le àAfter=, sinon l’ordre n’est pas défini.PartOf=propage l’arrêt et le redémarrage vers les dépendances, de sorte quesystemctl restart myapp.targetatteint chaque unité qui en estPartOf=.Upholds=(systemd 249 et versions ultérieures, donc Ubuntu 22.04 et versions ultérieures) maintient l’unité nommée en fonctionnement : si elle s’arrête, systemd la redémarre. Elle est soumise à la même limite de fréquence de démarrage que les autres unités.
Un worker qui ne doit jamais s’exécuter sans son API server et que systemd maintient actif tant que l’API est disponible :
# /etc/systemd/system/myapp-api.service
[Unit]
Description=myapp API server
Wants=network-online.target
After=network-online.target
Upholds=myapp-worker.service
[Service]
Type=exec
User=myapp
ExecStart=/usr/local/bin/myapp serve
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target# /etc/systemd/system/myapp-worker.service
[Unit]
Description=myapp background worker
BindsTo=myapp-api.service
After=myapp-api.service
StartLimitIntervalSec=120
StartLimitBurst=5
[Service]
Type=exec
User=myapp
ExecStart=/usr/local/bin/myapp worker
Restart=on-failure
RestartSec=5sLe worker ne comporte aucune section [Install] et n’est jamais activé manuellement. L’unité de l’API l’inclut avec Upholds=, de sorte que systemctl enable --now myapp-api.service est la seule commande à exécuter. Rechargez la configuration et vérifiez ce que systemd a fait des deux unités :
sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/myapp-worker.service
systemctl list-dependencies myapp-api.servicesystemd-analyze verify n’affiche absolument rien lorsque le fichier est correct. Toute sortie indique un problème, généralement une clé que systemd ne reconnaît pas dans la section où vous l’avez écrite, ou une dépendance vers une unité inexistante.
Deuxième correction : Type=notify, afin que systemd connaisse autre chose qu’un PID
Si le programme parle le protocole de notification de systemd, utilisez-le. Avec Type=notify, le service indique à systemd quand il est prêt. L’ordonnancement devient ainsi réel et non plus fondé sur une supposition. Le service peut aussi envoyer MAINPID= pour indiquer à systemd le processus important, plutôt qu’un processus lanceur.
WatchdogSec= est la partie qui justifie l’effort. Activez-le. Le service doit alors envoyer WATCHDOG=1 via sd_notify(3) au moins aussi souvent. Lorsque les messages s’arrêtent, systemd termine le service avec SIGABRT et le marque comme défaillant. Restart=on-failure ou Restart=on-watchdog le redémarre. C’est le seul mécanisme intégré capable de redémarrer un processus encore actif mais bloqué. Aucune stratégie fondée sur le code de sortie ne peut détecter ce cas.
[Service]
Type=notify
NotifyAccess=main
ExecStart=/usr/local/bin/myapp serve
WatchdogSec=30s
Restart=on-failure
RestartSec=5sUn déclenchement du watchdog apparaît dans le journal sous la forme de myapp.service: Watchdog timeout (limit 30s)!, suivi de l’arrêt du processus. Si, au contraire, l’unité reste dans activating (start) jusqu’à l’expiration de TimeoutStartSec, READY=1 n’est jamais arrivé. Soit le programme ne parle pas le protocole, soit NotifyAccess=main refuse une notification provenant d’un processus enfant. Le journal indique alors les deux PID.
Pour un logiciel qui expose un endpoint HTTP de vérification de santé, mais qui ne prend pas en charge sd_notify, les choix honnêtes sont les suivants : utiliser une petite unité timer qui interroge l’endpoint et appelle systemctl restart, ou laisser un runtime de conteneurs effectuer les vérifications. C’est le rôle de healthchecks de Compose et de leur comportement lors des redémarrages.
Correctif 3 : un supervisor dans l’unité, uniquement lorsqu’il n’y a pas d’autre choix
Certains logiciels sont réellement fournis sous la forme d’un ensemble de processus pilotés par un launcher que vous ne pouvez pas séparer. Vous exécutez alors un supervisor dans l’unité et vous en acceptez la conséquence : systemd surveille le supervisor, le supervisor surveille tout le reste, et votre stratégie de redémarrage se trouve désormais dans 2 fichiers.
Le cas courant concerne un container runtime. Une unité docker compose ou podman applique exactement ce modèle : la stratégie de redémarrage de chaque container est définie dans le fichier Compose, tandis que l’unité systemd se contente de maintenir le runtime actif. Si votre configuration correspond à ce modèle, l’unité qui démarre une stack Compose au boot présente une version fonctionnelle, notamment pourquoi Type=oneshot avec RemainAfterExit=yes est généralement le bon choix dans ce cas.
Le cgroup continue de jouer en votre faveur. Tout ce que le supervisor démarre reste dans le cgroup de l’unité. Ainsi, MemoryMax=, CPUQuota= et le nettoyage lors de l’arrêt couvrent toujours toute l’arborescence. Seule la décision de redémarrage est déléguée.
Quel que soit le supervisor choisi, ne définissez pas Restart=always sur l’unité externe et une stratégie de redémarrage agressive à l’intérieur sans en mesurer les conséquences. 2 couches de logique de redémarrage, chacune avec son propre backoff, produisent un service qui flappe pendant plusieurs minutes et un journal qui n’explique pas pourquoi.
ExitType=cgroup ne signifie pas « redémarrer lorsqu’un processus se termine »
ExitType= (systemd 250 et versions ultérieures, donc disponible dans Ubuntu 24.04 et Debian 12) est le paramètre que l’on trouve en recherchant ce problème. Il produit l’effet inverse de ce que son nom suggère. La valeur par défaut, ExitType=main, signifie que le service est considéré comme arrêté lorsque le processus principal se termine. ExitType=cgroup signifie que le service est considéré comme actif jusqu’à la fin du dernier processus du cgroup.
Ainsi, ExitType=cgroup rend une unité moins sensible à l’arrêt d’un processus, et non plus sensible. Ce paramètre convient à un programme qui fork son véritable worker, puis quitte le processus parent sans écrire de fichier PID, ce qui empêche Type=forking de trouver le daemon. Il ne convient pas au problème décrit ici.
Il n’existe pas de valeur Restart= signifiant « redémarrer l’unité lorsqu’un processus du cgroup se termine ». Pour obtenir ce comportement, vous devez utiliser un processus par unité. Si vous ne pouvez pas séparer le programme et que vous contrôlez le wrapper script, la solution la plus proche est wait -n, qui se termine dès qu’un premier processus fils se termine :
#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
wait -n
exit 1La fin d’un processus fils entraîne alors l’arrêt du wrapper avec un code différent de zéro, ce qui active Restart=on-failure. Il s’agit d’un compromis, pas d’une correction. Vous avez toujours un seul compteur de redémarrages pour deux programmes, un seul flux de logs et aucun moyen de redémarrer uniquement la partie défaillante.
Inspecter ce qui s’est réellement passé
Quatre commandes, dans cet ordre.
systemctl status myapp.service
systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Result -p ExecMainStatus myapp.service
journalctl -u myapp.service -b -o short-precisesystemctl status affiche l’état, le PID principal et l’arborescence du cgroup sur un seul écran. Une unité en bon état affiche Active: active (running), avec une ligne Main PID: qui indique le processus attendu. Si l’arborescence en bas contient des processus que vous ne reconnaissez pas, ou s’il en manque un que vous attendez, vous avez déjà la réponse.
systemd-cgls --unit affiche la même arborescence sans troncature. Cela devient utile lorsqu’une unité gère plus de quelques processus.
systemctl show fournit des informations lisibles par une machine. NRestarts= correspond au compteur de redémarrages. C’est le moyen le plus rapide de distinguer un service qui a redémarré quarante fois d’un service actif depuis le boot. Result= contient la raison du dernier échec : exit-code, signal, timeout, oom-kill, watchdog ou start-limit-hit. ExecMainStatus= correspond au code de sortie brut du dernier processus principal.
Le journal contient la séquence des événements. Voici les trois lignes à rechercher :
myapp.service: Main process exited, code=exited, status=1/FAILURE
myapp.service: Failed with result 'exit-code'.
myapp.service: Scheduled restart job, restart counter is at 1.code=exited, status=N signifie que le programme a choisi de renvoyer N. Le problème vient donc du programme ou de sa configuration. code=killed, signal=SEGV signifie qu’il a subi un crash. code=killed, signal=TERM signifie généralement qu’un autre processus lui a demandé de s’arrêter. Ce n’est pas un échec et cela ne déclenchera pas Restart=on-failure. code=dumped signifie qu’il a créé un core dump. coredumpctl list vous l’affichera lorsque systemd-coredump est installé.
Sur plusieurs machines, NRestarts est la valeur à collecter régulièrement. Une unité dont le compteur augmente chaque jour tombe en échec chaque jour, que quelqu’un l’ait remarqué ou non. Au-delà de deux ou trois serveurs, une méthode cohérente pour exécuter une commande sur chaque serveur transforme cette supposition en rapport.
FAQ
Pourquoi systemctl indique-t-il que mon service est actif alors que le processus s’est arrêté ?
systemd suit un processus par unité de service : le processus principal. Restart= lit uniquement le code de sortie de ce processus. Tous les autres processus lancés par l’unité appartiennent au même cgroup. systemd les arrête lorsque l’unité s’arrête, mais ne surveille jamais leur terminaison. Exécutez systemctl show -p MainPID myapp.service et comparez le nombre avec systemd-cgls --unit myapp.service. Si le processus arrêté apparaît dans l’arborescence, mais n’est pas MainPID, systemd se comporte exactement comme prévu. La solution consiste à utiliser un processus par unité et à définir la relation entre les unités avec BindsTo= et Upholds=.
Que signifie « start request repeated too quickly » ?
Cela signifie que l’unité a été démarrée plus de StartLimitBurst= fois pendant StartLimitIntervalSec=. La valeur par défaut est de 5 démarrages en 10 secondes. systemd cesse donc d’essayer. Il s’agit d’une limite de débit. Elle n’indique jamais pourquoi le service échoue. Consultez donc les lignes précédentes du journal. Réinitialisez l’état avec systemctl reset-failed myapp.service, puis corrigez la cause de l’échec. Si le service attend le démarrage d’un élément lent, augmentez RestartSec=. L’intervalle par défaut de 100 millisecondes consomme les cinq tentatives en moins d’une seconde.
Dois-je utiliser Restart=always ou Restart=on-failure ?
Utilisez on-failure dans presque tous les cas. Cette option redémarre le service après un crash, une sortie avec un code différent de zéro, un dépassement de délai ou un déclenchement du watchdog. Elle ne redémarre pas le service après une exit 0 volontaire. Utilisez always uniquement lorsque le programme se termine proprement pour une raison indépendante de sa volonté, par exemple lorsqu’un client renvoie 0 après la déconnexion de son pair. Le risque de always est qu’un service qui lit une configuration incorrecte, journalise une erreur, puis se termine avec le code 0 boucle indéfiniment. Le seul symptôme visible est alors que NRestarts augmente dans systemctl show.
Pourquoi l’arrêt manuel de mon processus ne déclenche-t-il pas de redémarrage ?
Parce que systemd considère SIGHUP, SIGINT, SIGTERM et SIGPIPE comme des sorties propres, et qu’un kill <pid> simple envoie SIGTERM. Avec Restart=on-failure, une sortie propre n’est pas un échec. Rien ne redémarre donc, et la configuration semble incorrecte alors qu’elle ne l’est pas. Effectuez le test avec kill -9 <pid> ou systemctl kill -s SIGKILL myapp.service. Il s’agit d’un arrêt anormal qui déclenche bien la règle. Cette même règle explique pourquoi systemctl stop ne contredit jamais votre politique de redémarrage.
Où placer StartLimitIntervalSec et StartLimitBurst ?
Dans la section [Unit]. Les anciennes documentations et les anciennes versions de systemd les placent dans [Service]. Les exemples copiés peuvent donc se contredire. Ne devinez pas quelle section votre version prend en compte. Après systemctl daemon-reload, demandez à systemd ce qu’il a chargé avec systemctl show -p StartLimitBurst -p StartLimitIntervalUSec myapp.service et considérez ces valeurs comme la référence. systemd-analyze verify /etc/systemd/system/myapp.service détecte les clés que systemd ne reconnaît pas du tout et n’affiche rien lorsque le fichier est correct.