Dépannage de Systemd : Comprendre les Dépendances de Services et les Directives d'Ordonnancement

Résolvez les problèmes de dépendance et d'ordonnancement de systemd en utilisant correctement les directives Requires, Wants, After, Before et les commandes de diagnostic.

Dépannage de Systemd : Comprendre les Dépendances de Services et les Directives d'Ordonnancement

La plupart des bugs de dépendance systemd proviennent de la confusion entre deux concepts distincts : la dépendance et l'ordonnancement. Requires= et Wants= répondent à la question "quelle autre unité doit être incluse ?" After= et Before= répondent à la question "quand cette unité doit-elle démarrer par rapport à une autre ?" Si vous ne retenez qu'une chose de ce guide, souvenez-vous que After=postgresql.service ne démarre pas PostgreSQL pour vous. Cela indique seulement que si les deux unités sont en cours de démarrage, votre unité doit attendre que le job de démarrage de PostgreSQL soit terminé.

Cette distinction est à l'origine de nombreux incidents du type "ça fonctionne quand je le démarre manuellement, mais échoue après un redémarrage". Une application web démarre avant que le socket de la base de données n'accepte les connexions. Un worker démarre avant que /mnt/jobs ne soit monté. Un service attend network.target et échoue toujours parce qu'une adresse IP n'a pas encore été attribuée. Ce ne sont pas des problèmes systemd exotiques. Ce sont des hypothèses de démarrage ordinaires qui doivent être explicitées.

Directives de Dépendance Principales : Requires et Wants

Systemd utilise deux directives principales pour définir les dépendances directes entre les unités : Requires et Wants. Ces directives sont placées dans la section [Unit] d'un fichier d'unité (par exemple, un fichier .service).

Requires=

La directive Requires= établit une dépendance forte. Si l'unité A Requires= l'unité B, systemd démarre B lorsque A est démarrée. Si B ne peut pas être démarrée, le job de démarrage de A échoue également. Si B est explicitement arrêtée alors que A est en cours d'exécution, A est normalement arrêtée aussi car la relation de dépendance n'est plus valide.

Exemple :

Considérons un service d'application web (myapp.service) qui dépend de manière critique d'un service de base de données (mariadb.service). Le fichier d'unité myapp.service pourrait inclure :

[Unit]
Description=Mon Application Web
Requires=mariadb.service

[Service]
ExecStart=/usr/bin/myapp

[Install]
WantedBy=multi-user.target

Dans ce scénario, si mariadb.service échoue à démarrer ou est arrêté manuellement, systemd arrêtera également myapp.service. Si vous essayez de démarrer myapp.service et que mariadb.service n'est pas en cours d'exécution, systemd tentera de démarrer mariadb.service en premier. Si mariadb.service échoue, myapp.service ne démarrera pas.

Wants=

La directive Wants= définit une dépendance plus faible et optionnelle. Si l'unité A Wants= l'unité B, systemd essaiera de démarrer l'unité B lors du démarrage de l'unité A, mais l'unité A s'activera même si l'unité B échoue à démarrer ou n'est pas en cours d'exécution. Cela est utile pour les services qui bénéficient d'un autre service mais peuvent fonctionner de manière indépendante, peut-être avec des fonctionnalités réduites ou un avertissement.

Exemple :

Supposons qu'un agent de surveillance (monitoring-agent.service) puisse fonctionner sans un service de journalisation spécifique (app-logger.service) mais qu'il serait idéal de l'avoir disponible. Le fichier d'unité monitoring-agent.service pourrait ressembler à ceci :

[Unit]
Description=Agent de Surveillance
Wants=app-logger.service

[Service]
ExecStart=/usr/bin/monitoring-agent

[Install]
WantedBy=multi-user.target

Ici, systemd tentera de démarrer app-logger.service lorsque monitoring-agent.service est activé. Cependant, si app-logger.service échoue à démarrer, monitoring-agent.service continuera à démarrer avec succès.

