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 istnetwork.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 schickenNotifyAccess=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 auchRestart=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