Dépannage des échecs de services Systemd : Un guide étape par étape

Diagnostiquez les échecs de services systemd avec des vérifications d'état, les journaux du journal, l'examen des fichiers d'unité, les corrections de dépendances et le débogage de l'environnement.

Dépannage des échecs de services Systemd : Un guide étape par étape

Les échecs de services systemd sont plus faciles à déboguer lorsque vous prenez le temps de suivre les preuves. Une unité défaillante laisse généralement trois indices utiles : l'état enregistré par systemd, la commande qu'il a tenté d'exécuter et les journaux écrits par systemd ou l'application. Si vous les lisez dans l'ordre, vous évitez le piège courant de modifier un fichier d'unité avant de savoir si le problème vient de l'unité, de l'application, d'une dépendance ou de l'hôte.

Les exemples ci-dessous utilisent un mywebapp.service fictif, mais le même flux de travail s'applique aux assistants de base de données, aux consommateurs de files d'attente, aux tâches de sauvegarde, aux exportateurs et aux démons internes.

La première ligne de défense : systemctl status

Lorsqu'un service ne démarre pas, la toute première commande que vous devez exécuter est systemctl status <nom_du_service>. Cette commande fournit un instantané de l'état actuel du service, y compris s'il est actif, chargé et, surtout, un extrait de ses journaux récents. Cela fournit souvent suffisamment d'informations pour identifier rapidement le problème.

Disons que votre service d'application web, mywebapp.service, ne démarre pas :

systemctl status mywebapp.service

Interprétation de l'exemple de sortie :

● mywebapp.service - Mon application web
     Loaded: loaded (/etc/systemd/system/mywebapp.service; enabled; vendor preset: disabled)
     Active: failed (Result: exit-code) since Mon 2023-10-26 10:30:05 UTC; 10s ago
    Process: 12345 ExecStart=/usr/local/bin/mywebapp-start.sh (code=exited, status=1/FAILURE)
   Main PID: 12345 (code=exited, status=1/FAILURE)
        CPU: 10ms

Oct 26 10:30:05 hostname systemd[1]: Started My Web Application.
Oct 26 10:30:05 hostname mywebapp-start.sh[12345]: Error: Port 8080 already in use
Oct 26 10:30:05 hostname systemd[1]: mywebapp.service: Main process exited, code=exited, status=1/FAILURE
Oct 26 10:30:05 hostname systemd[1]: mywebapp.service: Failed with result 'exit-code'.

À partir de cette sortie, nous pouvons immédiatement voir :

  • Le service mywebapp.service est failed.
  • Il a échoué avec Result: exit-code, ce qui signifie que la commande ExecStart s'est terminée avec un statut non nul.
  • La ligne Process montre que la commande mywebapp-start.sh a échoué avec status=1/FAILURE.
  • Crucialement, les lignes de journal indiquent : Error: Port 8080 already in use. C'est un indicateur clair du problème.

Cette commande est votre premier outil de diagnostic, pointant souvent directement vers la cause ou réduisant le champ de recherche.

Plongée en profondeur avec journalctl

Alors que systemctl status fournit un résumé rapide, journalctl est votre commande de référence pour une journalisation détaillée. Elle interroge le journal systemd, qui collecte les journaux de toutes les parties du système, y compris les services.

Examen de base des journaux

Pour afficher tous les journaux d'un service spécifique, y compris les entrées historiques :

journalctl -u mywebapp.service

Cela affichera toutes les entrées de journal associées à mywebapp.service. Si le service échoue à plusieurs reprises, vous verrez des entrées de chaque tentative échouée.

Filtrage et requêtes temporelles

Pour affiner les résultats, surtout après un échec récent, vous pouvez utiliser des indicateurs comme --since et --priority :

  • Afficher les journaux depuis un moment spécifique :
    journalctl -u mywebapp.service --since "10 minutes ago"
    journalctl -u mywebapp.service --since "2023-10-26 10:00:00"
    
  • Afficher uniquement les messages de niveau erreur ou supérieur :
    journalctl -u mywebapp.service -p err
    
  • Combiner avec -xe pour une explication étendue et une sortie verbeuse :
    journalctl -u mywebapp.service -xe --since "5 minutes ago"
    
    -x peut ajouter du texte explicatif pour certains messages systemd. Traitez ces explications comme des indices, pas comme un remplacement des journaux spécifiques à l'unité.

