systemd-Unit startet nicht: Exit-Code richtig lesen
Lesen Sie zuerst systemctl status: Erkennen Sie 203/EXEC und 226/NAMESPACE, und verstehen Sie, warum eine Unit sauber startet und eine Sekunde später endet.
Warum eine systemd-Unit nicht startet
Eine systemd-Unit, die nicht startet, nennt den Grund in einem Feld. Führen Sie systemctl status <unit> aus und suchen Sie in der Zeile mit der Fehlermeldung nach code= und status=. Eine Statusnummer im Bereich der 200er bedeutet, dass systemd Ihr Programm nie erreicht hat: Der Fehler ist beim Aufbau der Umgebung aufgetreten, die Ihre Unit-Datei angefordert hat. Eine Statusnummer unter 200 bedeutet, dass Ihr Programm tatsächlich ausgeführt wurde und selbst beendet wurde. Die Unit-Datei ist dann wahrscheinlich korrekt, und die Anwendung ist das Problem.
Diese Unterscheidung bestimmt das weitere Vorgehen. Alles Folgende leitet sich daraus ab, und zwar in der Reihenfolge, in der die Nummern auftreten.
Drei Befehle beantworten die Frage in dieser Reihenfolge
systemctl status myapp.service
journalctl -u myapp.service -b --no-pager
systemd-analyze verify /etc/systemd/system/myapp.servicesystemctl status liefert das Ergebnis. Lesen Sie zuerst die Zeile Loaded:, weil sie den Dateinamen nennt, den systemd tatsächlich eingelesen hat, und angibt, ob die Unit aktiviert, maskiert oder überhaupt nicht gefunden wurde. Lesen Sie anschließend die Zeile Active: sowie das darunterstehende Paar code= und status=.
journalctl -u myapp.service -b --no-pager liefert die Details. -u beschränkt die Ausgabe auf diese eine Unit, -b begrenzt sie auf den aktuellen Bootvorgang, damit Sie keinen Fehler der vergangenen Woche lesen, und --no-pager gibt die Ausgabe direkt im Terminal aus, sodass Sie sie an grep weiterleiten können. status zeigt nur die letzten Logzeilen und kürzt lange Zeilen. Das Journal zeigt alles, was das Programm vor dem Abbruch ausgegeben hat. Das ist normalerweise der eigentliche Fehler. Fügen Sie -n 100 für einen längeren Verlauf hinzu, oder führen Sie den Befehl mit -f in einem zweiten Terminal aus, während Sie die Unit neu starten.
systemd-analyze verify lädt eine Unit-Datei, ohne sie auszuführen. Der Befehl warnt vor unbekannten Abschnitten und Direktiven und markiert Befehle in ExecStart=, die er nicht ausführen kann. Dadurch werden die zwei unauffälligen Fehlerklassen erkannt: ein falsch geschriebener Schlüssel, den systemd beim Laden mit einer Warnung ignoriert, die die meisten Benutzer nie lesen, sowie ein nicht vorhandener Pfad.
Führen Sie nach dem Bearbeiten einer Unit-Datei sudo systemctl daemon-reload aus. Bis dahin verwendet systemd weiterhin die zuvor geladene Kopie, und systemctl status fügt eine Warnung hinzu, dass sich die Datei auf dem Datenträger geändert hat. Wenn eine Korrektur scheinbar „nichts bewirkt“, hat systemd sie häufig noch nicht eingelesen.
Zwei weitere Befehle sind ebenfalls sinnvoll. systemctl cat myapp.service gibt die effektive Unit aus, also die Hauptdatei zusammen mit jedem Drop-in unter /etc/systemd/system/myapp.service.d/. systemctl show myapp.service -p ExecStart -p User -p WorkingDirectory gibt diese Werte so aus, wie systemd sie eingelesen hat. Genau diese Konfiguration wird tatsächlich ausgeführt.
Was bedeutet status=203/EXEC?
203/EXEC bedeutet, dass systemd die Einrichtung abgeschlossen, execve() aufgerufen und der Kernel den Aufruf abgelehnt hat. Ihr Programm hat keine eigene Codezeile ausgeführt. In fast allen Fällen liegt eine von vier Ursachen vor.
- Der Pfad in
ExecStart=ist falsch oder nicht absolut. Prüfen Sie ihn mitls -lanhand der exakten Zeichenfolge in der Unit-Datei. - Die Datei besitzt kein Ausführungsbit.
sudo chmod +x /opt/myapp/run.shsetzt es. Eine aus einem Archiv entpackte oder von einem anderen Rechner kopierte Datei verliert dieses Bit häufig. - Die Shebang-Zeile ist fehlerhaft. Der Kernel liest die erste Zeile eines Skripts und startet den dort angegebenen Interpreter. Deshalb schlägt
#!/usr/bin/env python3fehl, wenn die Service-Variable PATH keinenpython3enthält. Eine Datei mit Windows-Zeilenenden fordert dagegen einen Interpreter namens/bin/bash\ran, der nicht existiert. - Die Datei kann auf diesem Rechner nicht ausgeführt werden: Sie hat die falsche Architektur oder ist eine Textdatei ohne Shebang.
Führen Sie den Aufruf zunächst als Dienstbenutzer manuell aus, bevor Sie etwas ändern.
sudo -u appuser /opt/myapp/run.sh
file /opt/myapp/run.sh
head -1 /opt/myapp/run.sh | cat -Afile nennt die Architektur und meldet „with CRLF line terminators“, wenn die Zeilenenden die Ursache sind. cat -A zeigt dasselbe als nachgestelltes ^M. Entfernen Sie diese Zeichen mit sed -i 's/\r$//' /opt/myapp/run.sh.
Ein wichtiger Hinweis zum Wertebereich: Werte ab 200 sind eine Konvention, keine Garantie. Ihr eigenes Programm kann mit 203 beendet werden, und systemd kann diese beiden Fälle nicht unterscheiden. systemd-analyze exit-status 203 gibt den Namen und die Klasse eines beliebigen Codes aus. Das erleichtert die Zuordnung zur Tabelle. Wenn Ihre Anwendung jedoch Exit-Codes über 199 verwendet, ändern Sie diese.
Warum erhalte ich 217/USER oder 216/GROUP?
217/USER bedeutet, dass das in User= angegebene Konto beim Start des Dienstes nicht existiert. 216/GROUP ist derselbe Fehler für Group= oder SupplementaryGroups=. Bestätigen Sie dies jeweils mit einem Befehl.
getent passwd appuser
getent group appgroupJeder dieser Befehle gibt entweder eine Zeile aus oder keine Ausgabe und beendet sich mit einem von 0 verschiedenen Rückgabewert. Keine Ausgabe bedeutet, dass der Name dem System unbekannt ist. systemd kann nicht zu diesem Konto wechseln und beendet den Start vor exec. Die Lösung besteht darin, das Konto anzulegen, nicht darin, User=root zu setzen. Einen Dienst unter einem dedizierten Systemkonto mit möglichst geringen Berechtigungen auszuführen, ist der eigentliche Zweck dieser Direktive.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin appuserDynamicUser=yes umgeht das Problem, indem systemd für jeden Start ein temporäres Konto zuweist. Das eignet sich für einen Dienst, der keinen Zustand speichert. Alles, was Dateien schreibt, benötigt zusätzlich StateDirectory=, weil sich die Benutzer-ID zwischen den Starts ändert und Dateien unter einem normalen Pfad anschließend einem Konto gehören, das nicht mehr existiert.
Was ist 226/NAMESPACE?
226/NAMESPACE stammt von den Sandboxing-Direktiven. Wenn eine Unit ProtectSystem=, ProtectHome=, PrivateTmp=, ReadWritePaths= oder eine ähnliche Einstellung setzt, erstellt systemd vor dem Start des Programms einen privaten Mount-Namespace für diesen Dienst. Ein Namespace ist hier eine private Ansicht des Dateisystems für einen Prozess. Wenn ein Mount in diesem Plan fehlschlägt, bricht der Start mit 226 ab und Ihr Programm wird nie ausgeführt.
Die häufigste Ursache ist ein Pfad in ReadWritePaths=, der nicht existiert. ProtectSystem=strict bindet das gesamte Dateisystem schreibgeschützt ein, und ReadWritePaths= öffnet benannte Pfade erneut zum Schreiben. systemd kann ein Verzeichnis nicht erneut öffnen, wenn es nicht vorhanden ist. Zwei Lösungen sind sinnvoll. Lassen Sie systemd das Verzeichnis mit StateDirectory= erstellen. Dadurch wird /var/lib/<name> bei jedem Start angelegt und dem Benutzer des Dienstes zugewiesen. Alternativ können Sie dem Pfad - voranstellen. Damit weist systemd an, den Eintrag zu ignorieren, wenn die Quelle fehlt. Die falsche Lösung besteht darin, die Härtung zu entfernen. Das tauscht ein fünfminütiges Problem gegen ein dauerhaftes aus.
[Service]
ProtectSystem=strict
ProtectHome=yes
StateDirectory=myapp
ReadWritePaths=-/srv/uploadsWenn Sie nicht feststellen können, welche Zeile verantwortlich ist, entfernen Sie den gesamten Härtungsblock, laden Sie die Konfiguration neu und starten Sie den Dienst. Wenn der Dienst startet, fügen Sie die Zeilen einzeln wieder hinzu und starten Sie den Dienst nach jeder Änderung neu. Zwei verwandte Fehler in dieser Gruppe sind 233/RUNTIME_DIRECTORY und 238/STATE_DIRECTORY. Sie bedeuten, dass systemd das in RuntimeDirectory= oder StateDirectory= angegebene Verzeichnis nicht erstellen oder dessen Besitz nicht übernehmen konnte. Die Ursache ist meist, dass der Pfad bereits existiert und einem anderen Benutzer gehört.
Warum erscheint 200/CHDIR, obwohl WorkingDirectory korrekt aussieht?
200/CHDIR bedeutet, dass chdir() in WorkingDirectory= fehlgeschlagen ist. Das Verzeichnis fehlt, oder der Dienstbenutzer kann es nicht betreten. Zum Betreten eines Verzeichnisses ist das Ausführungsrecht für dieses Verzeichnis und für jedes übergeordnete Verzeichnis erforderlich. Daher ist ein vollständig lesbares /home/deploy/app nicht erreichbar, wenn /home/deploy den Modus 700 hat und der Dienst als appuser ausgeführt wird.
sudo -u appuser test -x /srv/myapp && echo ok
namei -l /srv/myappnamei -l gibt Eigentümer und Modus jeder Komponente des Pfads aus. Damit lässt sich am schnellsten das Verzeichnis finden, das den weiteren Zugriff blockiert. Mit WorkingDirectory=-/srv/myapp wird ein fehlendes Verzeichnis nicht zum fatalen Fehler. Das ist für ein Programm richtig, dem das Startverzeichnis egal ist, und für ein Programm falsch, das Dateien über relative Pfade öffnet.
Warum startet der Dienst und beendet sich eine Sekunde später?
Hier gibt es keinen Statuscode aus dem Bereich 200, häufig auch überhaupt keinen Fehlertext. Die Unit zeigt direkt nach dem Start inactive (dead) oder durchläuft wiederholt activating (auto-restart). systemd hat die Umgebung korrekt aufgebaut. Die Abweichung liegt zwischen dem Verhalten Ihres Programms und dem Verhalten, das Type= voraussetzt.
Type=simple ist der Standardwert und bedeutet, dass das Programm im Vordergrund bleibt. Wenn Sie einen Daemon starten, der in den Hintergrund forkt und sich beendet, erkennt systemd, dass der Hauptprozess beendet wurde, und betrachtet den Dienst als abgeschlossen. Die meisten Daemons bieten eine Option, mit der sie im Vordergrund bleiben, beispielsweise nginx -g 'daemon off;'.
Type=forking bedeutet, dass sich der erste Prozess beendet, sobald sein Kindprozess bereit ist. Wenn Sie ein Vordergrundprogramm damit starten, wartet der Startauftrag, bis TimeoutStartSec= abläuft. Das geschieht standardmäßig nach 90 Sekunden. Anschließend beendet systemd den Prozess und protokolliert einen Timeout.
Type=notify bedeutet, dass das Programm sd_notify() aufruft, um seine Bereitschaft zu melden. Ein Programm ohne diese Unterstützung meldet nichts. Der Start läuft deshalb in einen Timeout, und das Journal verzeichnet das Ergebnis als Protokollfehler.
Wählen Sie den Typ anhand des tatsächlichen Programmverhaltens. Die Unterschiede zwischen simple, forking, oneshot und notify ist die entscheidende Auswahl für diese gesamte Fehlerklasse.
Wenn sich ein Dienst immer wieder beendet, stellt systemd weitere Startversuche ein und meldet, dass sich die Startanforderung zu schnell wiederholt hat. Die Unit bleibt anschließend im Zustand failed, bis das Zeitfenster für die Ratenbegrenzung abgelaufen ist oder Sie sudo systemctl reset-failed myapp.service ausführen. Eine Erhöhung des Limits verschleiert nur das Symptom. Lesen Sie die Journal-Einträge ab dem ersten Fehler statt ab dem letzten und sehen Sie unter was Restart=on-failure tatsächlich erneut startet nach, bevor Sie die Einstellung ändern.
Warum ist die Unit ohne jeden Fehler inaktiv?
Eine Unit kann übersprungen werden, statt gestartet zu werden. Condition*-Direktiven sind absichtlich stumm: Wenn die Prüfung fehlschlägt, markiert systemd den Auftrag als erfolgreich und führt keine Aktion aus. Eine Unit mit ConditionPathExists=/etc/myapp/config.yml wird niemals gestartet, solange diese Datei fehlt. Sie meldet dabei ebenfalls keinen Fehler.
systemctl show myapp.service -p ConditionResult -p ConditionTimestamp
journalctl -u myapp.service -b --no-pager | grep -i conditionConditionResult=no bestätigt, dass die Unit übersprungen wurde. Im Journal wird die nicht erfüllte Prüfung genannt. Verwenden Sie stattdessen eine Assert*-Direktive, wenn ein fehlendes erforderliches Element zu einem klaren Fehler führen soll. Bedingungen, Assertions und Unit-Reihenfolge beschreibt, welche Prüfung an welche Stelle gehört.
Weitere stille Fälle treten in diesem Zusammenhang auf. Ein Fehler „konnte nicht gefunden werden“ bedeutet meist, dass sich die Datei im falschen Verzeichnis befindet oder dass Sie die Konfiguration noch nicht neu geladen haben: Von Ihnen erstellte Unit-Dateien gehören nach /etc/systemd/system/. Eine maskierte Unit verweigert jeden Start, bis sudo systemctl unmask myapp.service die Maskierung aufhebt. Außerdem schlägt systemctl enable bei einer Unit ohne Abschnitt [Install] fehl. Geben Sie der Unit daher einen Abschnitt WantedBy=multi-user.target.
Was passiert, wenn der Prozess beendet statt als fehlgeschlagen markiert wurde?
code=killed unterscheidet sich von code=exited. Eine externe Ursache hat den Prozess beendet. status=9/KILL verweist auf den Out-of-Memory-(OOM-)Killer, und das Journal nennt den ausgewählten Prozess. Ein selbst gesetztes Limit bewirkt innerhalb der Cgroup (Control Group) dasselbe. Prüfen Sie daher den freien Speicher auf dem Host mit free -m und die Unit auf ein MemoryMax=. MemoryMax, CPUQuota und die anderen Cgroup-Limits erläutert, welches Limit einen Prozess beendet und welches ihn nur verlangsamt.
status=15/TERM direkt nach einem Startversuch bedeutet normalerweise, dass systemd das Start-Timeout erreicht und den Prozess beendet hat. Damit gelangen Sie zurück zu Type=.
Zwei Gewohnheiten verhindern die meisten dieser Fehler
Verwenden Sie überall absolute Pfade. systemd startet nicht Ihre Login-Shell. Daher gibt es kein .bashrc, kein .profile und keine aktivierte virtuelle Umgebung. $PATH für einen Systemdienst enthält nur eine kurze, fest eingebaute Liste. Diese enthält weder /opt noch die Shims eines Sprachversionsmanagers. Schreiben Sie /usr/bin/python3 oder /opt/myapp/venv/bin/python vollständig aus. Mit command -v myapp gibt Ihre Shell den einzufügenden Pfad aus. Dieselbe Regel gilt für WorkingDirectory=, EnvironmentFile= und jeden Pfad in ReadWritePaths=.
ExecStart= ist keine Shell. systemd teilt die Zeile in Wörter auf und startet execve() selbst. Pipes, Umleitungen, Globs, &&, Backticks und ~ haben keine Bedeutung. Sie werden als wörtliche Argumente an Ihr Programm übergeben. ExecStart=/usr/bin/myapp --flag > /tmp/out.log übergibt > und /tmp/out.log an myapp. Dieses beendet sich anschließend mit einem Usage-Fehler, der nicht wie ein systemd-Problem aussieht. Wenn Sie Shell-Funktionen benötigen, müssen Sie eine Shell aufrufen.
ExecStart=/bin/sh -c '/usr/bin/myapp --flag | /usr/bin/tee -a /var/log/myapp.log'Nur für die Ausgabe benötigen Sie das nicht. Die Ausgabe eines Dienstes wird standardmäßig in das Journal geschrieben, und StandardOutput=append:/var/log/myapp.log schreibt ohne Shell direkt in eine Datei.
Die Variablensubstitution ist entsprechend eingeschränkt. $MYVAR und ${MYVAR} werden aus Environment= und EnvironmentFile= ersetzt. Darüber hinaus wird nichts substituiert. $HOME ist für einen Systemdienst nicht gesetzt, sofern Sie die Variable nicht selbst setzen. Ein EnvironmentFile= ist ebenfalls kein Shell-Skript: export gehört nicht hinein, seine Regeln für das Quoting unterscheiden sich von bash, und eine fehlende Datei führt zu einem Fehler, sofern Sie dem Pfad nicht - voranstellen.
Arbeiten Sie direkt auf einem laufenden Server
Lesen Sie den Code, belegen Sie die Ursache, ändern Sie genau eine Sache und starten Sie den Dienst neu. Diese Reihenfolge ist wichtiger, als jede Zahl zu kennen. Sie verhindert, dass Sie drei spekulative Änderungen gleichzeitig vornehmen und anschließend nicht mehr feststellen können, welche davon geholfen hat. Dasselbe Vorgehen funktioniert auch für Units, die Sie nicht selbst geschrieben haben. Ein Timer, der nie ausgelöst wird, gehört zu einem Dienst, der nie gestartet wurde. Debuggen Sie deshalb zuerst den Dienst: ein systemd-Timer und der von ihm ausgelöste Dienst schlägt aus genau den oben genannten Gründen fehl. Der Timer verbirgt die Ausgabe, bis Sie sie im Journal abfragen.
FAQ
Was bedeutet status=203/EXEC in systemctl status?
systemd hat alles eingerichtet, was die Unit angefordert hat. Danach ist der execve()-Aufruf fehlgeschlagen, sodass Ihr Programm nie gestartet wurde. Prüfen Sie diese vier Punkte in dieser Reihenfolge: Der in ExecStart= angegebene Pfad ist vorhanden und absolut, die Datei besitzt das Ausführungsbit, die Shebang verweist auf einen Interpreter, der im PATH des Dienstes vorhanden ist, und die Datei verwendet Unix-Zeilenenden. file meldet im letzten Fall "with CRLF line terminators". Dadurch wird der Name des Interpreters zu /bin/bash\r, und der Kernel verweigert die Ausführung.
Warum startet mein Dienst und wird sofort wieder beendet?
Die Unit-Datei beschreibt ein Verhalten, das das Programm nicht aufweist. Bei Type=simple erwartet systemd, dass das Programm im Vordergrund bleibt. Ein Daemon, der sich in den Hintergrund abspaltet, gilt daher in dem Moment als beendet, in dem er sich abspaltet. Bei Type=forking wartet systemd, bis der erste Prozess beendet wird. Ein Vordergrundprogramm lässt den Startauftrag daher hängen, bis TimeoutStartSec= abläuft. Stimmen Sie Type= auf das Programm ab. Wenn das Programm einen Vordergrundschalter anbietet, verwenden Sie diesen Schalter zusammen mit dem Standardwert Type=simple.
Wie sehe ich den tatsächlichen Fehler statt der kurzen Statusausgabe?
systemctl status gibt nur die letzten Journalzeilen aus und kürzt lange Zeilen. Führen Sie journalctl -u myapp.service -b --no-pager aus, um alles abzurufen, was die Unit während dieses Boots protokolliert hat. Verwenden Sie -n 200 für einen größeren Zeitraum oder leiten Sie die Ausgabe an grep weiter. Wenn die Anwendung in eine eigene Logdatei schreibt, lesen Sie auch diese. systemd erfasst nur, was das Programm an die Standardausgabe und den Standardfehler sendet.
Warum ist meine Unit ohne Fehlermeldung inaktiv?
Meistens hat eine Condition*-Direktive den Start übersprungen. Diese Prüfungen sind still: Eine nicht erfüllte Bedingung markiert den Startauftrag als erfolgreich. Führen Sie systemctl show myapp.service -p ConditionResult aus und suchen Sie nach ConditionResult=no. Lesen Sie anschließend die Journalzeile, in der die Prüfung genannt wird. Eine weitere häufige Ursache ist eine maskierte Unit. Sie verweigert jeden Start, bis sudo systemctl unmask die Maskierung entfernt.
Benötige ich nach jeder Änderung an einer Unit-Datei daemon-reload?
Ja, bei jeder Änderung an einer Unit-Datei oder einem Drop-in. sudo systemctl daemon-reload veranlasst systemd, die Dateien erneut von der Festplatte einzulesen. Anschließend wendet sudo systemctl restart myapp.service die Änderungen auf den laufenden Dienst an. Nach systemctl edit ist der Befehl nicht erforderlich, da dieser die Unit-Dateien selbst neu einliest. Das gilt auch nicht für Änderungen an einer Konfigurationsdatei, die zur Anwendung und nicht zu systemd gehört.