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:

  1. Requires=mariadb.service: Garantiert, dass mariadb.service für die Funktion von my_app.service laufen muss. Wenn mariadb.service fehlschlägt, wird my_app.service gestoppt.
  2. Wants=other-optional-service.service: Versucht, other-optional-service.service zu starten, aber my_app.service wird fortgesetzt, selbst wenn es fehlschlägt.
  3. After=network.target mariadb.service: Ordnet my_app.service nach 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, dass network.target "das Netzwerk ist nutzbar" bedeutet.
  4. WantedBy=multi-user.target: Wenn aktiviert (systemctl enable my_app.service), fügt diese Direktive einen symbolischen Link hinzu, sodass my_app.service gestartet wird, wenn das System den Zustand multi-user.target erreicht.

Fortgeschrittene Überlegungen und Best Practices

  • WantedBy vs. RequiredBy: Ähnlich wie Wants vs. Requires ist WantedBy eine schwache und RequiredBy eine starke Anforderung. Die meisten Dienste verwenden WantedBy=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 Sie ConditionPathExists=, 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.