Server monitoringbeginner4 min lezen

systemd-services bewaken en automatisch herstarten: Restart=, OnFailure=, watchdogs — en wanneer dat niet genoeg is

Een service die crasht is met Restart=on-failure in seconden terug. Een service die hangt, een die in een restart-loop zit, of een die na een reboot nooit is gestart, ziet systemd niet vanzelf. Dit zijn de drop-in-instellingen, de OnFailure-handler die naar je telefoon pusht, de WatchdogSec-truc voor hangende processen en het cron-script dat de gaten dicht.

Inhoud
  1. Stap 1: weet wat er nu niet draait
  2. Stap 2: Restart= goed zetten (met een drop-in, niet in de unit zelf)
  3. Stap 3: OnFailure= — een alert naar je telefoon, ingebouwd
  4. Stap 4: hangende services — de watchdog
  5. Stap 5: het cron-script dat de gaten dicht
  6. Valkuilen
  7. Wat je hiermee nog niet hebt
  8. Zo doet monsys het
  9. FAQ

De meeste "de service is down"-tickets hebben een saaie oorzaak: het proces crashte om 03:12 en niemand had Restart= ingesteld; of het draaide wel, maar antwoordde niet meer; of het stond na een reboot van vorige week gewoon uit omdat niemand enable had gedaan. systemd kan alle drie afvangen — maar niet met de standaardinstellingen, en niet zonder dat je erover nadenkt wat "gezond" voor die service betekent.

Stap 1: weet wat er nu niet draait

# Units in failed-state (crash, exit ≠ 0, start-limit bereikt)
systemctl --failed
# Services die enabled zijn maar NIET draaien (bv. na een reboot niet opgekomen)
systemctl list-unit-files --type=service --state=enabled --no-legend | awk '$1 !~ /@\.service$/ {print $1}' \
  | while read -r u; do
      # oneshots en units waarvan de Condition niet klopt (cloud-init op bare metal) horen inactive te zijn
      [ "$(systemctl show "$u" -p Type --value)" = oneshot ] && continue
      [ "$(systemctl show "$u" -p ConditionResult --value)" = no ] && continue
      # netjes beëindigd (Result=success, bv. cloud-init-main) is geen storing; een crash geeft exit-code/signal
      [ "$(systemctl show "$u" -p Result --value)" = success ] && ! systemctl is-active --quiet "$u" && continue
      systemctl is-active --quiet "$u" || echo "enabled-but-inactive: $u"
    done
# Hoe vaak is een service herstart sinds boot?
systemctl show nginx -p NRestarts
# Waarom faalde hij?
journalctl -u nginx -b -p warning --no-pager | tail -20

Die tweede lijst is de verrassende: units die "enabled" zijn maar inactive. Oneshots, template-units en units waarvan de Condition… niet klopt (cloud-init op een fysieke machine) horen daar te staan; een webserver niet — daarom filtert de loop die drie eruit.

Stap 2: Restart= goed zetten (met een drop-in, niet in de unit zelf)