Comprendre les messages du journal

Recherchez des mots-clés comme Error, Failed, Warning, ou des messages spécifiques à l'application qui indiquent ce qui n'a pas fonctionné. Faites attention aux horodatages pour comprendre la séquence des événements ayant conduit à l'échec.

Astuce : Si le script ExecStart de votre service imprime sur la sortie standard ou l'erreur standard, ces messages sont généralement capturés par journalctl. Assurez-vous que vos scripts enregistrent des messages d'erreur descriptifs.

Inspection du fichier d'unité : Le plan de votre service

Chaque service systemd est défini par un fichier d'unité (par exemple, mywebapp.service). Les mauvaises configurations dans ce fichier sont une source courante d'échecs de démarrage. Vous devez comprendre ce que le service essaie de faire.

Récupération du fichier d'unité

Pour afficher le fichier d'unité actif de votre service :

systemctl cat mywebapp.service

Cette commande montre le fichier d'unité exact que systemd utilise, y compris les éventuelles surcharges.

Directives clés à vérifier

Concentrez-vous sur la section [Service] pour les problèmes liés à l'exécution et [Unit] pour les dépendances.

  • ExecStart : C'est la commande que systemd exécute pour démarrer votre service. Vérifiez que le chemin est correct et que la commande elle-même est exécutable et s'exécute avec succès lorsqu'elle est invoquée manuellement (par exemple, en tant qu'User spécifié).
    ExecStart=/usr/local/bin/mywebapp-start.sh
    
  • Type : Définit le type de démarrage du processus. Les types courants incluent :
    • simple (par défaut) : ExecStart est le processus principal.
    • forking : ExecStart crée un processus enfant et le parent se termine. Systemd attend la fin du parent.
    • oneshot : ExecStart s'exécute et se termine ; systemd considère le service comme actif tant que la commande est en cours d'exécution.
    • notify : Le service envoie une notification à systemd lorsqu'il est prêt.
    • Un Type incorrect peut amener systemd à penser qu'un service a échoué alors qu'il a réellement démarré, ou vice-versa.
  • User / Group : L'utilisateur et le groupe sous lesquels le service s'exécutera. Les problèmes de permissions proviennent souvent du fait que le service tente d'accéder à des fichiers ou des ressources pour lesquels il n'a pas les droits sous cet utilisateur.
    User=mywebappuser
    Group=mywebappgroup
    
  • WorkingDirectory : Le répertoire à partir duquel le service s'exécutera. Les chemins relatifs dans ExecStart ou d'autres commandes en dépendent.
  • Restart : Définit quand le service doit être redémarré. S'il est défini sur on-failure ou always, un service défaillant peut redémarrer constamment, rendant plus difficile la capture de l'échec initial.
  • TimeoutStartSec / TimeoutStopSec : Combien de temps systemd attend que le service démarre ou s'arrête. Si un service met plus de temps à s'initialiser que TimeoutStartSec, systemd le tuera et signalera un échec.

Problèmes courants de fichier d'unité

  • Chemins incorrects : Faute de frappe dans ExecStart ou d'autres chemins de fichiers.
  • Variables d'environnement Environment manquantes : Les services nécessitent souvent des variables d'environnement spécifiques (par exemple, PATH) qui peuvent ne pas être présentes dans l'environnement propre de systemd (voir ci-dessous).
  • Permissions : L'User spécifié n'a pas les permissions d'exécution pour le script ou les permissions de lecture/écriture pour les fichiers de données nécessaires.
  • Erreurs de syntaxe : Simples fautes de frappe dans le fichier d'unité lui-même.

Pour tester ExecStart manuellement :

Passez à l'utilisateur du service et essayez d'exécuter la commande directement :

sudo -u mywebappuser /usr/local/bin/mywebapp-start.sh

Cela reproduit souvent l'erreur vue dans journalctl directement dans votre terminal, facilitant le débogage.

Gestion des dépendances : Quand les services ne peuvent pas démarrer seuls

