Post

Einen Systemd-Service (Daemon) mit Watchdog überwachen und bei Bedarf neu starten

Einen Systemd-Service (Daemon) mit Watchdog überwachen und bei Bedarf neu starten

Wenn man einen Server oder Dienst schreibt, will man eigentlich, dass der zuverlässig verfügbar ist. Aber es gibt (neben Programmierfehlern natürlich) verschiedenste Gründe, warum ein Dienst plötzlich nicht mehr funktioniert, wie z.B. ein Reboot nach Systemupdate oder der Neustart eines andereren Dienstes, von dem man abhängt.

In diesen Fällen wäre es sinnvoll, wenn man systemseitig erkennen könnte, dass der Dienst abgestürzt/beendet ist, oder einfach irgendwie festhängt. In diesen Fällen würde man den Dienst dann einfach neu starten.

Dies gelingt durch einen externen Wachhund oder watchdog. Dieser muss vom überwachten Dienst regelmäßig gestreichelt werden, oder er bellt und lässt den Dienst neu starten. Soweit zur Theorie, aber wie geht das mit dem Linux Systemdienst systemd?

Das nachfolgende Tutorial führt Schritt-für-Schritt (zum Mitmachen) durch alle Elemente der Diensterstellung mit Watchdogüberwachung. Ganz zum Schluss wird noch ein Trick gezeigt, wie man einen existierenden Dienst ohne Quelltextänderung überwachen kann.

Einen systemd Dienst einrichten

Service-Datei

Wenn man einen System-Dienst konfiguriert, schreibt meine eine Service-Datei in der Art wie in folgendem Beispiel:

Datei watchdogtest.service:

1
2
3
4
5
6
7
8
9
10
[Unit]
Description=watchdogtest
After=network.target

[Service]
ExecStart=/usr/local/bin/watchdogtest
RemainAfterExit=yes 

[Install]
WantedBy=multi-user.target
  • RemainAfterExit wird gesetzt, da der Dienst dauerhaft laufen soll und erst gestoppt werden muss, bevor er neu gestartet werden kann. Das ist üblich bei Webservern oder sonstigen Diensten, die dauerhaft als Daemon laufen.
  • After ist optional und kann benutzt werden, damit der Dienst erst gestartet wird, wenn ein anderer benötigter Dienst bereits gestartet wurde. Für einen Webservice ist network.target eine sinnige Wahl.

Diese Datei wird ins Verzeichnis /etc/systemd/system kopiert. Dann lädt man den systemd cache neu:

1
sudo systemctl daemon-reload

Das muss man jedes Mal machen, wenn man die .service-Datei geändert hat.

Dienst/Daemon starten/stoppen/neu starten und Status anzeigen

Und dann kann man den Dienst starten

1
sudo systemctl start watchdogtest

sich den Status anschauen

1
sudo systemctl status watchdogtest

die letzten Ausgaben des Dienstes ansehen (die journalctl-Argumente -x sind für etwas längere Erläuterungen/Erklärungen und -u für eine bestimmte systemd-Einheit; wenn man noch -e hinzufügt, wird nur die letzte Seite ausgegeben)

1
journalctl -xu watchdogtest

den Dienst stoppen

1
sudo systemctl stop watchdogtest

oder neu starten

1
sudo systemctl restart watchdogtest

Um den Dienst automatisch bei Systemstart zu starten, muss man ihn einschalten:

1
sudo systemctl enable watchdogtest

oder mit

1
sudo systemctl disable watchdogtest

wieder deaktivieren.

Minimalistisches Beispiel für einen Dienst

Ein Dienst ist eigentlich nichts weiter als ein Programm, welches im Hintergrund gestartet wurde und unabhängig von einer User-Sitzung im System existiert. Hier ist mal ein absolut minimalistisches Beispiel für einen Dienst, der nichts weiter macht, als jede Sekunde eine Ausgabe zu schreiben (ohne Nutzersitzung landen solche herrenlosen Ausgaben in den System-Logs, wo sie mit journalctl angesehen werden können):

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h> // for sleep()
#include <iostream>

int main() {
	int counter = 0;
	while (true) {
		std::cout << "Service counts to " << ++counter << std::endl;
		sleep(1); // wait a second
	}
	return 0;
}

Diese Datei compiliert man und legt die unter /usr/local/bin/watchdogtest ab, oder ändert in der .service-Datei die Zeile ExecStart= auf den Pfad zur Binärdatei ab (bei mir ist das ExecStart=/home/ghorwin/git/ServiceWatchdogTest/bin/debug/ServiceWatchdogTest).

