Comprendre les dépendances Systemd : Prévenir et résoudre les conflits d'unités
Apprenez comment fonctionnent les dépendances, l'ordonnancement, les cibles et les conflits de systemd pour que les services démarrent de manière fiable et que les échecs soient plus faciles à diagnostiquer.
Comprendre les dépendances Systemd : Prévenir et résoudre les conflits d'unités
Les dépendances Systemd sont faciles à comprendre à moitié. Un fichier d'unité contient Requires=postgresql.service, l'application démarre quand même trop tôt, et tout le monde se demande pourquoi systemd a ignoré la dépendance. Il n'a rien ignoré. Requires= et After= répondent à des questions différentes.
Cette distinction est au cœur de la plupart des problèmes de dépendances systemd. Une directive contrôle si une autre unité est incluse dans la même transaction. Une directive différente contrôle l'ordonnancement. D'autres directives lient le comportement d'arrêt, lient la durée de vie d'une unité à une autre, ou rendent deux unités mutuellement exclusives. Une fois que vous séparez ces idées, les conflits d'unités deviennent beaucoup moins mystérieux.
Les Fondamentaux : Directives de Dépendance des Unités Systemd
Systemd utilise des directives spécifiques dans les fichiers d'unité (généralement situés dans /etc/systemd/system/ ou /lib/systemd/system/) pour dicter quand une unité doit démarrer, s'arrêter ou attendre une autre. Comprendre ces directives est la première étape pour gérer correctement les dépendances.
Directives de Dépendance Principales
Ces directives contrôlent les relations entre les unités. Elles ne contrôlent pas, par elles-mêmes, toujours l'ordre de démarrage :
Requires=:- Établit une dépendance forte. Si l'unité requise ne parvient pas à démarrer, l'unité actuelle échouera également.
- N'implique pas
PartOf=, et ne signifie pas automatiquement "démarrer après cette unité".
Wants=:- Une dépendance faible. Si l'unité souhaitée échoue, l'unité actuelle tentera quand même de démarrer. Ceci est utilisé pour les dépendances optionnelles.
BindsTo=:- Similaire à
Requires=, mais plus fort en ce qui concerne l'arrêt. Si l'unité liée s'arrête (pour une raison quelconque), l'unité actuelle est également arrêtée.
- Similaire à
PartOf=:- Indique que l'unité actuelle est une partie subordonnée d'une autre unité (par exemple, une activation de socket spécifique liée à un service principal). Si l'unité supérieure s'arrête, l'unité subordonnée s'arrête également.
Conflicts=:- Indique que deux unités ne doivent pas être actives en même temps. Démarrer l'une amène systemd à arrêter l'autre dans le cadre de la même transaction.
Directives Principales de Synchronisation du Démarrage
Ces directives dictent quand l'unité dépendante doit démarrer par rapport à l'unité requise :
After=:- Spécifie que le travail de démarrage de l'unité actuelle est ordonné après le travail de démarrage de l'unité listée. Il n'inclut pas cette unité par lui-même.
Before=:- Spécifie que l'unité actuelle doit démarrer avant l'unité listée.
Meilleure Pratique : Pour l'ordonnancement typique du démarrage des services,
Wants=combiné avecAfter=est le modèle le plus courant et le plus sûr.Requires=doit être réservé aux dépendances où l'échec de la dépendance doit entraîner l'échec du service dépendant.
Exemple : Définir des Dépendances dans un Fichier de Service
Considérons un service d'application personnalisé, myapp.service, qui doit communiquer avec une base de données gérée par PostgreSQL (postgresql.service).
# /etc/systemd/system/myapp.service
[Unit]
Description=Mon Application Personnalisée
# S'assurer que PostgreSQL est en cours d'exécution avant de tenter de me démarrer
Requires=postgresql.service
After=postgresql.service
[Service]
ExecStart=/usr/bin/myapp
[Install]
WantedBy=multi-user.target
Cet exemple est intentionnellement strict. Si PostgreSQL ne peut pas démarrer, myapp.service doit échouer aussi. Pour un service qui peut fonctionner en mode dégradé sans la base de données, utilisez Wants=postgresql.service avec le même ordonnancement After=postgresql.service. L'application demande toujours à systemd de démarrer PostgreSQL en premier, mais il est autorisé à continuer si PostgreSQL est indisponible.
Il n'y a pas de honte à choisir la relation la plus faible quand elle correspond à la réalité. Un exportateur de métriques, un expéditeur de journaux ou un réchauffeur de cache peut être utile lorsque sa dépendance est présente, mais ne doit pas bloquer le démarrage du système principal. Les dépendances dures sont mieux réservées aux cas où démarrer sans l'autre unité serait erroné ou dangereux.
Pour les applications en réseau, soyez plus prudent. network.target signifie souvent que la pile réseau de base est présente, pas que DHCP est terminé ou que DNS est utilisable. Si votre application a vraiment besoin d'une configuration réseau au démarrage, utilisez :
[Unit]
Wants=network-online.target
After=network-online.target
Confirmez ensuite que votre distribution a le service wait-online correspondant activé, comme systemd-networkd-wait-online.service ou l'unité wait-online de NetworkManager. Sans cela, network-online.target peut ne pas attendre ce que vous pensez qu'il attend.
Diagnostiquer les Problèmes de Dépendance
Lorsqu'un service ne parvient pas à démarrer, systemd fournit généralement suffisamment d'informations dans les journaux, mais les chaînes de dépendance peuvent obscurcir la cause racine. Voici des outils et commandes essentiels pour le dépannage.
1. Vérifier l'État de l'Unité et les Journaux
Le point de départ fondamental est de vérifier l'état du service et de consulter ses journaux immédiatement après une tentative de démarrage échouée.
# Vérifier l'état global, qui mentionne souvent les échecs de dépendance
systemctl status myapp.service
# Afficher les journaux détaillés spécifiquement liés à l'unité
journalctl -u myapp.service --since "il y a 5 minutes"
2. Analyser l'Arbre de Dépendance
Systemd fournit des outils de visualisation puissants pour voir exactement ce qui attend quoi.
systemctl list-dependencies
Cette commande montre les unités qui sont requises ou souhaitées par l'unité spécifiée, en parcourant toute la chaîne de dépendance.
Pour voir ce dont myapp.service a besoin pour démarrer :
# Dépendances directes (ce qui doit démarrer avant moi)
systemctl list-dependencies --after myapp.service
# Dépendances inverses (ce qui dépend de moi)
systemctl list-dependencies --before myapp.service
Utilisez --plain lorsque le formatage de l'arbre gêne, et ajoutez --all lorsque vous avez besoin de voir aussi les unités inactives :
systemctl list-dependencies --plain --all myapp.service
systemd-analyze dot
Pour des questions de dépendance plus larges, générez un graphique et inspectez-le visuellement :
systemd-analyze dot 'myapp.service' | dot -Tsvg > myapp-deps.svg
Sur un serveur sans Graphviz installé, la sortie texte est toujours utile car vous pouvez rechercher les noms d'unités et voir quelles arêtes systemd connaît. Pour les problèmes de chronométrage du démarrage, systemd-analyze critical-chain myapp.service est souvent plus facile à lire qu'un graphique complet.
3. Détecter les Conflits et les Problèmes d'Ordonnancement
Les conflits de dépendance se manifestent souvent par des services qui échouent parce qu'ils ont été démarrés trop tôt ou qui s'arrêtent de manière inattendue.
Dépendances Circulaires : C'est le conflit le plus dangereux, où l'Unité A nécessite B, et l'Unité B nécessite A. Systemd tente de résoudre cela mais aboutit souvent à ce qu'une ou les deux unités restent dans un état failed ou activating indéfiniment.
Pour trouver des problèmes potentiels couvrant l'ensemble du système, vous pouvez rechercher dans les journaux des messages d'échec spécifiques liés à l'ordonnancement :
journalctl -b | grep -E "failed|refused to start|dependency was not satisfied"
Exécutez également systemd-analyze verify sur les unités personnalisées avant de supposer que le comportement d'exécution est le problème :
systemd-analyze verify /etc/systemd/system/myapp.service
Cela peut signaler des cycles d'ordonnancement, des directives inconnues et des références d'unités invalides dès le début. Cela ne prouvera pas que votre conception est correcte, mais cela peut vous éviter de poursuivre une faute de frappe comme s'il s'agissait d'un problème de dépendance complexe.
Résoudre les Problèmes de Dépendance Courants
Une fois identifiés, les problèmes de dépendance peuvent être résolus en ajustant les directives dans les fichiers d'unité concernés.
Scénario 1 : Le Service Démarre Avant que son Prérequis ne Soit Prêt
Symptôme : Les journaux de votre application montrent des erreurs de connexion à la base de données, mais postgresql.service apparaît active dans systemctl status.
Diagnostic : Il peut manquer After=postgresql.service au service, ou PostgreSQL peut être actif avant que la base de données, le socket, les identifiants ou le schéma spécifiques dont votre application a besoin ne soient prêts. Systemd peut ordonner les unités, mais il ne peut pas comprendre automatiquement toutes les conditions de préparation au niveau de l'application.
Correctif : Commencez par la relation d'unité simple :
[Unit]
Requires=postgresql.service
After=postgresql.service
Si l'application est toujours en concurrence avec la base de données, résolvez le problème de préparation à la bonne couche. Certains services prennent en charge Type=notify et ne signalent qu'ils sont prêts après l'initialisation. Certaines applications ont besoin d'une logique de nouvelle tentative car les dépendances peuvent redémarrer à tout moment, pas seulement au démarrage. Pour une vérification locale étroite, une commande ExecStartPre= peut être raisonnable :
[Service]
ExecStartPre=/usr/bin/pg_isready -q -h 127.0.0.1 -p 5432
ExecStart=/usr/local/bin/myapp
Utilisez ce genre de pré-vérification avec parcimonie. Si elle se transforme en un script shell avec des sleep et des boucles, l'application a probablement besoin d'un comportement de nouvelle tentative approprié à la place.
Évitez sleep 30 comme correctif de dépendance. Cela peut masquer la course sur une machine de développement calme et échouer à nouveau sur un stockage plus lent, une VM occupée ou un hôte attendant DNS. Une directive d'ordonnancement réelle, une notification de préparation, une activation de socket ou une boucle de nouvelle tentative d'application vous donne une raison pour laquelle le service est prêt au lieu d'un espoir que suffisamment de temps s'est écoulé.
Scénario 2 : Ordre de Démarrage/Arrêt Conflictuel
Symptôme : L'arrêt du système provoque le blocage ou l'échec brutal de processus critiques.
Diagnostic : Cela indique souvent une mauvaise utilisation de BindsTo= ou une interaction complexe entre les directives Before= et After= dans des services frères.
Correctif : Passez en revue les services qui sont frères (par exemple, les services démarrés par la même cible). Assurez-vous que si le Service A doit s'exécuter pendant que le Service B est en cours d'exécution, vous utilisez BindsTo= ou Requires=. Si le Service A doit terminer ses tâches avant que le Service B ne commence le nettoyage, vérifiez que l'ordre After= est correct.
Rappelez-vous que l'ordre d'arrêt est l'inverse de l'ordre de démarrage. Si app.service a After=database.service, alors à l'arrêt, systemd arrête app.service avant database.service. C'est généralement ce que vous voulez : l'application arrête d'accepter du travail avant que la base de données ne disparaisse. De nombreux bugs d'arrêt proviennent d'un ordre de démarrage manquant, pas d'un paramètre d'arrêt séparé.
Scénario 3 : Supprimer les Dépendances Inutiles
Symptôme : Le démarrage du système est lent car des services inutiles sont inclus dans la chaîne de démarrage.
Diagnostic : Vous avez peut-être utilisé Requires= alors qu'une simple connexion optionnelle était nécessaire.
Correctif : Remplacez Requires= par Wants=. Si le service n'a pas absolument besoin de la dépendance pour fonctionner, Wants= permet au système de continuer même si la dépendance échoue ou est masquée.
# Avant (Trop Strict)
Requires=optional_logging.service
# Après (Meilleur)
Wants=optional_logging.service
After=optional_logging.service
Ceci est particulièrement utile pour les agents de surveillance, les exportateurs de métriques, les assistants sidecar et les caches locaux optionnels. Si le service principal peut effectuer un travail utile sans eux, Requires= transforme une panne partielle en une panne complète. Utilisez des dépendances strictes pour les choses qui sont vraiment requises : une base de données locale pour une application qui ne peut pas démarrer sans elle, un point de montage qui contient les données de l'application, ou une unité de socket qui est le seul chemin d'activation pris en charge.
Scénario 4 : Un Montage ou un Périphérique n'Est Pas Prêt
Symptôme : Un service échoue au démarrage avec No such file or directory, mais le chemin existe après votre connexion. Cela se produit souvent avec des services qui lisent depuis /mnt/data, /srv/app, des disques amovibles, des volumes cryptés ou des systèmes de fichiers réseau.
Diagnostic : Le service démarre avant que l'unité de montage ne soit active, ou le montage est optionnel et a échoué sans arrêter le service.
Correctif : Trouvez le nom de l'unité de montage :
systemd-escape -p --suffix=mount /mnt/data
Pour /mnt/data, cela produit généralement mnt-data.mount. Ordonnez ensuite votre service après lui et exigez-le si le service ne peut pas fonctionner sans les données :
[Unit]
Requires=mnt-data.mount
After=mnt-data.mount
Si le montage provient de /etc/fstab, des options telles que nofail, x-systemd.automount et _netdev affectent le comportement au démarrage. N'ajoutez pas de Requires= dur à un montage optionnel à moins que vous ne vouliez vraiment que cet échec de montage bloque le service.
Scénario 5 : Une Cible Inclut Trop de Choses
Symptôme : L'activation d'un service semble démarrer des unités sans rapport, ou la désactivation d'un service ne l'empêche pas d'apparaître au démarrage.
Diagnostic : Le service peut être inclus par une cible via [Install] WantedBy=..., par Wants= d'un autre service, ou par une unité de socket, de minuterie, de chemin ou de montage. enable crée des liens symboliques pour l'activation au démarrage ; ce n'est pas la seule façon dont une unité peut démarrer.
Correctif : Inspectez à la fois l'unité et les dépendances inverses :
systemctl cat myapp.service
systemctl list-dependencies --reverse myapp.service
systemctl list-timers --all | grep myapp
systemctl list-sockets --all | grep myapp
Si une unité de socket active le service, désactiver uniquement myapp.service peut ne pas suffire. Vous devrez peut-être désactiver ou masquer myapp.socket aussi, selon le comportement souhaité.
Appliquer les Modifications et Recharger
Chaque fois que vous modifiez un fichier d'unité, vous devez demander à systemd de recharger sa configuration avant de tester les modifications.
# 1. Recharger la configuration du gestionnaire systemd
sudo systemctl daemon-reload
# 2. Redémarrer le service concerné
sudo systemctl restart myapp.service
# 3. Vérifier l'état
systemctl status myapp.service
Une Petite Liste de Vérification Mentale
Lorsqu'un problème de dépendance semble embrouillé, réduisez-le à quelques vérifications simples.
D'abord, demandez-vous si l'autre unité doit être démarrée du tout. Si oui, utilisez Wants= ou Requires=. Si le service actuel peut fonctionner sans elle, préférez Wants=. Si l'échec de cette unité signifie que ce service doit échouer, utilisez Requires=.
Deuxièmement, demandez-vous si l'ordre de démarrage est important. Si oui, ajoutez After= ou Before=. N'attendez pas des directives de dépendance qu'elles gèrent l'ordonnancement par elles-mêmes.
Troisièmement, demandez-vous si le couplage de durée de vie après le démarrage est important. Si l'arrêt d'une unité doit arrêter l'autre, regardez BindsTo= ou PartOf=. Ne les utilisez pas à la légère ; ils peuvent faire cascader les redémarrages plus loin que prévu.
Enfin, inspectez ce que systemd a réellement chargé :
systemctl cat myapp.service
systemctl show myapp.service -p Wants -p Requires -p After -p Before -p Conflicts
Cette sortie est souvent plus utile que le fichier que vous pensez avoir modifié. Les drop-ins, les unités du fournisseur, les unités générées, les sockets, les minuteries et les cibles peuvent tous modifier le comportement final.