Fehlerbehebung bei Systemd: Abhängigkeiten und Reihenfolgedirektiven verstehen
Beheben Sie Systemd-Abhängigkeits- und Reihenfolgeprobleme durch korrekte Verwendung von Requires, Wants, After, Before und Diagnosebefehlen.
Fehlerbehebung bei Systemd: Abhängigkeiten und Reihenfolgedirektiven verstehen
Die meisten verwirrenden Systemd-Abhängigkeitsfehler entstehen durch die Verwechslung zweier getrennter Konzepte: Abhängigkeit und Reihenfolge. Requires= und Wants= beantworten die Frage "Welche andere Einheit soll einbezogen werden?" After= und Before= beantworten die Frage "Wann soll diese Einheit relativ zu einer anderen starten?" Wenn Sie sich nur eines aus diesem Leitfaden merken, dann dies: After=postgresql.service startet PostgreSQL nicht für Sie. Es besagt lediglich, dass Ihre Einheit warten soll, bis der Startjob von PostgreSQL abgeschlossen ist, falls beide Einheiten gestartet werden.
Diese Unterscheidung ist die Ursache vieler Vorfälle, bei denen "es funktioniert, wenn ich es manuell starte, schlägt aber nach einem Neustart fehl". Eine Web-App startet, bevor der Datenbank-Socket Verbindungen akzeptiert. Ein Worker startet, bevor /mnt/jobs gemountet ist. Ein Dienst wartet auf network.target und schlägt dennoch fehl, weil noch keine IP-Adresse zugewiesen wurde. Dies sind keine exotischen Systemd-Probleme. Es sind gewöhnliche Startannahmen, die explizit gemacht werden müssen.
Kern-Abhängigkeitsdirektiven: Requires und Wants
Systemd verwendet zwei primäre Direktiven, um direkte Abhängigkeiten zwischen Einheiten zu definieren: Requires und Wants. Diese Direktiven werden im Abschnitt [Unit] einer Einheitendatei (z.B. einer .service-Datei) platziert.
Requires=
Die Direktive Requires= definiert eine starke Abhängigkeit. Wenn Einheit A Requires= Einheit B, startet systemd B, wenn A gestartet wird. Wenn B nicht gestartet werden kann, schlägt auch der Startjob von A fehl. Wenn B explizit gestoppt wird, während A läuft, wird A normalerweise ebenfalls gestoppt, da die erforderliche Beziehung nicht mehr besteht.
Beispiel:
Betrachten Sie einen Webanwendungsdienst (myapp.service), der kritisch von einem Datenbankdienst (mariadb.service) abhängt. Die Einheitendatei myapp.service könnte Folgendes enthalten:
[Unit]
Description=Meine Webanwendung
Requires=mariadb.service
[Service]
ExecStart=/usr/bin/myapp
[Install]
WantedBy=multi-user.target
In diesem Szenario wird systemd, wenn mariadb.service nicht startet oder manuell gestoppt wird, auch myapp.service stoppen. Wenn Sie versuchen, myapp.service zu starten und mariadb.service nicht läuft, wird systemd versuchen, zuerst mariadb.service zu starten. Wenn mariadb.service fehlschlägt, wird myapp.service nicht starten.
Wants=
Die Direktive Wants= definiert eine schwächere, optionale Abhängigkeit. Wenn Einheit A Wants= Einheit B, wird systemd versuchen, Einheit B zu starten, wenn Einheit A gestartet wird, aber Einheit A wird trotzdem aktiviert, selbst wenn Einheit B nicht startet oder nicht läuft. Dies ist nützlich für Dienste, die von einem anderen Dienst profitieren, aber unabhängig funktionieren können, möglicherweise mit reduzierten Funktionen oder einer Warnung.
Beispiel:
Angenommen, ein Überwachungsagent (monitoring-agent.service) kann ohne einen bestimmten Protokollierungsdienst (app-logger.service) ausgeführt werden, hätte ihn aber idealerweise verfügbar. Die Einheitendatei monitoring-agent.service könnte wie folgt aussehen:
[Unit]
Description=Überwachungsagent
Wants=app-logger.service
[Service]
ExecStart=/usr/bin/monitoring-agent
[Install]
WantedBy=multi-user.target
Hier wird systemd versuchen, app-logger.service zu starten, wenn monitoring-agent.service aktiviert wird. Wenn jedoch app-logger.service nicht startet, wird monitoring-agent.service trotzdem erfolgreich starten.
Requires= vs. Wants=
Requires=: Starke Abhängigkeit. Wenn die erforderliche Einheit fehlschlägt, schlägt die abhängige Einheit fehl oder stoppt.Wants=: Schwache Abhängigkeit. Die abhängige Einheit versucht, die gewünschte Einheit zu starten, fährt aber fort, selbst wenn sie fehlschlägt.
Es ist wichtig zu beachten, dass Requires= implizit Wants= beinhaltet. Wenn eine Einheit eine andere benötigt, wünscht sie diese auch implizit.
Reihenfolgedirektiven: After und Before
Während Requires und Wants definieren, was laufen muss, definieren After und Before, wann Einheiten relativ zueinander gestartet werden sollen. Diese Direktiven steuern die Abfolge der Vorgänge während des Systemstartprozesses oder wenn Einheiten bei Bedarf aktiviert werden. Sie werden oft in Verbindung mit Abhängigkeitsdirektiven verwendet.
After=
Die Direktive After= gibt an, dass der Startjob der aktuellen Einheit nach den Startjobs der aufgelisteten Einheiten angeordnet werden soll. Sie zieht diese Einheiten nicht von selbst in die Transaktion. Sie beweist auch nicht, dass die Abhängigkeit logisch für Ihre Anwendung bereit ist; sie verwendet nur die Sichtweise von systemd auf den Aktivierungszustand der Einheit.
Beispiel:
Ein netzwerkabhängiger Dienst (custom-network-app.service) sollte erst starten, nachdem das Netzwerk vollständig konfiguriert ist. Dies wird normalerweise dadurch gehandhabt, dass sichergestellt wird, dass er nach dem Netzwerkziel (network.target) startet.
[Unit]
Description=Benutzerdefinierte Netzwerkanwendung
Requires=network.target
After=network.target
[Service]
ExecStart=/usr/bin/custom-network-app
[Install]
WantedBy=multi-user.target
In dieser Konfiguration ordnet systemd custom-network-app.service nach network.target an, wenn beide Teil derselben Transaktion sind. Für Dienste, die eine Adresse, DNS oder eine Route zu einem anderen Host benötigen, ist network-online.target oft näher an der Absicht, aber nur, wenn der Wait-Online-Dienst der Distribution aktiviert und korrekt konfiguriert ist.
Before=
Die Direktive Before= gibt an, dass die aktuelle Einheit vor den in Before= aufgeführten Einheiten gestartet werden soll. Dies ist nützlich für Dienste, die während des Herunterfahrens nach anderen gestoppt oder vor bestimmten Diensten gestartet werden müssen, um eine Umgebung für sie bereitzustellen.
Beispiel:
Stellen Sie sich ein Szenario vor, in dem ein Mailserver (postfix.service) laufen muss, bevor benutzerorientierte Dienste, die E-Mails senden könnten, gestartet werden. Sie könnten Before= verwenden, um sicherzustellen, dass postfix.service früh startet.
[Unit]
Description=Postfix Mail Transfer Agent
# ... andere Direktiven wie Conflicts=
Before=user-session.target
[Service]
ExecStart=/usr/lib/postfix/master
[Install]
WantedBy=multi-user.target
Dieses Setup versucht, postfix.service vor allem zu starten, was Teil von user-session.target ist, mit seinem Start beginnt. Ähnlich würde postfix.service während des Herunterfahrens zu den letzten gehören, die gestoppt werden, wenn es ein entsprechendes After=user-session.target hat.
After= vs. Before=
After=: Garantiert, dass die aufgelisteten Einheiten vor dem Start der aktuellen Einheit aktiv sind.Before=: Garantiert, dass die aktuelle Einheit vor den aufgelisteten Einheiten startet.
After= und Before= ergänzen sich. Wenn Einheit A Before=B sagt, ist die Reihenfolge A zuerst, dann B. Wenn Einheit B After=A sagt, ist das Ergebnis dasselbe. Normalerweise müssen Sie die Beziehung nur in einer Einheitendatei ausdrücken. Wenn Sie Ihren eigenen Dienst bearbeiten, ist es normalerweise klarer zu sagen, wonach Ihr Dienst kommen muss, da dies die Argumentation lokal hält.
Kombinieren von Direktiven für robuste Konfigurationen
In realen Szenarien kombinieren Sie diese Direktiven oft, um komplexe Abhängigkeitsgraphen zu erstellen. Das multi-user.target ist ein häufiges Ziel, das signalisiert, dass das System für den Mehrbenutzerbetrieb bereit ist. Viele Dienste sind so konfiguriert, dass sie WantedBy=multi-user.target und After=multi-user.target sind (oder genauer After=basic.target und After=getty.target usw., von denen multi-user.target abhängt).
Ein gängiges Muster:
Ein Dienst, der eine Datenbank benötigt und nach der Netzwerkkonfiguration starten soll, könnte wie folgt aussehen:
[Unit]
Description=Mein Anwendungsdienst
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
Erklärung des Musters:
Requires=mariadb.service: Garantiert, dassmariadb.servicefür die Funktion vonmy_app.servicelaufen muss. Wennmariadb.servicefehlschlägt, wirdmy_app.servicegestoppt.Wants=other-optional-service.service: Versucht,other-optional-service.servicezu starten, abermy_app.servicewird fortgesetzt, selbst wenn es fehlschlägt.After=network.target mariadb.service: Ordnetmy_app.servicenach dem Netzwerkziel und dem MariaDB-Startjob an. Wenn die App eine TCP-Verbindung zu einer Datenbank auf einem anderen Host herstellen muss, verwenden Sie das entsprechende Network-On-Setup anstatt anzunehmen, dassnetwork.target"das Netzwerk ist nutzbar" bedeutet.WantedBy=multi-user.target: Wenn aktiviert (systemctl enable my_app.service), fügt diese Direktive einen symbolischen Link hinzu, sodassmy_app.servicegestartet wird, wenn das System den Zustandmulti-user.targeterreicht.
Fortgeschrittene Überlegungen und Best Practices
WantedByvs.RequiredBy: Ähnlich wieWantsvs.RequiresistWantedByeine schwache undRequiredByeine starke Anforderung. Die meisten Dienste verwendenWantedBy=multi-user.target.Conflicts=: Diese Direktive gibt Einheiten an, die nicht gleichzeitig mit der aktuellen Einheit laufen sollen. Wenn die aktuelle Einheit gestartet wird, werden widersprüchliche Einheiten gestoppt und umgekehrt.- Transitive Abhängigkeiten: Abhängigkeiten sind transitiv. Wenn A B benötigt und B C benötigt, dann benötigt A indirekt C. Systemd verarbeitet diese Ketten automatisch.
Condition*=-Direktiven: Verwenden SieConditionPathExists=,ConditionFileNotEmpty=,ConditionVirtualization=usw., um die Einheitenaktivierung basierend auf dem Systemzustand bedingt zu machen und so die Robustheit weiter zu erhöhen.- Verwenden Sie
systemctl list-dependencies <unit>: Dieser Befehl ist unverzichtbar, um den Abhängigkeitsbaum einer Einheit zu visualisieren, einschließlich direkter und indirekter Abhängigkeiten. - Verwenden Sie
systemctl status <unit>: Überprüfen Sie immer den Status Ihres Dienstes nach Konfigurationsänderungen. Er zeigt oft Gründe für Fehler an, einschließlich Abhängigkeitsproblemen. - Vermeiden Sie zirkuläre Abhängigkeiten: Obwohl systemd versucht, sie aufzulösen, können direkte zirkuläre Abhängigkeiten (
A Requires B,B Requires A) zu Startschleifen oder Fehlern führen. Entwerfen Sie Ihre Abhängigkeiten sorgfältig, um dies zu vermeiden.
Ein praktischer Debugging-Durchlauf
Wenn ein Abhängigkeitsfehler auftritt, beginnen Sie damit, sich die Transaktion anzusehen, die systemd tatsächlich erstellt hat:
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 sagt Ihnen, ob systemd die Einheit nicht starten konnte, sie später beendet hat oder sie als aktiv betrachtet, obwohl die Anwendung nicht gesund ist. journalctl -b hält Sie innerhalb des aktuellen Bootvorgangs, was wichtig ist, da Abhängigkeitsprobleme oft nur beim Booten auftreten. systemctl show ist direkt, aber nützlich: Es zeigt die endgültigen zusammengeführten Einheiteneigenschaften nach Anwendung von Drop-Ins, Herstellerdateien und generierten Abhängigkeiten.
Wenn Sie nicht sicher sind, woher eine Abhängigkeit stammt, überprüfen Sie die vollständige Einheit:
systemctl cat my_app.service
Dies zeigt die paketierte Einheit und alle Überschreibungsdateien unter /etc/systemd/system/my_app.service.d/. Ich habe in Produktionsumgebungen Dienste gesehen, bei denen die Basiseinheit korrekt war, aber eine alte Überschreibung noch After=mysql.service aus einer früheren Migration enthielt. Der Dienst wartete auf eine Einheit, die nicht mehr existierte, und die Protokolle ließen es so aussehen, als ob die Anwendung defekt wäre.
Für Fragen zum Startzeitpunkt verwenden Sie:
systemd-analyze critical-chain my_app.service
systemd-analyze blame
critical-chain ist besser, als auf Zeitstempel zu starren, da es zeigt, welche Einheiten den Pfad zu Ihrem Dienst verzögert haben. blame kann irreführend sein, wenn Sie es als Rangliste "schlechter" Dienste behandeln, ist aber hilfreich, wenn eine Abhängigkeit viel länger dauert als erwartet.
Muster, die in der Produktion Bestand haben
Für eine lokale Datenbankabhängigkeit ist dies ein vernünftiger Ausgangspunkt:
[Unit]
Description=API-Dienst
Requires=postgresql.service
After=postgresql.service
[Service]
ExecStart=/usr/local/bin/api
User=api
Restart=on-failure
[Install]
WantedBy=multi-user.target
Das besagt, dass PostgreSQL mit der API gestartet werden soll und der API-Start nach PostgreSQL angeordnet werden soll. Es garantiert nicht, dass jede Migration abgeschlossen ist oder dass die Datenbank die genauen Anmeldeinformationen akzeptiert, die Ihre App verwendet. Wenn das wichtig ist, fügen Sie eine Bereitschaftsprüfung auf Anwendungsebene hinzu. Systemd kann Prozesse ordnen, aber es kann Ihren Schema-Zustand nicht verstehen, es sei denn, Sie bringen Ihrem Dienst bei, ihn zu überprüfen.
Für einen gemounteten Pfad bevorzugen Sie RequiresMountsFor= gegenüber der manuellen Benennung von Mount-Einheiten:
[Unit]
RequiresMountsFor=/srv/uploads
[Service]
ExecStart=/usr/local/bin/upload-worker
User=uploads
Systemd leitet die benötigten Mount-Einheiten für den Pfad ab. Dies ist einfacher zu warten, als sich zu merken, dass /srv/uploads auf srv-uploads.mount abgebildet wird.
Für optionale Helfer verwenden Sie Wants=:
[Unit]
Wants=metrics-agent.service
After=metrics-agent.service
Wenn der Metrik-Agent fehlschlägt, kann der Hauptdienst trotzdem starten. Das ist normalerweise das, was Sie für Protokollierungs-Seitenwagen, optionale Exporteure und lokale Benachrichtigungshelfer möchten. Verwenden Sie Requires= nicht nur, weil zwei Dienste verwandt sind. Verwenden Sie es nur, wenn der abhängige Dienst ohne die andere Einheit wirklich keine nützliche Arbeit leisten kann.
Für eng gekoppelte Dienste, die zusammen gestoppt werden sollen, sehen Sie sich BindsTo= und PartOf= an. BindsTo= ist stärker als Requires= und nützlich, wenn ein Dienst verschwinden soll, wenn die gebundene Einheit verschwindet, z.B. ein Dienst, der an eine bestimmte Geräteeinheit gebunden ist. PartOf= ist oft nützlich für Gruppen: Das Neustarten oder Stoppen der übergeordneten Einheit kann auf untergeordnete Einheiten übertragen werden. Dies sind keine Erstwahlen, aber sie lösen Probleme, die Requires= nicht sauber ausdrücken kann.
Häufige Fallstricke
Fügen Sie After=multi-user.target nicht zu einem normalen langlebigen Dienst hinzu, der mit WantedBy=multi-user.target aktiviert ist. Dies erzeugt oft eine seltsame Reihenfolge und sagt selten das, was der Autor beabsichtigt hat. Die meisten Dienste werden von multi-user.target einbezogen; sie müssen nicht starten, nachdem es bereits erreicht wurde.
Gehen Sie nicht davon aus, dass network.target "Internet ist erreichbar" bedeutet. Es ist ein Synchronisationspunkt für die Netzwerkverwaltung, kein Konnektivitätstest. Wenn Ihre Anwendung mit einer entfernten API spricht, fügen Sie trotzdem Wiederholungslogik innerhalb der Anwendung hinzu. Die Netzwerkreihung beim Booten reduziert Rauschen, kann Sie aber nicht vor DNS-Ausfällen, Routing-Änderungen oder einem Ausfall einer entfernten Abhängigkeit schützen.
Verstecken Sie keine langen sleeps in ExecStartPre=/bin/sleep 30, es sei denn, Sie haben keine bessere Option. sleeps machen Bootvorgänge langsamer, wenn die Abhängigkeit schnell bereit ist, und schlagen immer noch fehl, wenn die Abhängigkeit länger als erwartet braucht. Eine kleine Bereitschaftsschleife, die den tatsächlichen Socket, die Datei oder die API überprüft, ist normalerweise klarer.
Wenn Sie Abhängigkeitsdirektiven ändern, führen Sie systemctl daemon-reload aus, starten Sie den Dienst neu und überprüfen Sie das Journal vom selben Bootvorgang. Die schnellste Lösung ist normalerweise diejenige, die die genaue Annahme über die Reihenfolge beweist, die falsch war.