Nun kann man das Starten und Stoppen des Dienstes testen. Der Befehl sudo systemctl start watchdogtest.service liefert keine Ausgabe, wenn die .service-Datei soweit fehlerfrei ist.

Die Statusausgabe sudo systemctl status watchdogtest.service liefert:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
● watchdogtest.service - watchdogtest
     Loaded: loaded (/etc/systemd/system/watchdogtest.service; disabled; vendor preset: enabled)
     Active: active (running) since Wed 2025-09-17 20:40:56 CEST; 36s ago
   Main PID: 14215 (ServiceWatchdog)
      Tasks: 1 (limit: 38095)
     Memory: 1.1M
        CPU: 11ms
     CGroup: /system.slice/watchdogtest.service
             └─14215 /home/ghorwin/git/ServiceWatchdogTest/bin/debug/ServiceWatchdogTest

Sep 17 20:41:23 Shadowboxli ServiceWatchdogTest[14215]: Service counts to 28
Sep 17 20:41:24 Shadowboxli ServiceWatchdogTest[14215]: Service counts to 29
Sep 17 20:41:25 Shadowboxli ServiceWatchdogTest[14215]: Service counts to 30
Sep 17 20:41:26 Shadowboxli ServiceWatchdogTest[14215]: Service counts to 31
Sep 17 20:41:27 Shadowboxli ServiceWatchdogTest[14215]: Service counts to 32
Sep 17 20:41:28 Shadowboxli ServiceWatchdogTest[14215]: Service counts to 33
Sep 17 20:41:29 Shadowboxli ServiceWatchdogTest[14215]: Service counts to 34
Sep 17 20:41:30 Shadowboxli ServiceWatchdogTest[14215]: Service counts to 35
Sep 17 20:41:31 Shadowboxli ServiceWatchdogTest[14215]: Service counts to 36
Sep 17 20:41:32 Shadowboxli ServiceWatchdogTest[14215]: Service counts to 37

systemd für die Überwachung konfigurieren

Nun wird die Service-Datei um eine Überwachungsfunktion erweitert:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=watchdogtest
After=network.target

[Service]
ExecStart=/home/ghorwin/git/ServiceWatchdogTest/bin/debug/ServiceWatchdogTest
RemainAfterExit=yes
Type=notify
NotifyAccess=all
WatchdogSec=10s
Restart=always
RestartSec=5s

[Install]
WantedBy=multi-user.target

Es kommen folgende Einträge dazu:

  • Type=notify - erlaube den Dienst, Nachrichten an systemd zu schicken
  • NotifyAccess=all - erlaube Nachrichten auch von nachträglich gestarteten Prozessen zu erhalten, die nicht die Haupt-ProzessID haben. Dies ist notwendig für Dienste, die beim Start einen separaten Prozess abspalten und sich dann selbst beenden.
  • WatchdogSec=10s - Das ist die Zeitspanne, die der Wachhund geduldig auf’s Streicheln wartet (zur korrekten Angabe der Zeit siehe unten).
  • Restart=always - Starte den Dienst immer neu, egal warum er beendet wurde. Das ist immer dann sinnvoll, wenn es keinen plausiblen Grund dafür gibt, dass sich der Dienst planmäßig selbst beendet. Sonst kann man auch Restart=on-failure verwenden.
  • RestartSec=5s - Wie lange soll systemd nach Beenden des Dienstes warten, bis er neu gestartet wird.

Nicht vergessen, nach dieser Änderung mit sudo systemctl daemon-reload die systemd-Konfiguration zu aktualisieren.

Der Quelltext des Dienstes muss nun um die Kommunikation zum Watchdog erweitert werden:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <unistd.h> // for sleep()
#include <iostream>
#include <systemd/sd-daemon.h> // for sd_notify()

int main() {
	sd_notify (0, "READY=1");
	int counter = 0;
	while (true) {
		std::cout << "Service counts to " << ++counter << std::endl;
		// send repeatedly a note to the watchdog
		sd_notify (0, "WATCHDOG=1");
		sleep(1); // wait a second
	}
	return 0;
}

Es gibt einige Fehlerquellen, über die man gerne mal stolpert.

Fehlerquelle: READY=1 vergessen

Wenn man vergisst, die anfängliche “bin fertig” Nachricht mit sd_notify (0, "READY=1"); zu schicken, wartet systemctl start ewig. Das verzögert auch den Systemstart ordentlich.

Fehlerquelle: NotifyAccess vergessen

