Warum systemd Ihren Dienst nicht neu startet
Restart= überwacht nur den Hauptprozess. Ein beendeter Kindprozess in derselben cgroup bleibt unsichtbar. Erfahren Sie, wie Type=, Limits und das Journal wirken.
Die kurze Antwort: systemd-Neustartregeln überwachen einen Prozess
systemd-Neustartregeln überwachen pro Unit einen Prozess: den Hauptprozess. Restart= liest ausschließlich den Exit-Status dieses Prozesses. Die Control Group einer Unit kann zwanzig Prozesse enthalten. Einer davon kann beendet werden, während die Unit active (running) bleibt, weil der Hauptprozess noch vorhanden ist. Aus Sicht von systemd ist nichts fehlgeschlagen. Daher wird auch nichts neu gestartet.
systemd kennt die anderen Prozesse durchaus. Beim Stoppen der Unit beendet systemd sie. Ihr Speicherverbrauch wird auf die Limits der Unit angerechnet. Die CPU-Quota der Unit gilt auch für sie. Außerdem zeigt systemd sie in systemctl status an. Ihren Exit-Status liest systemd jedoch nie. Die Neustartlogik und die cgroup sind zwei verschiedene Dinge. Der größte Teil dieses Leitfadens behandelt die Lücke zwischen beiden.
Was die cgroup enthält und was die Neustartlogik ausliest
Eine cgroup (control group) ist ein Kernelobjekt, das eine Gruppe von Prozessen verwaltet. Jede Service-Unit erhält eine eigene cgroup mit dem Namen der Unit. Ein Prozess kann sie nicht verlassen. Untergeordnete Prozesse übernehmen die cgroup ihres übergeordneten Prozesses, und ein Prozess ohne erhöhte Berechtigungen kann sich nicht selbst in eine andere cgroup verschieben. Deshalb kann systemd einen Daemon bereinigen, der sich zweimal forkt. Alte Init-Skripte konnten das nie zuverlässig.
Betrachten Sie beide Fakten direkt nebeneinander:
systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Restart -p RestartUSec myapp.servicesystemd-cgls listet jeden Prozess in der Unit auf. MainPID ist die einzige Zahl, die die Neustartrichtlinie ausliest. Wenn diese beiden Werte nicht mit Ihrem mentalen Modell übereinstimmen, liegt genau dort der Fehler. MainPID=0 ist schlimmer als eine falsche PID: Das bedeutet, dass systemd überhaupt nichts überwacht. Daher kann kein Restart=-Wert jemals auslösen.
Es gibt eine echte Ausnahme von der Regel für den Hauptprozess. Wenn der Out-of-Memory-Killer des Kernels einen beliebigen Prozess innerhalb der cgroup der Unit beendet, erkennt systemd dies, weil es die Datei memory.events der cgroup überwacht. OOMPolicy= legt fest, was als Nächstes geschieht. Der Standardwert ist stop: Die gesamte Unit wird gestoppt, das Ergebnis wird als oom-kill aufgezeichnet, und dies gilt als Fehler. Dadurch wird Restart=on-failure ausgelöst. Das Journal zeigt dies eindeutig.
myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Failed with result 'oom-kill'.Ein wegen Speichermangels beendeter untergeordneter Prozess beendet also auch die Unit. Derselbe untergeordnete Prozess, der durch einen Segmentation Fault beendet wird, beendet die Unit dagegen nicht. Wenn Sie Speicherlimits für eine Unit festlegen, lesen Sie wie MemoryMax und CPUQuota auf die cgroup einer Unit angewendet werden, bevor Sie die Neustartrichtlinie anpassen. Denn diese beiden Funktionen greifen genau hier ineinander und nirgendwo sonst.
Wie Type= den Hauptprozess bestimmt
Type= im Abschnitt [Service] betrifft nicht nur die Startreihenfolge. Damit wird festgelegt, welche PID (Prozess-ID) zu MainPID wird. Dadurch wird auch festgelegt, was Restart= sehen kann.
Type=simpleist der Standardwert. Der Prozess, den systemd ausExecStart=forkt, ist der Hauptprozess. systemd markiert die Unit sofort als gestartet, bevor bekannt ist, obexecüberhaupt funktioniert hat. Ein Tippfehler im Binärpfad führt daher zu einem erfolgreichen Start-Job, anschließend schlägtMain process exited, code=exited, status=203/EXECjedoch einen Moment später fehl.Type=execverhält sich wiesimple, wartet beim Start-Job jedoch, bisexecerfolgreich war. Dadurch wird der obige Tippfehler korrekt als Startfehler erkannt. Dafür ist systemd 240 oder neuer erforderlich. Diese Version ist in jeder unterstützten Distribution enthalten. Verwenden SieType=execbevorzugt gegenübersimple.Type=forkingerwartet, dass der Prozess ausExecStart=einen Hintergrund-Daemon forkt und anschließend beendet wird. systemd wartet, bis sich der übergeordnete Prozess beendet hat, und sucht dann nach dem eigentlichen Daemon. Geben SiePIDFile=an. Ohne diese Angabe funktioniertGuessMainPID=(standardmäßig aktiviert) nur, wenn genau ein Prozess in der Cgroup verbleibt. Bleiben zwei Prozesse zurück, bleibtMainPIDauf0.Type=notifybedeutet, dass der Dienstsd_notify(3)aufruft undREADY=1sendet, sobald er Netzwerkverkehr verarbeiten kann. Er kann außerdemMAINPID=senden, um systemd einen anderen zu überwachenden Prozess zu übergeben.NotifyAccess=ist standardmäßig aufmaingesetzt. Daher wird eine von einem untergeordneten Prozess gesendete Benachrichtigung ignoriert, und das Journal nennt die PID, von der sie stammt.Type=oneshotbesitzt keinen dauerhaft laufenden Hauptprozess. Die Unit wird inaktiv, sobaldExecStart=beendet ist, sofern Sie nichtRemainAfterExit=yessetzen.Restart=alwaysundRestart=on-successwerden hier mit der MeldungService has Restart= set to either always or on-success, which isn't allowed for Type=oneshot services. Refusing.abgelehnt. Die übrigen Werte, einschließlichon-failure, werden akzeptiert.
Zwei Type=forking-Fehler sollten Sie sich merken, weil jeder davon eine Unit hinterlässt, die ohne sichtbaren Grund defekt wirkt:
myapp.service: Can't open PID file /run/myapp.pid (yet?) after start: No such file or directory
myapp.service: New main PID 4711 does not belong to service, and PID file is not owned by root. Refusing.Der erste Fehler bedeutet, dass der Daemon seine PID-Datei an einer anderen Stelle schreibt oder sie erst nach dem Zeitpunkt erstellt, zu dem systemd danach sucht. Der zweite Fehler bedeutet, dass die PID-Datei einen Prozess außerhalb der Cgroup der Unit angibt. systemd übernimmt diesen Prozess nicht, weil eine beschreibbare PID-Datei andernfalls dazu verwendet werden könnte, systemd Signale an beliebige Prozesse auf dem Server zu senden.
Warum ein Wrapper-Skript das Beenden seiner Kindprozesse verbirgt
Dies ist die Konstellation, die zur Frage aus der Überschrift führt.
#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
waitDie Unit ist Type=simple. Der Hauptprozess ist daher die Shell. wait ohne Argumente kehrt erst zurück, wenn alle Kindprozesse beendet wurden. Beenden Sie den Worker, wartet die Shell weiterhin auf den Webprozess. Deshalb beendet sich die Shell nicht, MainPID kehrt nicht zurück und Restart= wird nie ausgewertet. Die cgroup enthält jetzt einen Prozess weniger, systemctl status gibt den verkürzten Prozessbaum aus und die Unit bleibt active (running). systemd überwacht den Prozessbaum nicht auf Änderungen.
Eine zweite Variante desselben Fehlers ist unauffälliger:
ExecStart=/bin/sh -c 'export APP_ENV=production; /usr/local/bin/myapp'Der Hauptprozess ist die Shell, nicht myapp. Bei systemctl stop sendet systemd SIGTERM an den Hauptprozess. Eine Shell, die auf einen Vordergrundprozess wartet, leitet das Signal nicht weiter. Der Stopp dauert dann die gesamte TimeoutStopSec, standardmäßig 90 Sekunden, und endet so:
myapp.service: State 'stop-sigterm' timed out. Killing.
myapp.service: Killing process 4711 (myapp) with signal SIGKILL.Die Lösung ist exec. Schreiben Sie exec /usr/local/bin/myapp. Dadurch wird die Shell durch das Programm ersetzt, sodass MainPID das Programm ist und Signale dieses erreichen. Besser ist es, die Shell zu entfernen und in der Unit Environment= oder EnvironmentFile= zu verwenden. Beachten Sie, dass dieser Fehler verborgen bleibt, wenn die Zeichenkette -c nur einen Befehl enthält. bash und dash optimieren diesen Fall zu einem direkten exec. Fügen Sie der Zeichenkette einen zweiten Befehl hinzu, bleibt die Shell vor Ihrem Programm aktiv.
In zwei Minuten auf einem Test-VPS reproduzieren
Speichern Sie den obigen Wrapper als /usr/local/bin/two-children.sh, machen Sie ihn mit chmod +x ausführbar und ersetzen Sie die beiden Programmpfade durch sleep 3600. Verweisen Sie mit Type=simple und Restart=on-failure aus einer Unit darauf. Führen Sie anschließend systemctl daemon-reload aus und starten Sie die Unit. Führen Sie systemd-cgls --unit two-children.service aus und notieren Sie die drei PIDs: die Shell und ihre beiden Kindprozesse. Beenden Sie einen Kindprozess mit sudo kill <pid>. Prüfen Sie die Unit erneut. Der Prozessbaum ist um einen Prozess kürzer, der Status ist weiterhin active (running) und das Journal enthält keine neuen Meldungen. Führen Sie jetzt stattdessen sudo kill -9 <shell pid> aus. Die Unit schlägt fehl, der verbleibende Kindprozess wird beendet, weil KillMode=control-group der Standardwert ist, und das Journal zeigt Scheduled restart job, restart counter is at 1.
Die vollständige Restart=-Syntax und wann on-failure besser als always ist
Restart= akzeptiert einen von sieben Werten. Der entscheidende Unterschied ist, was als sauberer Exit gilt. systemd behandelt den Exit-Code 0, alle in SuccessExitStatus= aufgeführten Codes sowie die Signale SIGHUP, SIGINT, SIGTERM und SIGPIPE als sauberen Exit. Alle anderen Fälle gelten als unsauber, einschließlich SIGKILL und SIGSEGV.
noist der Standardwert. Die Unit startet sich niemals selbst neu. Deshalb beendet sich eine Unit ohneRestart=-Zeile beim ersten Absturz und bleibt beendet.on-successstartet die Unit nur nach einem sauberen Exit neu.on-failurestartet die Unit bei einem Exit-Code ungleich 0, einem unsauberen Signal, einer Zeitüberschreitung beim Starten oder Stoppen sowie nach Ablauf des Watchdogs neu.on-abnormalstartet die Unit nach einem unsauberen Signal, einer Zeitüberschreitung oder nach Ablauf des Watchdogs neu, aber niemals nach einem normalen Exit-Code ungleich 0.on-abortstartet die Unit nur nach einem unsauberen Signal neu, also nach einem Absturz.on-watchdogstartet die Unit nur neu, wennWatchdogSec=abläuft.alwaysstartet die Unit nach jedem oben genannten Fall neu, einschließlich eines sauberen Exits mit Status 0.
on-failure ist der passende Standardwert für einen dauerhaft laufenden Daemon. Die Unit wird nach einem Absturz erneut gestartet, ein bewusstes exit 0 bleibt jedoch unbeeinflusst. always eignet sich für ein Programm, das aus Gründen außerhalb seiner Kontrolle sauber beendet wird, beispielsweise einen Tunnel-Client, der bei einer Trennung durch die Gegenstelle den Exit-Code 0 zurückgibt. Der Nachteil von always ist, dass dadurch Fehler verborgen werden: Ein Dienst, der startet, eine fehlerhafte Konfigurationsdatei einliest, den Fehler protokolliert und sich mit Exit-Code 0 beendet, läuft endlos in einer Neustartschleife. Das einzige Anzeichen ist ein steigender Neustartzähler.
SuccessExitStatus= verschiebt die Grenze zwischen sauberen und unsauberen Exits. Borg beendet sich bei Warnungen mit Exit-Code 1 und bei Fehlern mit Exit-Code 2. Deshalb wird eine Backup-Unit ohne SuccessExitStatus=1 jedes Mal als fehlgeschlagen markiert, wenn eine nicht lesbare Datei übersprungen wird. RestartPreventExitStatus= führt Exit-Codes auf, die selbst unter always einen Neustart verhindern. Das ist die saubere Möglichkeit für ein Programm mitzuteilen, dass es nicht erneut gestartet werden soll. RestartForceExitStatus= bewirkt das Gegenteil. Ein Backup-Job gehört in eine Type=oneshot-Unit, die von einem Timer gesteuert wird, nicht in eine Neustartschleife. Das Paar aus Service-Unit und Timer-Unit für die zeitgesteuerte Ausführung eines Jobs ist dafür das passende Muster.
Ein Hinweis zum Testen: Wenn Sie Ihren Dienst mit einem einfachen kill <pid> beenden, wird SIGTERM gesendet. Dieses Signal steht auf der Liste der sauberen Signale. Daher führt Restart=on-failure korrekterweise keinen Neustart durch, und Sie kommen zu dem Schluss, dass Ihre Konfiguration fehlerhaft ist. Verwenden Sie stattdessen kill -9 <pid> oder systemctl kill -s SIGKILL myapp.service. Beachten Sie außerdem, dass kein Wert von Restart= nach systemctl stop greift oder wenn die Unit beendet wurde, weil eine BindsTo=- oder PartOf=-Abhängigkeit weggefallen ist. Ein Stop-Auftrag ist kein Fehler.
RestartSec und der Standardwert von 100 Millisekunden
RestartSec= ist die Pause zwischen dem Beenden der Unit und dem erneuten Start durch systemd. Der Standardwert beträgt 100 Millisekunden. Prüfen Sie, welchen Wert Ihre Unit tatsächlich geladen hat:
systemctl show -p RestartUSec -p StartLimitIntervalUSec -p StartLimitBurst myapp.serviceFür eine Unit ohne diese Einstellung wird RestartUSec=100ms ausgegeben. Dieser Standardwert ist für einen Dienst geeignet, der einmal abstürzt und anschließend wieder startet. Für einen Dienst, der überhaupt nicht starten kann, ist er ungeeignet, weil dadurch fünf Neustarts innerhalb einer halben Sekunde erfolgen. Genau das löst die im nächsten Abschnitt beschriebene Ratenbegrenzung aus. Für Dienste, die auf eine Datenbank, einen Mount oder eine Netzwerkroute warten, sollten Sie RestartSec=5s oder einen höheren Wert setzen.
Seit August 2026 bieten systemd 254 und neuere Versionen außerdem RestartSteps= und RestartMaxDelaySec=. Damit wächst die Verzögerung von RestartSec= bis zu einem Höchstwert über die angegebene Anzahl von Versuchen. Ubuntu 24.04 enthält systemd 255 und unterstützt diese Einstellungen. Debian 12 enthält systemd 252 und unterstützt sie nicht. Wachsende Verzögerungen sind die richtige Lösung, wenn die Abhängigkeit längere Zeit nicht verfügbar sein kann.
Was „start request repeated too quickly“ wirklich bedeutet
Dies ist der Zustand, bei dem Leser den Eindruck bekommen, dass systemd willkürlich aufgibt. Tatsächlich handelt es sich um einen Zähler. Die Regel lautet: Wenn eine Unit innerhalb von StartLimitIntervalSec= mehr als StartLimitBurst=-mal gestartet wird, verweigert systemd einen weiteren Start und versetzt die Unit in den Status failed. Die Standardwerte sind 5 Starts innerhalb von 10 Sekunden.
Das Journal zeigt die Abfolge:
myapp.service: Scheduled restart job, restart counter is at 5.
myapp.service: Start request repeated too quickly.
myapp.service: Failed with result 'start-limit-hit'.
Failed to start myapp.service - My application.und systemctl start liefert bereits die passende Behebung:
Job for myapp.service failed because start of the service was attempted too often. See "systemctl status myapp.service" and "journalctl -xeu myapp.service" for details. To force a start use "systemctl reset-failed myapp.service" followed by "systemctl start myapp.service" again.systemctl reset-failed myapp.service setzt den Zähler und den Status failed zurück. Keine andere Aktion bewirkt das. Ein einfacher Aufruf von systemctl start wird daher weiterhin verweigert, bis Sie systemctl reset-failed myapp.service ausführen. Auch manuelle Starts werden auf das Limit angerechnet. Einige ungeduldige systemctl restart-Aufrufe während der Bearbeitung einer Konfigurationsdatei können das Limit daher auslösen, ohne dass überhaupt ein Absturz vorliegt.
Der irreführende Teil: start-limit-hit nennt nie den Grund für den Fehler des Dienstes. Die Meldung besagt nur, dass der Dienst wiederholt und in kurzer Folge fehlgeschlagen ist. Die eigentliche Ursache steht in den darüberliegenden Zeilen des Journals.
Beide Einstellungen gehören in den Abschnitt [Unit]. Sie finden Beispiele, die sie in [Service] eintragen. Ältere systemd-Versionen akzeptierten das, und dadurch entsteht die Verwirrung. Tragen Sie die Einstellungen in [Unit] ein. Fragen Sie anschließend mit systemctl show ab, was systemd geladen hat, denn nur der geladene Wert ist maßgeblich.
[Unit]
Description=My application
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=exec
ExecStart=/usr/local/bin/myapp
Restart=on-failure
RestartSec=10sDamit erhält die Unit fünf Startversuche innerhalb eines Zeitfensters von fünf Minuten, bevor systemd aufgibt. StartLimitIntervalSec=0 deaktiviert das Limit vollständig. Sie sollten sich über die Konsequenzen im Klaren sein: Ein Dienst, der niemals erfolgreich starten kann, wird nun dauerhaft erneut gestartet und schreibt bei jedem Versuch einen Eintrag in das Journal. Die systemweiten Standardwerte befinden sich in /etc/systemd/system.conf unter den Einstellungen DefaultStartLimitIntervalSec= und DefaultStartLimitBurst=.
Eine benachbarte Einstellung verdient einen Warnhinweis. StartLimitAction= legt fest, was beim Erreichen des Limits geschieht. Zulässige Werte sind unter anderem reboot, reboot-force und poweroff. Der Standardwert ist none. Damit schlägt die Unit fehl, während das System unverändert bleibt. Auf einem Remote-VPS bedeutet poweroff, dass der Rechner ausgeschaltet bleibt, bis Sie die Konsole des Providers öffnen.
Ein Prozess pro Unit
Das ist fast immer die richtige Lösung. Wenn zwei Programme ausgeführt werden müssen, schreiben Sie zwei Units. Jede Unit hat dann einen echten Hauptprozess, einen echten Exit-Status und eine eigene Neustartrichtlinie. Außerdem erhalten Sie getrennte Logs, getrennte Ressourcenlimits und getrennte Neustartzähler. Das ist genau das, was Sie um drei Uhr morgens benötigen.
Beschreiben Sie die Beziehung zwischen den Units in den Unit-Dateien, nicht in einem Shell-Skript.
After=legt nur die Startreihenfolge fest. Über Fehler sagt es nichts aus.Requires=startet die andere Unit zusammen mit dieser Unit und stoppt diese Unit, wenn die andere Unit ausdrücklich gestoppt wird.BindsTo=istRequires=plus der für Sie wichtige Fall: Diese Unit wird gestoppt, wenn die andere Unit aus irgendeinem Grund stoppt, auch bei einem Absturz. Kombinieren Sie es mitAfter=, da die Reihenfolge sonst nicht definiert ist.PartOf=überträgt Stop- und Neustartvorgänge nach unten. Dadurch erreichtsystemctl restart myapp.targetjede Unit, diePartOf=ist.Upholds=(systemd 249 und neuer, also Ubuntu 22.04 und neuer) hält die angegebene Unit am Laufen. Wenn sie stoppt, startet systemd sie erneut. Dabei gilt dasselbe Start-Ratenlimit wie für alle anderen Units.
Ein Worker, der niemals ohne seinen API-Server laufen darf und den systemd immer aktiv hält, solange die API läuft:
# /etc/systemd/system/myapp-api.service
[Unit]
Description=myapp API server
Wants=network-online.target
After=network-online.target
Upholds=myapp-worker.service
[Service]
Type=exec
User=myapp
ExecStart=/usr/local/bin/myapp serve
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target# /etc/systemd/system/myapp-worker.service
[Unit]
Description=myapp background worker
BindsTo=myapp-api.service
After=myapp-api.service
StartLimitIntervalSec=120
StartLimitBurst=5
[Service]
Type=exec
User=myapp
ExecStart=/usr/local/bin/myapp worker
Restart=on-failure
RestartSec=5sDer Worker hat keinen Abschnitt [Install] und wird niemals manuell aktiviert. Die API-Unit zieht ihn mit Upholds= hinzu. Daher ist systemctl enable --now myapp-api.service der einzige Befehl, den Sie ausführen müssen. Laden Sie die Konfiguration neu und prüfen Sie, wie systemd das Paar verarbeitet:
sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/myapp-worker.service
systemctl list-dependencies myapp-api.servicesystemd-analyze verify gibt überhaupt nichts aus, wenn die Datei fehlerfrei ist. Jede Ausgabe weist auf ein Problem hin. Meist erkennt systemd einen Schlüssel in dem Abschnitt, in dem Sie ihn eingetragen haben, nicht oder eine Abhängigkeit verweist auf eine nicht vorhandene Unit.
Korrektur 2: Type=notify, damit systemd mehr als nur eine PID kennt
Wenn das Programm das Benachrichtigungsprotokoll von systemd unterstützt, sollten Sie es verwenden. Mit Type=notify teilt der Dienst systemd mit, wann er bereit ist. Dadurch basiert die Reihenfolge tatsächlich auf dem Bereitschaftszustand und nicht auf einer bloßen Annahme. Außerdem kann der Dienst MAINPID= senden, damit systemd den relevanten Prozess statt eines Startprogramms überwacht.
WatchdogSec= ist der Teil, der den Aufwand rechtfertigt. Wenn Sie es setzen, muss der Dienst mindestens so häufig WATCHDOG=1 über sd_notify(3) senden. Sobald diese Nachrichten ausbleiben, beendet systemd den Dienst mit SIGABRT und markiert ihn als fehlgeschlagen. Restart=on-failure oder Restart=on-watchdog startet ihn dann erneut. Dies ist die einzige integrierte Möglichkeit, einen noch laufenden, aber blockierten Prozess neu zu starten. Keine Richtlinie für Exit-Status kann einen solchen Zustand erkennen.
[Service]
Type=notify
NotifyAccess=main
ExecStart=/usr/local/bin/myapp serve
WatchdogSec=30s
Restart=on-failure
RestartSec=5sEin ausgelöster Watchdog erscheint im Journal als myapp.service: Watchdog timeout (limit 30s)!, gefolgt vom Beenden des Prozesses. Wenn die Unit stattdessen in activating (start) verbleibt, bis TimeoutStartSec abläuft, ist READY=1 nie eingegangen. Entweder unterstützt das Programm das Protokoll nicht, oder NotifyAccess=main weist eine Benachrichtigung zurück, die von einem untergeordneten Prozess stammt. Das Journal meldet in diesem Fall beide PIDs.
Wenn die Software einen HTTP-Endpunkt für Zustandsprüfungen bereitstellt, aber keine sd_notify-Unterstützung besitzt, bleiben zwei sachgerechte Möglichkeiten: eine kleine Timer-Unit, die den Endpunkt prüft und systemctl restart aufruft, oder die Prüfung durch eine Container-Laufzeitumgebung. Genau dafür gibt es Compose-Healthchecks und ihr Neustartverhalten.
Korrektur drei: Ein Supervisor innerhalb der Unit, nur wenn es keine Alternative gibt
Manche Software wird tatsächlich als Bündel von Prozessen hinter einem Launcher ausgeliefert, das Sie nicht in einzelne Komponenten aufteilen können. Dann führen Sie innerhalb der Unit einen Supervisor aus und akzeptieren die Konsequenz: systemd überwacht den Supervisor, der Supervisor überwacht alle übrigen Prozesse, und Ihre Neustartrichtlinie ist nun in zwei Dateien definiert.
Die häufigste Ausprägung davon ist eine Container-Runtime. Eine docker compose- oder podman-Unit folgt genau diesem Muster. Die Neustartrichtlinie für die einzelnen Container wird in der Compose-Datei festgelegt, während die systemd-Unit nur dafür sorgt, dass die Runtime läuft. Wenn das Ihrer Struktur entspricht, zeigt die Unit, die einen Compose-Stack beim Booten startet die funktionierende Variante einschließlich der Begründung, warum Type=oneshot mit RemainAfterExit=yes dort normalerweise korrekt ist.
Die cgroup arbeitet weiterhin zu Ihrem Vorteil. Alles, was der Supervisor startet, bleibt in der cgroup der Unit. Daher decken MemoryMax=, CPUQuota= und die Bereinigung beim Stoppen weiterhin den gesamten Prozessbaum ab. Nur die Entscheidung über einen Neustart wird delegiert.
Unabhängig davon, welchen Supervisor Sie wählen, setzen Sie nicht unüberlegt Restart=always für die äußere Unit und zusätzlich eine aggressive Neustartrichtlinie innerhalb der Unit. Zwei Ebenen mit Neustartlogik und jeweils eigener Wartezeit führen zu einem Dienst, der minutenlang flapping zeigt, und zu einem Journal, das den Grund dafür nicht erklärt.
ExitType=cgroup bedeutet nicht „Neustart, wenn ein beliebiger Prozess beendet wird“
ExitType= (systemd 250 und neuer, also in Ubuntu 24.04 und Debian 12 enthalten) ist die Einstellung, die bei der Suche nach diesem Problem häufig gefunden wird. Sie bewirkt jedoch das Gegenteil von dem, was der Name nahelegt. Der Standardwert ExitType=main bedeutet, dass der Dienst als beendet gilt, sobald der Hauptprozess endet. Bei ExitType=cgroup gilt der Dienst als aktiv, bis der letzte Prozess in der cgroup beendet wird.
ExitType=cgroup macht eine Unit also unempfindlicher gegenüber dem Ende eines einzelnen Prozesses, nicht empfindlicher. Die Einstellung ist für ein Programm geeignet, das seinen eigentlichen Worker-Prozess als Fork startet und den übergeordneten Prozess beendet, ohne eine PID-Datei zu schreiben, sodass Type=forking den Daemon nicht finden kann. Für den hier beschriebenen Fehler ist sie nicht geeignet.
Es gibt keinen Wert Restart= mit der Bedeutung „Unit neu starten, wenn ein beliebiger Prozess in der cgroup beendet wird“. Wenn Sie dieses Verhalten benötigen, brauchen Sie einen Prozess pro Unit. Wenn sich das Programm nicht aufteilen lässt und Sie das Wrapper-Skript kontrollieren, kommt wait -n diesem Verhalten am nächsten. Der Befehl kehrt zurück, sobald der erste untergeordnete Prozess beendet wird:
#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
wait -n
exit 1Das Beenden eines beliebigen untergeordneten Prozesses beendet den Wrapper nun mit einem Status ungleich 0, sodass Restart=on-failure greift. Dies ist ein Kompromiss, keine Behebung. Sie erhalten weiterhin einen gemeinsamen Neustartzähler für zwei Programme, einen gemeinsamen Logdatenstrom und keine Möglichkeit, nur die fehlerhafte Hälfte neu zu starten.
Was tatsächlich passiert ist
Vier Befehle, in dieser Reihenfolge.
systemctl status myapp.service
systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Result -p ExecMainStatus myapp.service
journalctl -u myapp.service -b -o short-precisesystemctl status zeigt Status, Haupt-PID und den cgroup-Baum auf einem Bildschirm. Bei einer fehlerfreien Unit steht dort Active: active (running) mit einer Main PID:-Zeile, in der der erwartete Prozess genannt wird. Wenn der Baum unten Prozesse enthält, die Sie nicht erkennen, oder einen erwarteten Prozess nicht enthält, kennen Sie die Ursache bereits.
systemd-cgls --unit gibt denselben Baum ohne Kürzung aus. Das wird wichtig, sobald eine Unit mehr als eine Handvoll Prozesse verwaltet.
systemctl show gibt maschinenlesbare Informationen aus. NRestarts= ist der Neustartzähler. Damit erkennen Sie am schnellsten, ob ein Dienst bereits vierzigmal neu gestartet wurde oder seit dem Bootvorgang läuft. Result= enthält den Grund für den letzten Fehler: exit-code, signal, timeout, oom-kill, watchdog oder start-limit-hit. ExecMainStatus= ist der rohe Exit-Status des letzten Hauptprozesses.
Das Journal enthält die Abfolge. Nach diesen drei Zeilen sollten Sie suchen:
myapp.service: Main process exited, code=exited, status=1/FAILURE
myapp.service: Failed with result 'exit-code'.
myapp.service: Scheduled restart job, restart counter is at 1.code=exited, status=N bedeutet, dass das Programm selbst N zurückgeben wollte. Der Fehler liegt dann im Programm oder in seiner Konfiguration. code=killed, signal=SEGV bedeutet, dass es abgestürzt ist. code=killed, signal=TERM bedeutet normalerweise, dass ein anderer Prozess den Dienst zum Beenden aufgefordert hat. Das ist kein Fehler und löst Restart=on-failure nicht aus. code=dumped bedeutet, dass eine Core-Datei hinterlassen wurde. coredumpctl list zeigt sie an, wenn systemd-coredump installiert ist.
Bei mehreren Rechnern ist NRestarts die relevante Zahl, die Sie regelmäßig erfassen sollten. Wenn der Zähler einer Unit jeden Tag steigt, schlägt der Dienst jeden Tag fehl, unabhängig davon, ob es jemand bemerkt. Sobald Sie mehr als zwei oder drei Rechner verwalten, macht eine einheitliche Methode zum Ausführen eines Befehls auf jedem Server daraus statt einer Vermutung einen Bericht.
FAQ
Warum meldet systemctl, dass mein Dienst aktiv ist, obwohl der Prozess beendet wurde?
systemd verfolgt pro Service-Unit einen Prozess: den Hauptprozess. Restart= liest nur den Exit-Status dieses Prozesses. Alle anderen Prozesse, die die Unit startet, befinden sich in derselben cgroup. systemd beendet diese Prozesse, wenn die Unit stoppt, überwacht sie aber nicht auf ihr Ende. Führen Sie systemctl show -p MainPID myapp.service aus und vergleichen Sie die Anzahl mit systemd-cgls --unit myapp.service. Wenn der beendete Prozess im Baum erscheint, aber nicht MainPID ist, hat sich systemd genau wie vorgesehen verhalten. Die Lösung besteht aus einem Prozess pro Unit. Die Beziehung wird zwischen den Units mit BindsTo= und Upholds= festgelegt.
Was bedeutet „start request repeated too quickly“?
Die Unit wurde innerhalb von StartLimitIntervalSec= mehr als StartLimitBurst= Mal gestartet. Standardmäßig sind das 5 Starts in 10 Sekunden. Deshalb hat systemd weitere Startversuche beendet. Das ist eine Ratenbegrenzung. Sie erklärt nicht, warum der Dienst fehlschlägt. Lesen Sie deshalb die darüberstehenden Zeilen im Journal. Setzen Sie den Status mit systemctl reset-failed myapp.service zurück und beheben Sie anschließend den eigentlichen Fehler. Wenn der Dienst auf das langsame Starten einer Abhängigkeit wartet, erhöhen Sie RestartSec=. Das Standardintervall von 100 Millisekunden verbraucht alle fünf Versuche in weniger als einer Sekunde.
Sollte ich Restart=always oder Restart=on-failure verwenden?
Verwenden Sie für fast alle Fälle on-failure. Dadurch wird ein Absturz, ein Exit ungleich 0, eine Zeitüberschreitung und ein ausgelöster Watchdog neu gestartet. Ein absichtlicher exit 0 bleibt davon unberührt. Verwenden Sie always nur, wenn das Programm aus Gründen außerhalb seiner Kontrolle erfolgreich beendet wird. Ein Beispiel ist ein Client, der bei einer Trennung seines Gegenübers den Exit-Status 0 zurückgibt. Der Nachteil von always besteht darin, dass ein Dienst eine fehlerhafte Konfiguration einliest, einen Fehler protokolliert und mit Exit-Status 0 beendet werden kann. Er läuft dann endlos weiter. Das einzige sichtbare Anzeichen ist, dass NRestarts in systemctl show ansteigt.
Warum löst das manuelle Beenden meines Prozesses keinen Neustart aus?
systemd behandelt SIGHUP, SIGINT, SIGTERM und SIGPIPE als reguläre Beendigungen. Ein einfaches kill <pid> sendet SIGTERM. Unter Restart=on-failure gilt eine reguläre Beendigung nicht als Fehler. Deshalb erfolgt kein Neustart. Die Konfiguration wirkt dann fehlerhaft, obwohl sie es nicht ist. Testen Sie mit kill -9 <pid> oder systemctl kill -s SIGKILL myapp.service. Dabei handelt es sich um eine nicht reguläre Beendigung, die die Richtlinie auslöst. Dieselbe Regel erklärt, warum systemctl stop Ihre Neustartrichtlinie nie außer Kraft setzt.
Wo werden StartLimitIntervalSec und StartLimitBurst eingetragen?
Im Abschnitt [Unit]. Ältere Dokumentation und ältere systemd-Versionen ordnen diese Optionen [Service] zu. Deshalb widersprechen sich kopierte Beispiele. Raten Sie nicht, welche Zuordnung Ihre Version unterstützt. Fragen Sie systemd nach systemctl daemon-reload mit systemctl show -p StartLimitBurst -p StartLimitIntervalUSec myapp.service, welche Werte geladen wurden, und betrachten Sie diese Zahlen als maßgeblich. systemd-analyze verify /etc/systemd/system/myapp.service erkennt Schlüssel, die systemd überhaupt nicht kennt, und gibt bei einer fehlerfreien Datei nichts aus.