Les services dépendent souvent d'autres services ou composants système pour être actifs avant de pouvoir démarrer eux-mêmes. Systemd utilise les directives Wants, Requires, After et Before pour gérer ces dépendances.

Identification des dépendances

Utilisez systemctl list-dependencies <nom_du_service> pour voir ce qu'un service nécessite explicitement ou souhaite exécuter.

systemctl list-dependencies mywebapp.service

Directives courantes dans la section [Unit] :

  • After= : Spécifie que ce service doit démarrer après les unités listées. Si l'unité listée échoue, ce service tentera toujours de démarrer (sauf si Requires= est également utilisé).
  • Requires= : Spécifie que ce service nécessite les unités listées. Si l'une des unités requises échoue à démarrer, ce service ne démarrera pas.
  • Wants= : Une forme plus faible de Requires=. Si une unité souhaitée échoue, ce service tentera toujours de démarrer.

Exemple :

[Unit]
Description=Mon application web
After=network.target mysql.service
Requires=mysql.service

Ici, mywebapp.service est ordonné après network.target et mysql.service, et nécessite que mysql.service démarre avec succès. Si mysql.service échoue, mywebapp.service ne démarrera pas.

Résolution des conflits de dépendances

Si un service échoue en raison d'un problème de dépendance, journalctl indiquera généralement quelle dépendance n'a pas pu être satisfaite. Par exemple, il pourrait indiquer Dependency failed for My Web Application suivi de détails sur l'échec de mysql.service.

Étapes pour résoudre :

  1. Vérifiez le service dépendant : Exécutez systemctl status <service_dépendant> (par exemple, systemctl status mysql.service) et journalctl -u <service_dépendant> pour déboguer son échec en premier.
  2. Vérifiez les directives After= et Requires= : Assurez-vous qu'elles reflètent correctement l'ordre de démarrage souhaité et la rigueur. Parfois, un service doit attendre qu'un port spécifique soit ouvert, pas seulement que le travail de démarrage d'une autre unité soit terminé. Pour des vérifications étroites, ExecStartPre= peut aider. Pour les démons réseau, l'activation par socket ou la logique de nouvelle tentative au niveau de l'application est souvent plus fiable.

Variables d'environnement et chemins : Les pièges cachés

Les services systemd s'exécutent dans un environnement très propre et minimal. Cela conduit souvent à des problèmes où des commandes qui fonctionnent parfaitement dans le shell d'un utilisateur échouent lorsqu'elles sont exécutées par systemd car des variables d'environnement cruciales (comme PATH) sont manquantes.

L'environnement propre de systemd

Lorsque systemd démarre un service, il n'hérite pas de l'environnement complet de l'utilisateur qui a initié systemctl start. La variable PATH, par exemple, est souvent réduite, ce qui signifie que des commandes comme python ou node peuvent ne pas être trouvées si elles ne se trouvent pas dans des emplacements standard comme /usr/bin ou /bin.

Symptôme : ExecStart=/usr/local/bin/myscript.sh échoue avec python: command not found, node: command not found, une erreur de bibliothèque manquante, ou un message de l'application indiquant qu'un paramètre requis est vide.

Correctif : Rendre l'environnement du service explicite.

[Service]
WorkingDirectory=/opt/mywebapp
Environment="APP_ENV=production"
Environment="PATH=/opt/mywebapp/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"
ExecStart=/opt/mywebapp/venv/bin/gunicorn app:app

Pour de nombreuses variables, utilisez un fichier d'environnement :

[Service]
EnvironmentFile=/etc/mywebapp/mywebapp.env
ExecStart=/opt/mywebapp/bin/server

Gardez ce fichier simple. EnvironmentFile= n'est pas un script Bash. Utilisez des lignes KEY=valeur, pas export KEY=valeur, de substitution de commande ou de conditionnels shell. Définissez également des permissions restrictives si le fichier contient des secrets :

sudo chown root:mywebapp /etc/mywebapp/mywebapp.env
sudo chmod 0640 /etc/mywebapp/mywebapp.env

Permissions : Reproduire l'échec en tant qu'utilisateur du service

Les problèmes de permissions sont courants car les tests manuels se font souvent en tant que root ou en tant qu'utilisateur connecté, tandis que l'unité s'exécute en tant que compte de service dédié.

Vérifiez l'utilisateur configuré :