Wenn man einen Dienst schreibt, wo der von systemd gestartete Prozess den eigentlichen Dienst in einem anderen Prozess abgetrennt startet, hat der dann laufende Prozess eine andere Prozess-ID, als der zunächst von systemd gestartete Prozess. Ohne die Option NotifyAccess=all bekommt man dann eine Fehlermeldung wie:

1
2
3
4
Sep 17 21:13:34 Shadowboxli ServiceWatchdogTest[18190]: Watchdog notify
Sep 17 21:13:34 Shadowboxli systemd[1]: watchdogtest.service: Got 
notification message from PID 18190, but reception only permitted for 
main PID which is currently not known

Die Ausgabe von systemctl status zeigt die ursprüngliche Main PID des gestarteten Prozesses:

1
2
3
4
5
6
7
8
9
10
● watchdogtest.service - watchdogtest
     Loaded: loaded (/etc/systemd/system/watchdogtest.service; disabled; vendor preset: enabled)
     Active: active (exited) since Wed 2025-09-17 21:13:31 CEST; 1min 23s ago
    Process: 18188 ExecStart=/home/ghorwin/git/ServiceWatchdogTest/bin/debug/ServiceWatchdogTest (code=exited, status=0/SUCCESS)
   Main PID: 18188 (code=exited, status=0/SUCCESS)
      Tasks: 1 (limit: 38095)
     Memory: 1.0M
        CPU: 24ms
     CGroup: /system.slice/watchdogtest.service
             └─18190 /home/ghorwin/git/ServiceWatchdogTest/bin/debug/ServiceWatchdogTest

Und da die Main PID 18188 nicht mit der PID 18190 des gestarteten Unter-Prozesses übereinstimmt, gibt es keine Kommunikation zum Watchdog.

Dieses Problem tritt z.B. bei der Verwendung von QtSolution/QtService auf.

Fehlerquelle: falsche Zeitangabe/falsches Format

Die Zeitangaben in .service-Dateien sind generell ohne Anführungszeichen. Es kann eine Zeitdauer ohne Einheit in Sekunden angegeben werden. Oder eine Zeitspanne wie

1
2
3
4
WatchdogSec=120

# oder
WatchdogSec=10min 30s

Weitere Details zu Zeitangaben sind in der systemd Dokumentation zu finden: https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html

Fehlerquelle: Groß-/Kleinschreibung

Immerhin wird bei falscher Schreibweise, also z.B. WatchDogSec statt WatchdogSec gleich beim Start des Dienstes eine Warnung ausgegeben. Aber es ist dennoch eine häufige Fehlerquelle, die man bei schnellen Änderungen als Tippfehler gerne mal übersieht (und dann startet der Dienst plötzlich beim nächsten Reboot nicht mehr).

Fehlerquelle: daemon-reload beim Testen vergessen

Auch nervig, weil man das gerne mal vergisst. Wenn man eine .service-Datei bearbeitet hat, muss man sudo systemctl daemon-reload ausführen. Sonst testet man mit der alten, gecachten Konfiguration (und wundert sich, warum die Änderungen nicht greifen).

Ein Funktiontest

Um die Wachhund-Funktion zu testen, wird das Beispiel minimal abgewandelt. Nach 20 s hört der Dienst auf, Nachrichten zu verschicken. Da der Watchdog noch 10 s geduldig wartet (WatchdogSec=10s) , und systemd danach noch 5 s für den Restart pausiert (RestartSec=5s) dauert es ca 25s, bis der Dienst neu gestartet wird.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <unistd.h> // for sleep()
#include <iostream>
#include <systemd/sd-daemon.h> // for sd_notify()

int main() {
	sd_notify (0, "READY=1");
	int counter = 0;
	while (true) {
		if (++counter < 20) {
			std::cout << "Service counts to " << counter << std::endl;
			sd_notify (0, "WATCHDOG=1");
		}
		sleep(1); // wait a second
	}
	return 0;
}

