Heimserver & Self-Hosting

systemd-Abhängigkeiten für einen Home-Server - Anforderungen und Reihenfolge trennen

systemd-Abhängigkeiten für einen Home-Server - Anforderungen und Reihenfolge trennen

Ein Dienst soll nach einem anderen Dienst starten. Das klingt nach einer einzigen Regel, doch systemd versteht darunter mindestens zwei getrennte Fragen: Soll das Starten der ersten Unit auch die zweite starten, und welche soll zuerst starten, wenn beide gestartet werden? Viele schwer verständliche Unit-Dateien beantworten nur eine dieser Fragen.

Dieser Unterschied ist auf einem kleinen Home-Server wichtig. Ein Webprozess kann eine lokale Datenbank verwenden, ein Indexierungsjob kann ein eingehängtes Verzeichnis benötigen oder ein Backup-Job kann einen entfernten Rechner kontaktieren. After= kann den Boot-Graphen geordnet aussehen lassen, ohne die benötigte Unit in die Transaktion aufzunehmen. Requires= kann beide Units einbeziehen, sie aber gleichzeitig starten lassen. Keine der beiden Direktiven bedeutet für sich genommen: „Warte, bis die Anwendung funktionsfähig ist.“

Dieser Artikel entwickelt ein bewusst enges Denkmodell für die häufigen Direktiven Wants=, Requires=, After= und Before=. Er zeigt außerdem, wo dieses Modell nicht mehr ausreicht.

Zwei Achsen statt einer Abhängigkeitsleiter

Am klarsten ist es, Anforderung und Reihenfolge voneinander zu trennen. Das Handbuch systemd.unit beschreibt sie als unabhängige, orthogonale Beziehungen.

  • Wants= und Requires= beantworten, ob eine weitere Unit in dieselbe Starttransaktion aufgenommen werden soll.
  • After= und Before= beantworten, wie Jobs geordnet werden sollen, wenn beide Units an der Transaktion beteiligt sind.

Man kann sich eine Werkstatt statt einer Warteschlange vorstellen. Eine Anforderungsdirektive setzt eine weitere Person auf die Anwesenheitsliste. Eine Ordnungsdirektive legt fest, wer einen Startschritt abschließen muss, bevor jemand anderes beginnt. Ein Name im Zeitplan lädt die Person nicht ein, und eine Einladung weist keinen Platz im Zeitplan zu.

Daraus ergeben sich vier nützliche Fälle:

Konfiguration in app.serviceAusgedrückte Bedeutung
After=store.serviceWenn beide Units starten, wird die App nach dem Store eingeordnet. Der Store wird dadurch nicht einbezogen.
Wants=store.serviceEs wird auch versucht, den Store zu starten, jedoch ohne eine Reihenfolge festzulegen.
Wants=store.service plus After=store.serviceEs wird versucht, beide zu starten und die App nach dem Store einzuordnen.
Requires=store.service plus After=store.serviceBeide werden mit einer stärkeren Anforderung gestartet; die App startet nicht, wenn die geordnete Store-Aktivierung fehlschlägt.

Wants und Requires unterscheiden sich in ihrer Fehlerpolitik

Wants= ist die schwächere Beziehung. Wenn die konfigurierende Unit startet, versucht systemd auch, die gewünschten Units zu starten. Kann eine gewünschte Unit nicht hinzugefügt werden oder schlägt ihr Start fehl, wird dadurch nicht die gesamte Transaktion ungültig. Die Upstream-Dokumentation empfiehlt diese Form in vielen Fällen, um den Start einer Unit mit dem Start einer anderen zu verbinden.

Requires= formuliert eine stärkere Anforderung. Beim Start der konfigurierenden Unit wird auch die erforderliche Unit aktiviert. Zusammen mit After= verhindert eine fehlgeschlagene Aktivierung der erforderlichen Unit den Start der abhängigen Unit. Wird die erforderliche Unit ausdrücklich gestoppt oder neu gestartet, wird diese Aktion ebenfalls an die anfordernde Unit weitergegeben.

Das ist dennoch kein dauerhafter Zustandsvertrag. Das Handbuch nennt Fälle, in denen eine erforderliche Unit später inaktiv werden kann, ohne die anfordernde Unit automatisch zu stoppen. Dazu gehören unter anderem ein sauber beendeter Dienst und eine nicht erfüllte Unit-Bedingung. Stärkere Beziehungen wie BindsTo= existieren, doch ihr pauschaler Einsatz kann eine Installation fragil machen. Die praktische Frage lautet nicht „Welche Direktive ist am stärksten?“, sondern „Muss dieser Dienst zurückgehalten werden, wenn genau diese Aktivierung fehlschlägt?“

Bei einem optionalen lokalen Metrik-Collector kann Wants= den nützlichen Dienst verfügbar halten, wenn die Metriken ausfallen. Bei einer Anwendung, die ohne einen lokalen Store nicht einmal initialisiert werden kann, können Requires= und After= die beabsichtigte Fehlerpolitik beschreiben. Der Unit-Graph sollte eine tatsächliche betriebliche Anforderung ausdrücken, nicht die Sorge vor jeder denkbaren Störung.