Wijzig nooit /lib/systemd/system/*.service — een package-update overschrijft het. Een drop-in in /etc/systemd/system/<unit>.service.d/ overleeft alles.

sudo systemctl edit nginx
[Unit]
# Start-limit: standaard 5 pogingen in 10 s, daarna failed en NIETS meer. Dat is te krap voor
# een service die op een database wacht. Ruimer venster.
StartLimitIntervalSec=300
StartLimitBurst=10

[Service]
# on-failure: bij crash of exit ≠ 0. always: ook bij nette exit 0 (voor daemons die "klaar" denken te zijn).
Restart=on-failure
RestartSec=5s

Let op de sectie: StartLimitIntervalSec en StartLimitBurst horen in [Unit] (sinds systemd 230); oudere voorbeelden zetten ze in [Service], wat een waarschuwing geeft en op nieuwere versies genegeerd wordt.

Controleer daarna wat systemd effectief gebruikt:

sudo systemctl daemon-reload
systemctl show nginx -p Restart -p RestartUSec -p StartLimitBurst -p StartLimitIntervalUSec

Voor services met afhankelijkheden (app die een database nodig heeft): After=postgresql.service in [Unit], en in de app zelf een retry op de connectie. Requires= klinkt logisch maar zorgt dat je app mee stopt als je de database herstart — meestal niet wat je wilt.

Stap 3: OnFailure= — een alert naar je telefoon, ingebouwd

systemd kan een andere unit starten wanneer een service failed. Eén generieke notify-unit voor alle services:

sudo tee /etc/systemd/system/notify-failed@.service >/dev/null <<'EOF'
[Unit]
Description=Push failure of %i to ntfy

[Service]
Type=oneshot
# %i = de naam van de gefaalde unit (via OnFailure=notify-failed@%n.service)
ExecStart=/bin/sh -c 'journalctl -u %i -n 15 --no-pager -o cat | curl -s -H "Title: FAILED %i on $(hostname -s)" -H "Priority: high" --data-binary @- https://ntfy.example.be/services >/dev/null'
EOF

# Koppel aan een service via drop-in
sudo systemctl edit nginx
[Unit]
OnFailure=notify-failed@%n.service

%n is de volledige unit-naam (nginx.service); die wordt de instance van de notify-unit. Test: sudo systemctl kill -s SIGKILL nginx met Restart=no tijdelijk, of eenvoudiger een wegwerp-unit:

sudo systemd-run --unit=crashtest -p OnFailure=notify-failed@crashtest.service /bin/false
# → push met de laatste 15 journal-regels van crashtest

Let op: OnFailure vuurt pas als de unit echt failed — dus na het bereiken van de start-limit, niet bij elke individuele crash die door Restart= wordt opgevangen. Dat is precies goed: je wilt geen push per herstart, wel één als herstarten niet meer helpt.

Stap 4: hangende services — de watchdog

Restart= reageert op een proces dat stopt. Een proces dat vastloopt in een deadlock, met alle threads bezig, stopt niet. Voor services die systemd's sd_notify ondersteunen (nginx niet; veel Go/Rust/Python-daemons wel, en alles wat via systemd-notify communiceert), is er WatchdogSec=:

[Service]
Type=notify
WatchdogSec=30s
# Als het proces niet elke 30 s WATCHDOG=1 stuurt, krijgt het SIGABRT en herstart systemd het
Restart=on-watchdog

Ondersteunt je service dat niet, dan bouw je de watchdog buiten de service: een timer die een echte request doet en bij falen herstart.

sudo tee /etc/systemd/system/healthcheck-web.service >/dev/null <<'EOF'
[Unit]
Description=HTTP healthcheck for the web app; restart on failure
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'curl -fsS -m 5 -o /dev/null http://127.0.0.1:8080/healthz || { echo "healthz failed, restarting"; systemctl restart webapp.service; }'
EOF
sudo tee /etc/systemd/system/healthcheck-web.timer >/dev/null <<'EOF'
[Unit]
Description=Run web healthcheck every minute
[Timer]
OnBootSec=2min
OnUnitActiveSec=1min
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now healthcheck-web.timer

Twee regels bij zo'n externe watchdog: de healthcheck moet iets echts doen (een query, niet alleen "200 OK"), en de herstart moet gelogd én geteld worden — anders maskeer je een geheugenlek als "af en toe een herstart".

Stap 5: het cron-script dat de gaten dicht

Wat systemd zelf niet meldt: units die enabled-maar-inactive zijn, restart-loops die nét onder de start-limit blijven, en services die na een reboot niet terugkwamen.

sudo tee /usr/local/sbin/service-check.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
NTFY="https://ntfy.example.be/services"
HOST=$(hostname -s)
STATE=/var/lib/service-check.state
touch "$STATE"; MSG=()

# 1. Failed units
f=$(systemctl --failed --no-legend | awk '{print $1}' | paste -sd, -)
[ -n "$f" ] && MSG+=("failed: $f")

# 2. Enabled maar niet actief (templates, oneshots en units met niet-vervulde Condition uitgesloten)
while read -r u; do
  [ "$(systemctl show "$u" -p Type --value)" = oneshot ] && continue
  [ "$(systemctl show "$u" -p ConditionResult --value)" = no ] && continue
  systemctl is-active --quiet "$u" && continue
  # Netjes beëindigd (Result=success) = boot-job zoals cloud-init-main, geen storing.
  # Een daemon die onverwacht met exit 0 stopt, vang je met Restart=always (stap 2).
  [ "$(systemctl show "$u" -p Result --value)" = success ] && continue
  MSG+=("enabled-but-inactive: $u ($(systemctl show "$u" -p Result --value))")
done < <(systemctl list-unit-files --type=service --state=enabled --no-legend | awk '$1 !~ /@\.service$/ {print $1}')

# 3. Restart-loops: NRestarts gestegen met ≥ 3 sinds vorige check (10 min)
while read -r u; do
  n=$(systemctl show "$u" -p NRestarts --value); [ "${n:-0}" -eq 0 ] && continue
  prev=$(grep "^$u " "$STATE" | awk '{print $2}'); prev=${prev:-0}
  [ $((n - prev)) -ge 3 ] && MSG+=("restart-loop: $u ($((n - prev)) restarts in 10 min, total $n)")
  sed -i "/^$u /d" "$STATE"; echo "$u $n" >> "$STATE"
done < <(systemctl list-units --type=service --state=running --no-legend | awk '{print $1}')

[ ${#MSG[@]} -gt 0 ] && printf '%s\n' "${MSG[@]}" | curl -s -H "Title: services@$HOST" -H "Priority: high" --data-binary @- "$NTFY" >/dev/null
EOF
sudo chmod 0755 /usr/local/sbin/service-check.sh
echo '*/10 * * * * root /usr/local/sbin/service-check.sh' | sudo tee /etc/cron.d/service-check