Wenn man jetzt den Dienst mit systemctl start startet, sieht man in journalctl folgende Ausgabe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ journalctl -eu watchdogtest.service
...
Sep 17 21:34:29 Shadowboxli ServiceWatchdogTest[20255]: Service counts to 15
Sep 17 21:34:30 Shadowboxli ServiceWatchdogTest[20255]: Service counts to 16
Sep 17 21:34:31 Shadowboxli ServiceWatchdogTest[20255]: Service counts to 17
Sep 17 21:34:32 Shadowboxli ServiceWatchdogTest[20255]: Service counts to 18
Sep 17 21:34:33 Shadowboxli ServiceWatchdogTest[20255]: Service counts to 19
Sep 17 21:34:43 Shadowboxli systemd[1]: watchdogtest.service: Watchdog timeout (limit 10s)!
Sep 17 21:34:43 Shadowboxli systemd[1]: watchdogtest.service: Killing process 20255 (ServiceWatchdog) with signal SIGABRT.
Sep 17 21:34:43 Shadowboxli systemd[1]: watchdogtest.service: Main process exited, code=dumped, status=6/ABRT
Sep 17 21:34:43 Shadowboxli systemd[1]: watchdogtest.service: Failed with result 'watchdog'.
Sep 17 21:34:48 Shadowboxli systemd[1]: watchdogtest.service: Scheduled restart job, restart counter is at 1.
Sep 17 21:34:48 Shadowboxli systemd[1]: Stopped watchdogtest.
Sep 17 21:34:48 Shadowboxli systemd[1]: Starting watchdogtest...
Sep 17 21:34:48 Shadowboxli ServiceWatchdogTest[20302]: Service counts to 1
Sep 17 21:34:48 Shadowboxli systemd[1]: Started watchdogtest.
Sep 17 21:34:49 Shadowboxli ServiceWatchdogTest[20302]: Service counts to 2
Sep 17 21:34:50 Shadowboxli ServiceWatchdogTest[20302]: Service counts to 3
Sep 17 21:34:51 Shadowboxli ServiceWatchdogTest[20302]: Service counts to 4
Sep 17 21:34:52 Shadowboxli ServiceWatchdogTest[20302]: Service counts to 5

21:34:33 ist die letzte Nachricht an den Watchdog geschickt worden, 21:34:43 fing der Wachhund an zu bellen und weitere 5 Sekunden später um 21:34:48 wurde der Dienst neu gestartet.

Man kann das auch gut mit btop überwachen (Filter auf “ServiceWatchDogTest” setzen) - alle 25 s oder so wird der alte Prozess gekillt und ein neuer gestartet, erkennbar durch die jeweils neue PID.

Nachrüsten eines Watchdogs für einen fertigen Dienst

Wenn man den Dienst selbst nicht anpassen kann, so kann man mit einem kleinen Bash-Script eine Überwachung außen herum bauen.

Die Definition des Watchdog-Supports in der .service-Datei ist genau wie oben, aber anstatt den Dienst direkt zu starten, führt man einfach folgende Bash-Datei aus (in StartExec= den Pfad zum Bash-Script eintragen):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash

# This script is a wrapper around a running process
# It starts a process and remembers its PID.
# Then it repeatedly checks if the process is still running, and if so
# sends a WATCHDOG notification to systemd.
# Systemd will restart the service (effectively this script) when
# the watchdog notification is missing.

# start daemon in background and grab its PID
/home/ghorwin/git/ServiceWatchdogTest/bin/debug/ServiceWatchdogTest &
PID=$!

# signal ready to systemd
/bin/systemd-notify --ready

while(true); do
    FAIL=0

    # test-send a signal to the process; will return 0 if still alive
    kill -0 $PID
    if [[ $? -ne 0 ]]; then FAIL=1; fi

    if [[ $FAIL -eq 0 ]]; then /bin/systemd-notify WATCHDOG=1; fi

    sleep 1
done