After und Before ordnen nur beteiligte Units

Wenn web.service die Direktive After=store.service enthält, verzögert systemd den Start des Webdienstes, bis der Startjob des Stores abgeschlossen ist, sofern beide Jobs vorhanden sind. Die umgekehrte Angabe Before=web.service in der Store-Unit beschreibt dieselbe Ordnungskante von der anderen Seite.

Die Reihenfolge wirkt sich auch auf das Herunterfahren aus. Werden zwei Units mit einer Ordnungsbeziehung gemeinsam gestoppt, wendet systemd die umgekehrte Reihenfolge an. Eine Unit, die mit After= hinter einer anderen liegt, wird vor dieser anderen Unit gestoppt. So kann ein Client seine Arbeit beenden, bevor der Provider verschwindet.

Bei Targets gibt es eine Besonderheit. Laut systemd.target ergänzt ein Target seine eigenen konfigurierten Beziehungen vom Typ Wants= und Requires= normalerweise um After=. Umgekehrt gilt das nicht automatisch: Ein Dienst mit Wants=some.target sollte weiterhin eine ausdrückliche Ordnungsdirektive hinzufügen, wenn er sie benötigt. Dieses Standardverhalten darf nicht zu „Anforderungen implizieren immer eine Reihenfolge“ verallgemeinert werden.

Ein minimales Unit-Paar

Betrachten wir zwei absichtlich künstliche Dienste. Sie führen sleep statt einer echten Anwendung aus. Das Beispiel zeigt dadurch die Unit-Semantik, ohne vorzugeben, eine Datenbank oder einen Web-Stack zu konfigurieren.

# dependency-demo-store.service
[Unit]
Description=Dependency demo store

[Service]
Type=exec
ExecStart=/usr/bin/sleep infinity
# dependency-demo-web.service
[Unit]
Description=Dependency demo web process
Requires=dependency-demo-store.service
After=dependency-demo-store.service

[Service]
Type=exec
ExecStart=/usr/bin/sleep infinity

[Install]
WantedBy=multi-user.target

Der Start von dependency-demo-web.service bezieht den Store wegen Requires= ein. Die Zeile After= ordnet den Webprozess getrennt davon hinter der Store-Aktivierung ein. Ohne Requires= bliebe nur die Ordnungsbeziehung; der alleinige Start der Web-Unit würde den Store nicht zwingend starten. Ohne After= bliebe die Anforderung bestehen, aber beide Startjobs könnten ohne diese Reihenfolge ablaufen.

Das Beispiel lässt sich vor der Installation prüfen:

systemd-analyze verify \
  dependency-demo-store.service \
  dependency-demo-web.service

systemd-analyze verify hilft dabei, unbekannte Direktiven, fehlende Abhängigkeiten und andere Probleme in Unit-Dateien zu finden. Keine Ausgabe beweist jedoch nicht, dass die Beziehung auf Anwendungsebene richtig ist. Das Werkzeug kann Graph und Syntax leichter validieren als die Absicht des Betreibers.

Geordnet bedeutet nicht betriebsbereit

Die wichtigste Einschränkung steckt im Ausdruck „vollständig gestartet“. Bei einer Service-Unit hängt dieser Zeitpunkt von Type= ab. Das Handbuch systemd.service erklärt, dass der Standardtyp Type=simple unmittelbar nach dem Erzeugen des Prozesses als gestartet gelten kann, sogar bevor das Programm erfolgreich ausgeführt wurde. Type=exec wartet, bis die Ausführung des Dienstprogramms erfolgreich ist, weiß aber weiterhin nicht, ob ein HTTP-Listener Anfragen annimmt oder eine Datenbank ihre Wiederherstellung abgeschlossen hat.

Ein Dienst mit nativer Unterstützung für systemd-Benachrichtigungen kann Type=notify verwenden und nach Abschluss seiner Initialisierung READY=1 senden. Andere Entwürfe vermeiden das Race durch Socket- oder Bus-Aktivierung. Wenn beides nicht verfügbar ist, benötigt die abhängige Anwendung häufig Wiederholungsversuche mit begrenztem Backoff. Ein Sleep in ExecStartPre= schätzt lediglich die benötigte Zeit; bei einem schnellen Start verschwendet er Zeit und bei einem langsamen Start kann er trotzdem scheitern.

Darum sollte etwa After=mariadb.service nicht als „Die Datenbank wird die Abfrage dieser Anwendung annehmen“ umschrieben werden. Es bedeutet, dass systemd die von MariaDB definierte Aktivierungsgrenze beobachtet hat, bevor der abhängige Start beginnt. Ob diese Grenze genügt, muss anhand des tatsächlichen Provider-Dienstes und des Client-Verhaltens geprüft werden.

Das Netzwerk zeigt, warum Synchronisationspunkte lokale Zusagen sind