systemctl show mywebapp.service -p User -p Group

Exécutez ensuite la même commande en tant que cet utilisateur :

sudo -u mywebappuser /usr/local/bin/mywebapp-start.sh

Si l'application a besoin d'un répertoire de travail, incluez-le :

sudo -u mywebappuser bash -lc 'cd /opt/mywebapp && /usr/local/bin/mywebapp-start.sh'

Regardez au-delà de l'exécutable. L'utilisateur du service peut avoir besoin d'un accès en lecture à /etc/mywebapp/config.yml, d'un accès en écriture à /var/lib/mywebapp, d'un accès en exécution sur chaque répertoire parent, ou de la permission de créer un socket Unix sous /run/mywebapp. Une vérification rapide peut économiser beaucoup de suppositions :

sudo -u mywebappuser test -r /etc/mywebapp/config.yml
sudo -u mywebappuser test -w /var/lib/mywebapp
namei -l /var/lib/mywebapp/uploads

Si le service échoue uniquement lors de la liaison à un port bas comme 80 ou 443, ne l'exécutez pas immédiatement en tant que root. Un proxy inverse, une activation par socket ou une capacité ciblée peuvent être plus sûrs selon le service.

Limites de démarrage et boucles de redémarrage

Un service qui plante à plusieurs reprises peut s'arrêter avec un message comme start request repeated too quickly. Cela signifie que la limite de débit de systemd s'est déclenchée. L'échec d'origine s'est produit plus tôt, donc ne vous concentrez pas uniquement sur le message de limite de débit.

Utilisez :

journalctl -u mywebapp.service --since "30 minutes ago"
systemctl show mywebapp.service -p NRestarts -p Restart -p StartLimitBurst -p StartLimitIntervalUSec

Après avoir corrigé la cause première, effacez l'état d'échec :

sudo systemctl reset-failed mywebapp.service
sudo systemctl start mywebapp.service

Soyez prudent avec Restart=always. C'est utile pour les démons résilients, mais pendant le débogage, cela peut inonder le journal et cacher la première erreur claire. Vous pouvez temporairement arrêter l'unité, examiner les journaux et la démarrer manuellement une fois que vous avez changé une chose.

Valider l'unité avant de recharger

Avant de redémarrer un service après avoir modifié un fichier d'unité, validez le fichier et rechargez systemd :

sudo systemd-analyze verify /etc/systemd/system/mywebapp.service
sudo systemctl daemon-reload
sudo systemctl restart mywebapp.service

Si le service a des surcharges drop-in, inspectez la version fusionnée :

systemctl cat mywebapp.service
systemctl show mywebapp.service -p FragmentPath -p DropInPaths -p ExecStart

Cela capture les cas gênants : vous avez modifié un fichier sous /usr/lib/systemd/system, mais un drop-in sous /etc/systemd/system/mywebapp.service.d/override.conf modifie toujours ExecStart ; ou vous avez corrigé un fichier d'unité copié qui n'est pas celui chargé par systemd.

Un ordre pratique des opérations

Lorsqu'un service de production est en panne, utilisez une boucle courte et reproductible :

  1. Exécutez systemctl status mywebapp.service --no-pager.
  2. Lisez journalctl -u mywebapp.service --since "15 minutes ago".
  3. Inspectez systemctl cat mywebapp.service.
  4. Vérifiez la commande, l'utilisateur, le répertoire de travail, l'environnement et les dépendances.
  5. Reproduisez la commande en tant qu'utilisateur du service.
  6. Faites un changement.
  7. Exécutez systemctl daemon-reload si l'unité a changé.
  8. Redémarrez et vérifiez à nouveau le journal.

Cet ordre maintient l'investigation ancrée. Si le journal dit Permission denied, corrigez les permissions. S'il dit No such file or directory, vérifiez les chemins du point de vue de systemd. S'il dit Dependency failed, déboguez d'abord la dépendance. S'il dit que le processus s'est terminé avec le statut 0/SUCCESS mais que le service est en échec, vérifiez Type= et si l'application se démonise ou se termine immédiatement.

L'objectif n'est pas de mémoriser chaque directive systemd. C'est de continuer à faire correspondre le message d'échec à la couche qui l'a produit.