Valkuilen

  • Restart=always op een oneshot. Een backup-script met Restart=always draait eindeloos opnieuw. Restart= is voor daemons; voor jobs gebruik je timers.
  • ExecStartPre dat faalt telt mee voor de start-limit. Een mkdir die faalt omdat een mount ontbreekt, brandt je tien pogingen op in seconden. Zet - voor het commando (ExecStartPre=-/bin/mkdir …) als falen acceptabel is.
  • systemctl restart in een healthcheck zonder rate-limit. Als de database weg is, herstart je elke minuut een app die toch niet werkt, en spamt je de logs vol. Tel de herstarts en stop na drie: dan is het een alert, geen fix.
  • KillMode= en kinderen. Een service die worker-processen spawnt (gunicorn, php-fpm): bij Restart= blijven oude workers soms hangen als KillMode=process. Standaard (control-group) is bijna altijd juist.
  • daemon-reload vergeten. Een drop-in zonder systemctl daemon-reload is een tekstbestand. systemctl show vertelt wat er echt actief is.

Wat je hiermee nog niet hebt

  • Historie. NRestarts reset bij elke boot. "Hoe vaak is deze service in de laatste 30 dagen herstart" vraagt een log die je zelf bijhoudt.
  • Fleet-zicht. Dertig servers, dertig --failed-lijsten, dertig ntfy-topics.
  • Context. De service die om 03:12 crashte en de apt-upgrade van 03:10 staan in twee logs; de correlatie is handwerk.

Zo doet monsys het

De monsys-agent leest de systemd-state van elke host: failed units, enabled-maar-inactive, NRestarts, restart-events met tijdstip, en de exit-status. Restart-loops en down-services worden gededupliceerde alerts met de journal-regels en de host-metrics van dat moment ernaast; de SLA-engine rekent per service een beschikbaarheidspercentage uit over 5-minuten-buckets, zodat "99,7 % vorige maand" een feit is en geen schatting. Herstarten blijft een handeling die een mens goedkeurt — via een ondertekende Emergency Action Token, met de output van systemctl restart in het audit-log.

FAQ

Herstart systemd een service automatisch na een crash?

Alleen als Restart= is ingesteld (on-failure of always); de standaard is no. En zelfs dan stopt systemd na StartLimitBurst pogingen binnen StartLimitIntervalSec en zet de unit op failed.

Hoe zie ik waarom een service failed is?

systemctl status <unit> toont de laatste regels; journalctl -u <unit> -b -p warning de volledige context sinds de boot. systemctl show <unit> -p Result -p ExecMainStatus geeft de exit-code en de reden (exit-code, signal, start-limit-hit, watchdog).

Werkt de watchdog met elke service?

Alleen met services die Type=notify gebruiken en periodiek WATCHDOG=1 sturen via sd_notify. Voor andere services bouw je een externe healthcheck met een timer, zoals in stap 4.

Geschreven door het monsys-team — sysadmins die dit dagelijks doen.

Zelf gedaan? Laat monsys het bijhouden.

Alles uit deze how-to draait in monsys als doorlopende check, met historie, alerts en audit-bewijs. 5 servers gratis, EU-gehost in België, geïnstalleerd in 60 seconden.