Netzwerk-Targets machen diese Grenze besonders deutlich. Die systemd-Hinweise zur Netzwerksynchronisation erklären, dass network.target nur den Start des Netzwerkverwaltungs-Stacks anzeigt; konfigurierte Konnektivität wird dadurch nicht definiert. network-online.target wartet aktiv auf die Definition von „online“ durch den Netzwerkmanager, doch diese Definition hängt von lokaler Software und Konfiguration ab.

Selbst das Erreichen des Online-Targets ist ein vergangenes Ereignis und keine Garantie dafür, dass DNS, ein entfernter Host oder das Internet erreichbar bleiben. Ein Netzwerk-Client, der nach dem Boot wichtig ist, muss weiterhin mit verlorener Konnektivität umgehen. network-online.target einzubeziehen kann für einen Client sinnvoll sein, der ohne anfängliche Netzwerkkonfiguration nicht starten kann. Bei jedem netzwerkbezogenen Dienst eingesetzt, kann es jedoch Verzögerungen erzeugen, ohne dauerhafte Zuverlässigkeit zu schaffen.

WantedBy betrifft die Installation, nicht die Betriebsbereitschaft

Im Beispiel steht WantedBy=multi-user.target unter [Install]. systemd liest diesen Abschnitt während einer normalen Aktivierung nicht als Laufzeitanweisung. Stattdessen verwendet systemctl enable ihn, um einen symbolischen Link in einem .wants/-Verzeichnis anzulegen. Dadurch bezieht das Target den Dienst bei einem späteren Boot oder einer späteren Target-Aktivierung ein.

Drei Vorgänge werden daher leicht verwechselt:

  • systemctl enable example.service ändert die Enablement-Links; der Dienst wird nicht gestartet, sofern nicht zusätzlich --now angegeben wird.
  • systemctl start example.service aktiviert ihn jetzt; dadurch wird er nicht zwingend für einen späteren Boot aktiviert.
  • WantedBy= beschreibt die beim Enablement anzulegenden Links; es ersetzt weder After= noch ein Protokoll für die Anwendungsbereitschaft.

Den Graphen vor Änderungen untersuchen

Die wirksamen Beziehungen einer Unit können aus ihrer Hauptdatei, Drop-ins, Enablement-Links, Standardwerten und impliziten Abhängigkeiten stammen. Nur eine Datei zu lesen kann daher unvollständig sein. Diese Befehle zeigen unterschiedliche Teile des Ergebnisses:

systemctl cat dependency-demo-web.service

systemctl show dependency-demo-web.service \
  -p Wants -p Requires -p After -p Before

systemctl list-dependencies dependency-demo-web.service

systemd-analyze critical-chain dependency-demo-web.service

systemctl cat zeigt die Unit und ihre Drop-ins. systemctl show zeigt ausgewählte wirksame Eigenschaften. list-dependencies hilft zu untersuchen, was einbezogen wird, während critical-chain die zeitkritische Reihenfolge betrachtet. Keiner dieser Befehle ersetzt Tests von Fehlerfällen: Die abhängige Unit sollte bei gestopptem Provider gestartet, die Provider-Aktivierung in einer Testumgebung kontrolliert zum Scheitern gebracht und das Verhalten beim Herunterfahren oder Neustart beobachtet werden. Produktionsdienste sollten nicht allein für ein Tutorial absichtlich beschädigt werden.

Den kleinsten wahrheitsgemäßen Graphen bevorzugen

Mehr Kanten machen einen Dienst nicht automatisch zuverlässiger. Sie können parallel mögliche Arbeit serialisieren, den Bootvorgang von optionalen Komponenten abhängig machen oder einen Fehler auf ansonsten nützliche Dienste ausbreiten. Die Upstream-Dokumentation von systemd empfiehlt ausdrücklich, Abhängigkeiten sparsam einzusetzen und Socket- oder Bus-Aktivierung in Betracht zu ziehen, wenn diese Entwürfe passen.

Eine praktische Prüfung kann vier Fragen stellen:

  1. Soll die Aktivierung dieser Unit auch die andere aktivieren? Falls ja, ist eine Anforderungsbeziehung nötig.
  2. Soll diese Unit weiterhin starten, wenn die Aktivierung der anderen fehlschlägt? Diese Frage trennt viele Fälle für Wants= von Fällen für Requires=.
  3. Ist die Start- oder Stopp-Reihenfolge bedeutsam, wenn beide beteiligt sind? Falls ja, sollte After= oder Before= ausdrücklich ergänzt werden.
  4. Welches beobachtbare Ereignis bedeutet, dass der Provider wirklich bereit ist, und bildet sein Service-Type= dieses Ereignis ab? Falls nicht, reicht die Reihenfolge allein nicht aus.

Die nützliche Erkenntnis ist kein magisches Direktivenpaar. Entscheidend ist die Gewohnheit, eine Zusage genau zu benennen. Anforderung, Reihenfolge, Enablement und Betriebsbereitschaft sind unterschiedliche Zusagen. Eine Unit-Datei lässt sich leichter verstehen, wenn jede Zeile nur das verspricht, was systemd tatsächlich einhalten kann.

References