Requires= vs. Wants=

  • Requires= : Dépendance forte. Si l'unité requise échoue, l'unité dépendante échoue ou s'arrête.
  • Wants= : Dépendance faible. L'unité dépendante tente de démarrer l'unité souhaitée mais continue même si elle échoue.

Il est important de noter que Requires= implique Wants=. Si une unité en requiert une autre, elle la souhaite aussi implicitement.

Directives d'Ordonnancement : After et Before

Alors que Requires et Wants définissent ce qui doit être en cours d'exécution, After et Before définissent quand les unités doivent être démarrées les unes par rapport aux autres. Ces directives contrôlent la séquence des opérations pendant le processus de démarrage du système ou lorsque les unités sont activées à la demande. Elles sont souvent utilisées conjointement avec les directives de dépendance.

After=

La directive After= spécifie que le job de démarrage de l'unité actuelle doit être ordonné après les jobs de démarrage des unités listées. Elle n'inclut pas, par elle-même, ces unités dans la transaction. Elle ne prouve pas non plus que la dépendance est logiquement prête pour votre application ; elle utilise uniquement la vue de systemd sur l'état d'activation de l'unité.

Exemple :

Un service dépendant du réseau (custom-network-app.service) devrait démarrer seulement après que le réseau soit entièrement configuré. Cela est généralement géré en s'assurant qu'il démarre après la cible réseau (network.target).

[Unit]
Description=Application Réseau Personnalisée
Requires=network.target
After=network.target

[Service]
ExecStart=/usr/bin/custom-network-app

[Install]
WantedBy=multi-user.target

Dans cette configuration, systemd ordonnera custom-network-app.service après network.target si les deux font partie de la même transaction. Pour les services qui ont besoin d'une adresse, DNS ou d'une route vers un autre hôte, network-online.target est souvent plus proche de l'intention, mais seulement si le service wait-online de la distribution est activé et correctement configuré.

Before=

La directive Before= spécifie que l'unité actuelle doit être démarrée avant les unités listées dans Before=. Cela est utile pour les services qui doivent être arrêtés après d'autres lors de l'arrêt, ou démarrés avant certains services pour leur fournir un environnement.

Exemple :

Imaginez un scénario où un serveur de messagerie (postfix.service) doit être en cours d'exécution avant tout service orienté utilisateur qui pourrait envoyer des emails. Vous pourriez utiliser Before= pour garantir que postfix.service démarre tôt.

[Unit]
Description=Agent de Transfert de Courrier Postfix
# ... autres directives comme Conflicts=
Before=user-session.target

[Service]
ExecStart=/usr/lib/postfix/master

[Install]
WantedBy=multi-user.target

Cette configuration tente de démarrer postfix.service avant que tout ce qui fait partie de user-session.target ne commence son démarrage. De même, lors de l'arrêt, postfix.service serait parmi les derniers à être arrêté s'il a un After=user-session.target correspondant.

After= vs. Before=

  • After= : Garantit que les unités listées sont actives avant que l'unité actuelle ne démarre.
  • Before= : Garantit que l'unité actuelle démarre avant les unités listées.

After= et Before= sont complémentaires. Si l'unité A dit Before=B, l'ordonnancement est A d'abord, puis B. Si l'unité B dit After=A, le résultat est le même. Vous n'avez généralement besoin d'exprimer la relation que dans un seul fichier d'unité. Lorsque vous modifiez votre propre service, il est généralement plus clair de dire ce que votre service doit venir après, car cela garde le raisonnement local.

Combinaison de Directives pour des Configurations Robustes

Dans des scénarios réels, vous combinerez souvent ces directives pour créer des graphes de dépendance complexes. La cible multi-user.target est une cible courante qui signifie que le système est prêt pour des opérations multi-utilisateurs. De nombreux services sont configurés pour être WantedBy=multi-user.target et After=multi-user.target (ou plus précisément, After=basic.target et After=getty.target etc. dont multi-user.target dépend).

Un Modèle Courant :

Un service qui nécessite une base de données et doit démarrer après la configuration du réseau pourrait ressembler à ceci :

