shutdown-blame: systemd-analyze blame für den Shutdown

Ein kleines Werkzeug, das das Journal des vorherigen Boots liest und die beim Herunterfahren gestoppten Units nach Dauer sortiert auflistet.

Das Problem

Bootet ein Linux-Rechner langsam, listet systemd-analyze blame die Units nach Startdauer, und systemd-analyze critical-chain zeigt, was worauf gewartet hat. Für das Herunterfahren gibt es keinen solchen Befehl. Ein langsamer Shutdown zeigt sich meist als Meldung "A stop job is running for ..." mit einem Zähler bis 90 Sekunden. Der übliche Rat ist, mit einer Debug-Shell zu booten oder einen Shutdown-Hook einzurichten und dann das rohe Log zu lesen.

Der Bedarf ist belegt:

  • systemd/systemd#3371, "Provide systemd-analyze blame equivalent for system shutdown", ist seit Mai 2016 offen und hat 58 Reaktionen.
  • Drei Fragen zu "A stop job is running" (1, 2, 3) haben zusammen etwa 400.000 Aufrufe.

Wie es funktioniert

systemd schreibt die nötigen Daten bereits ins Journal. Jeder Stop-Job bekommt einen Eintrag "Stopping ..." und einen Eintrag "Stopped ..." mit Job-ID, Unit-Name und Zeitstempel in Mikrosekunden. Läuft eine Unit in ihren Stop-Timeout, protokolliert systemd das ebenfalls, gefolgt von einem Eintrag für jeden Prozess, den es beendet. Ist das Journal persistent, liegt all das nach dem Neustart noch auf der Platte.

shutdown-blame liest diese strukturierten Felder mit journalctl -o json für den vorherigen Boot. Es findet den Beginn des Shutdowns (die logind-Meldung "System is powering down", ein Signal an PID 1 wie SIGRTMIN+3 oder ersatzweise den ersten Stop-Job nach dem letzten Start-Job), ordnet jeden Stop-Job über seine Job-ID zu und gibt die Jobs nach Dauer sortiert aus. Units der User-Manager sind enthalten. Das deckt den häufigen Fall "A stop job is running for User Manager for UID 1000" ab.

Das ist die Ausgabe für den unten beschriebenen Testcontainer, gestoppt mit docker stop:

Shutdown of boot bcc6c0a68390 began 2026-11-01 05:06:51 UTC (Received SIGRTMIN+3.).
The journal stopped 4.353s later; anything after that (final unmounts, killing leftover processes, firmware) is not recorded.

     TOOK    STARTED  UNIT                           NOTES
   4.216s    +0.014s  ignores-term.service           timed out in stop-sigterm; SIGKILL: sh, sleep
   3.009s    +0.021s  slow-stop.service
   2.220s    +0.023s  user@1500.service
   2.207s    +0.024s  user 1500: user-slow.service   timed out in stop-sigterm; SIGKILL: sh, sleep

Das Werkzeug ist eine einzelne Python-Datei ohne Abhängigkeiten. Man kann sie also auf einen Rechner kopieren, der gerade langsam heruntergefahren ist. --json gibt denselben Bericht maschinenlesbar aus.

Tests

Die Unit-Tests laufen gegen ein Journal, das bei einem echten Container-Shutdown aufgezeichnet wurde, und gegen konstruierte Fälle: zwei Shutdowns in einem Boot, ein Absturz ohne Shutdown, derselbe Unit-Name im System- und in einem User-Manager, ein fehlgeschlagenes Unmount und Meldungen, die kein gültiges UTF-8 sind.

Der End-to-End-Test bootet systemd in einem privilegierten Container mit drei Diensten: Einer stoppt sofort, einer hat ein ExecStop von 3 Sekunden, und einer ignoriert SIGTERM bei TimeoutStopSec=4. Ein Benutzer mit Linger betreibt einen User-Dienst, der SIGTERM ebenfalls ignoriert. Der Container wird zweimal heruntergefahren, einmal mit docker stop und einmal mit systemctl poweroff, und der Bericht wird gegen die erwarteten Dauern, Timeouts und beendeten Prozesse geprüft. Das CI führt das auf Ubuntu 24.04, Debian 12 und Fedora 42 aus. Dabei fiel ein Unterschied auf: systemd 257 auf Fedora beendet Units nach dem Timeout mit SIGABRT. Das Werkzeug meldet deshalb das Signal, das es im Log findet.

Grenzen

  • Das Journal muss persistent sein (/var/log/journal muss existieren).
  • journald stoppt kurz vor dem Ende des Shutdowns. Die letzten Unmounts, das abschließende Beenden übriger Prozesse und Verzögerungen der Firmware stehen nicht im Journal und erscheinen deshalb nicht im Bericht.
  • Es listet Dauern. Eine Abhängigkeitskette wie bei critical-chain berechnet es nicht.
  • Getestet ist es in Containern. Der Neustart eines physischen Rechners schreibt dieselben Journal-Einträge, das CI prüft das aber nicht.

Code und Installationsanleitung liegen unter github.com/Jwrede/shutdown-blame.