Wenn man nun nach einer Weile den Prozess abschießt, sieht das im journalctl Log so aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Sep 17 22:22:26 Shadowboxli ServiceWatchdogTest-wrapper.sh[27290]: Service counts to 292
Sep 17 22:22:27 Shadowboxli ServiceWatchdogTest-wrapper.sh[27290]: Service counts to 293
Sep 17 22:22:28 Shadowboxli ServiceWatchdogTest-wrapper.sh[27290]: Service counts to 294
Sep 17 22:22:29 Shadowboxli ServiceWatchdogTest-wrapper.sh[27290]: Service counts to 295
Sep 17 22:22:31 Shadowboxli ServiceWatchdogTest-wrapper.sh[27289]: /home/ghorwin/git/ServiceWatchdogTest/systemd/ServiceWatchdogTest-wrapper.sh: Zeile 21: kill: (27290) - Kein passender >
Sep 17 22:22:32 Shadowboxli ServiceWatchdogTest-wrapper.sh[27289]: /home/ghorwin/git/ServiceWatchdogTest/systemd/ServiceWatchdogTest-wrapper.sh: Zeile 21: kill: (27290) - Kein passender >
Sep 17 22:22:33 Shadowboxli ServiceWatchdogTest-wrapper.sh[27289]: /home/ghorwin/git/ServiceWatchdogTest/systemd/ServiceWatchdogTest-wrapper.sh: Zeile 21: kill: (27290) - Kein passender >
Sep 17 22:22:34 Shadowboxli ServiceWatchdogTest-wrapper.sh[27289]: /home/ghorwin/git/ServiceWatchdogTest/systemd/ServiceWatchdogTest-wrapper.sh: Zeile 21: kill: (27290) - Kein passender >
Sep 17 22:22:35 Shadowboxli ServiceWatchdogTest-wrapper.sh[27289]: /home/ghorwin/git/ServiceWatchdogTest/systemd/ServiceWatchdogTest-wrapper.sh: Zeile 21: kill: (27290) - Kein passender >
Sep 17 22:22:36 Shadowboxli ServiceWatchdogTest-wrapper.sh[27289]: /home/ghorwin/git/ServiceWatchdogTest/systemd/ServiceWatchdogTest-wrapper.sh: Zeile 21: kill: (27290) - Kein passender >
Sep 17 22:22:37 Shadowboxli ServiceWatchdogTest-wrapper.sh[27289]: /home/ghorwin/git/ServiceWatchdogTest/systemd/ServiceWatchdogTest-wrapper.sh: Zeile 21: kill: (27290) - Kein passender >
Sep 17 22:22:38 Shadowboxli ServiceWatchdogTest-wrapper.sh[27289]: /home/ghorwin/git/ServiceWatchdogTest/systemd/ServiceWatchdogTest-wrapper.sh: Zeile 21: kill: (27290) - Kein passender >
Sep 17 22:22:39 Shadowboxli ServiceWatchdogTest-wrapper.sh[27289]: /home/ghorwin/git/ServiceWatchdogTest/systemd/ServiceWatchdogTest-wrapper.sh: Zeile 21: kill: (27290) - Kein passender >
Sep 17 22:22:40 Shadowboxli ServiceWatchdogTest-wrapper.sh[27289]: /home/ghorwin/git/ServiceWatchdogTest/systemd/ServiceWatchdogTest-wrapper.sh: Zeile 21: kill: (27290) - Kein passender >
Sep 17 22:22:40 Shadowboxli systemd[1]: watchdogtest.service: Watchdog timeout (limit 10s)!
Sep 17 22:22:40 Shadowboxli systemd[1]: watchdogtest.service: Killing process 27289 (ServiceWatchdog) with signal SIGABRT.
Sep 17 22:22:40 Shadowboxli systemd[1]: watchdogtest.service: Killing process 28716 (sleep) with signal SIGABRT.
Sep 17 22:22:40 Shadowboxli systemd[1]: watchdogtest.service: Main process exited, code=dumped, status=6/ABRT
Sep 17 22:22:40 Shadowboxli systemd[1]: watchdogtest.service: Failed with result 'watchdog'.
Sep 17 22:22:40 Shadowboxli systemd[1]: watchdogtest.service: Consumed 2.391s CPU time.
Sep 17 22:22:45 Shadowboxli systemd[1]: watchdogtest.service: Scheduled restart job, restart counter is at 2.
Sep 17 22:22:45 Shadowboxli systemd[1]: Stopped watchdogtest.
Sep 17 22:22:45 Shadowboxli systemd[1]: watchdogtest.service: Consumed 2.391s CPU time.
Sep 17 22:22:45 Shadowboxli systemd[1]: Starting watchdogtest...
Sep 17 22:22:45 Shadowboxli systemd[1]: Started watchdogtest.
Sep 17 22:22:45 Shadowboxli ServiceWatchdogTest-wrapper.sh[29120]: Service counts to 1
Sep 17 22:22:46 Shadowboxli ServiceWatchdogTest-wrapper.sh[29120]: Service counts to 2
Sep 17 22:22:47 Shadowboxli ServiceWatchdogTest-wrapper.sh[29120]: Service counts to 3
Sep 17 22:22:48 Shadowboxli ServiceWatchdogTest-wrapper.sh[29120]: Service counts to 4

Im Prinzip ist das eine leichtgewichtige Variante, aber funktioniert nur, wenn der überwachte Prozess crasht oder sonstwie beendet wird. Ein Steckenbleiben in einer Endlosschleife wird so nicht erkannt.

Hier ist noch ein voll funktionsfähiges Beispiel: ServiceWatchdogTest-Example.7z

This post is licensed under CC BY 4.0 by the author.