[Unit]
Description=Mon Service d'Application
Requires=mariadb.service
Wants=other-optional-service.service
After=network.target mariadb.service

[Service]
ExecStart=/usr/local/bin/my_app

[Install]
WantedBy=multi-user.target

Explication du modèle :

  1. Requires=mariadb.service : Garantit que mariadb.service doit être en cours d'exécution pour que my_app.service fonctionne. Si mariadb.service échoue, my_app.service s'arrêtera.
  2. Wants=other-optional-service.service : Tente de démarrer other-optional-service.service mais my_app.service continuera même s'il échoue.
  3. After=network.target mariadb.service : Ordonne my_app.service après la cible réseau et le job de démarrage de MariaDB. Si l'application doit se connecter via TCP à une base de données sur un autre hôte, utilisez la configuration réseau en ligne appropriée au lieu de supposer que network.target signifie "le réseau est utilisable".
  4. WantedBy=multi-user.target : Lorsqu'il est activé (systemctl enable my_app.service), cette directive ajoute un lien symbolique pour que my_app.service soit démarré lorsque le système atteint l'état multi-user.target.

Considérations Avancées et Meilleures Pratiques

  • WantedBy vs. RequiredBy : Similaire à Wants vs. Requires, WantedBy est un ordre faible et RequiredBy est un ordre fort. La plupart des services utilisent WantedBy=multi-user.target.
  • Conflicts= : Cette directive spécifie les unités qui ne doivent pas fonctionner simultanément avec l'unité actuelle. Si l'unité actuelle est démarrée, les unités en conflit seront arrêtées, et vice versa.
  • Dépendances Transitives : Les dépendances sont transitives. Si A requiert B, et B requiert C, alors A requiert indirectement C. Systemd gère ces chaînes automatiquement.
  • Directives Condition*= : Utilisez ConditionPathExists=, ConditionFileNotEmpty=, ConditionVirtualization=, etc., pour rendre l'activation de l'unité conditionnelle en fonction de l'état du système, améliorant ainsi la robustesse.
  • Utilisez systemctl list-dependencies <unité> : Cette commande est inestimable pour visualiser l'arbre de dépendance d'une unité, y compris les dépendances directes et indirectes.
  • Utilisez systemctl status <unité> : Vérifiez toujours le statut de votre service après avoir apporté des modifications de configuration. Il montrera souvent les raisons de l'échec, y compris les problèmes de dépendance.
  • Évitez les Dépendances Circulaires : Bien que systemd essaie de les résoudre, les dépendances circulaires directes (A Requiert B, B Requiert A) peuvent entraîner des boucles de démarrage ou des échecs. Concevez soigneusement vos dépendances pour éviter cela.

Un Passage de Débogage Pratique

Lorsqu'un bug de dépendance se manifeste, commencez par examiner la transaction que systemd a réellement construite :

systemctl status my_app.service
journalctl -b -u my_app.service --no-pager
systemctl list-dependencies my_app.service
systemctl show my_app.service -p Wants -p Requires -p After -p Before -p BindsTo -p PartOf

systemctl status vous indique si systemd a échoué à démarrer l'unité, l'a tuée plus tard, ou la considère active même si l'application est défaillante. journalctl -b vous maintient dans le démarrage actuel, ce qui est important car les problèmes de dépendance sont souvent spécifiques au démarrage. systemctl show est brut mais utile : il montre les propriétés finales fusionnées de l'unité après l'application des fichiers de remplacement, des fichiers fournisseurs et des dépendances générées.

Si vous n'êtes pas sûr de l'origine d'une dépendance, inspectez l'unité complète :

systemctl cat my_app.service

Cela montre l'unité fournie et tous les fichiers de remplacement sous /etc/systemd/system/my_app.service.d/. J'ai vu des services en production où l'unité de base était correcte mais un ancien remplacement contenait encore After=mysql.service d'une migration précédente. Le service attendait une unité qui n'existait plus, et les journaux donnaient l'impression que l'application était cassée.

Pour les questions de chronométrage au démarrage, utilisez :

systemd-analyze critical-chain my_app.service
systemd-analyze blame

critical-chain est meilleur que de regarder les horodatages car il montre quelles unités ont retardé le chemin vers votre service. blame peut être trompeur si vous le traitez comme un classement des services "mauvais", mais il est utile lorsqu'une dépendance prend beaucoup plus de temps que prévu.

Modèles Qui Tiennent en Production

Pour une dépendance de base de données locale, voici un point de départ raisonnable :

[Unit]
Description=Service API
Requires=postgresql.service
After=postgresql.service

[Service]
ExecStart=/usr/local/bin/api
User=api
Restart=on-failure

[Install]
WantedBy=multi-user.target

Cela indique que PostgreSQL doit être démarré avec l'API, et que le démarrage de l'API doit être ordonné après PostgreSQL. Cela ne garantit pas que chaque migration est terminée ou que la base de données accepte les identifiants exacts que votre application utilise. Si cela est important, ajoutez une vérification de disponibilité au niveau de l'application. Systemd peut ordonner les processus, mais il ne peut pas comprendre l'état de votre schéma à moins que vous n'appreniez à votre service à le vérifier.

Pour un chemin monté, préférez RequiresMountsFor= plutôt que de nommer manuellement les unités de montage :

[Unit]
RequiresMountsFor=/srv/uploads

[Service]
ExecStart=/usr/local/bin/upload-worker
User=uploads

Systemd dérivera les unités de montage nécessaires pour le chemin. C'est plus facile à maintenir que de se souvenir que /srv/uploads correspond à srv-uploads.mount.

Pour les assistants optionnels, utilisez Wants= :

[Unit]
Wants=metrics-agent.service
After=metrics-agent.service

Si l'agent de métriques échoue, le service principal peut toujours démarrer. C'est généralement ce que vous voulez pour les sidecars de journalisation, les exportateurs optionnels et les assistants de notification locaux. N'utilisez pas Requires= simplement parce que deux services sont liés. Utilisez-le uniquement lorsque le service dépendant ne peut vraiment pas effectuer de travail utile sans l'autre unité.

Pour les services étroitement couplés qui doivent s'arrêter ensemble, regardez BindsTo= et PartOf=. BindsTo= est plus fort que Requires= et est utile lorsqu'un service doit disparaître si l'unité liée disparaît, comme un service lié à une unité de périphérique spécifique. PartOf= est souvent utile pour les groupes : redémarrer ou arrêter l'unité parente peut se propager aux unités enfants. Ce ne sont pas des directives de premier choix, mais elles résolvent des problèmes que Requires= ne peut pas exprimer proprement.

Pièges Courants

N'ajoutez pas After=multi-user.target à un service normal à longue durée de vie qui est activé avec WantedBy=multi-user.target. Cela crée souvent un ordre étrange et dit rarement ce que l'auteur voulait. La plupart des services sont inclus par multi-user.target ; ils n'ont pas besoin de démarrer après qu'il ait déjà été atteint.

Ne supposez pas que network.target signifie "internet est accessible". C'est un point de synchronisation pour la gestion du réseau, pas un test de connectivité. Si votre application communique avec une API distante, ajoutez de toute façon une logique de nouvelle tentative dans l'application. L'ordonnancement réseau au démarrage réduit le bruit, mais il ne peut pas vous protéger contre une panne DNS, des changements de routage ou une dépendance distante en panne.

Ne cachez pas de longues pauses dans ExecStartPre=/bin/sleep 30 sauf si vous n'avez pas de meilleure option. Les pauses ralentissent les démarrages lorsque la dépendance est prête rapidement et échouent toujours lorsque la dépendance prend plus de temps que prévu. Une petite boucle de disponibilité qui vérifie le socket, le fichier ou l'API réel est généralement plus claire.

Lorsque vous modifiez les directives de dépendance, exécutez systemctl daemon-reload, redémarrez le service et vérifiez le journal du même démarrage. La correction la plus rapide est généralement celle qui prouve l'hypothèse d'ordonnancement exacte qui était erronée.