ZIM-НИЙ SAAALEЗимние скидки: до −50% на старт и −20% на продление
до 31.01.2026 Подробнее
Выберите продукт

systemd Watchdog: авто‑рестарт зависших веб‑приложений и проверки живости

Практическое руководство по systemd watchdog для веб‑сервисов: от директив и Type=notify до примеров sd_notify на Python/Node/Go/PHP. Показано, как связать healthcheck и watchdog, настроить таймауты и избежать перезапусков по кругу в продакшне.
systemd Watchdog: авто‑рестарт зависших веб‑приложений и проверки живости

Если веб‑приложение подвисло, но процесс не упал — классический перезапуск по Restart=on-failure не сработает. Эту дыру закрывает watchdog в systemd: сервис регулярно подтверждает свою «живость» через sd_notify, а при молчании менеджер считает его зависшим и делает restart. В итоге повышаем стабильность без внешних костылей и тяжелой оркестрации.

Как работает сервисный watchdog в systemd

На уровне единичного сервиса есть «программный» watchdog: в юните задаётся период ожидания через WatchdogSec=. Если процесс отмечается как готовый (READY=1) и периодически отправляет сигнал живости (WATCHDOG=1) по сокету уведомлений, systemd считает его здоровым. Как только подтверждения нет — фиксируется таймаут, по умолчанию отправляется WatchdogSignal=SIGABRT (можно настроить), а затем срабатывает политика Restart=.... Это позволяет рестартовать зависшие процессы, даже если они «висят» в системном плане.

Важно: программный watchdog привязан к Type=notify (или реализации уведомлений через sd_notify). Без уведомлений процесс будет регулярно попадать в таймаут. Именно поэтому сервису нужно либо уметь отправлять sd_notify, либо запускаться через обёртку, которая делает healthcheck и пингует watchdog от имени сервиса.

Если вы управляете сервисами на собственном сервере — удобно делать это на VDS, где есть полный доступ к systemd и логам.

Сценарии применения

  • Веб‑сервер приложения (например, API) зависает под нагрузкой: запросы не обслуживаются, но процесс жив — watchdog инициирует restart.
  • Воркер очереди «залип» на блокирующей операции: нет прогресса — перезапускаем.
  • Сервис готов к трафику не сразу: сначала отправляем READY=1 после инициализации, затем периодические WATCHDOG=1.

Про воркеры и очереди см. материал «Супервизия воркеров через systemd» — он дополняет эту статью по практикам перезапуска и мониторинга (подробности).

Базовый юнит с watchdog и авто‑рестартом

[Unit]
Description=MyApp web service with watchdog
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
NotifyAccess=all
ExecStart=/opt/myapp/bin/myapp
WorkingDirectory=/opt/myapp
Environment=MYAPP_ENV=prod

# Включаем watchdog и перезапуск
WatchdogSec=30s
WatchdogSignal=SIGABRT
Restart=on-failure
RestartSec=5s
StartLimitBurst=5
StartLimitIntervalSec=10min

# Аккуратное завершение
TimeoutStartSec=60s
TimeoutStopSec=30s
KillMode=mixed

[Install]
WantedBy=multi-user.target

Пояснения к ключевым директивам

  • Type=notify — сервис должен посылать уведомления через sd_notify; иначе watchdog будет считать его зависшим.
  • NotifyAccess=all — разрешает отправку уведомлений любым процессам в cgroup сервиса; удобно, если пинг шлёт обёртка/дочерний процесс.
  • WatchdogSec=30s — дедлайн для подтверждения живости. Посылать пинги лучше каждые WatchdogSec/2.
  • WatchdogSignal=SIGABRT — полезно для снятия core dump при зависании.
  • Restart=on-failure, RestartSec=5s — политика перезапуска после фейла/таймаута.
  • StartLimit* — защита от перезапусков по кругу.

Юнит systemd с WatchdogSec и Type=notify: что где настроить

Готовность и живость — разные сигналы. Сначала сервис инициализируется и отправляет READY=1; только после этого его перестают считать «стартующим». Дальше он регулярно отправляет WATCHDOG=1 для подтверждения живости (healthcheck/liveness).

Где взять sd_notify и как его использовать

systemd выставляет переменные окружения, которые помогут настроить период пингов:

  • NOTIFY_SOCKET — путь к сокету уведомлений. Если переменной нет — уведомления не требуются/не включены.
  • WATCHDOG_USEC — окно watchdog в микросекундах. Рекомендуемый период пингов — WATCHDOG_USEC/2.

Минимальная обёртка на shell

#!/usr/bin/env bash
set -euo pipefail

# Сообщаем: сервис готов
systemd-notify --ready --status="booting"

# Запускаем приложение фоном
/opt/myapp/bin/myapp &
APP_PID=$!

# Период пинга из WATCHDOG_USEC
INTERVAL_USEC=${WATCHDOG_USEC:-0}
if [ "$INTERVAL_USEC" -gt 0 ]; then
  SLEEP_SEC=$(( INTERVAL_USEC / 2000000 ))
else
  SLEEP_SEC=5
fi

# Цикл healthcheck + watchdog
while kill -0 "$APP_PID" 2>/dev/null; do
  curl -fsS http://127.0.0.1:8080/health > /dev/null || break
  systemd-notify --status="ok" WATCHDOG=1
  sleep "$SLEEP_SEC"
done

# Нет здорового ответа — выходим с ошибкой, даст перезапуск
exit 1

В юните тогда указываем ExecStart=/opt/myapp/bin/watchdog-wrapper.sh и сохраняем Type=notify. Благодаря NotifyAccess=all пинг может отправлять не только основной PID.

Python: systemd.daemon

from systemd.daemon import notify, Notification
import os
import time
import requests

notify(Notification.READY, status="booted")
interval = int(os.getenv("WATCHDOG_USEC", "0"))
period = interval / 2 / 1_000_000 if interval else 5

while True:
    try:
        r = requests.get("http://127.0.0.1:8080/health", timeout=2)
        r.raise_for_status()
        notify(Notification.WATCHDOG, status="ok")
    except Exception:
        raise SystemExit(1)
    time.sleep(period)

Node.js: sd-notify

const http = require("http");
const notify = require("sd-notify");

notify.ready();
const usec = parseInt(process.env.WATCHDOG_USEC || "0", 10) || 0;
const period = usec ? usec / 2000000 : 5;

setInterval(() => {
  http.get("http://127.0.0.1:8080/health", res => {
    if (res.statusCode === 200) notify.watchdog();
    else process.exit(1);
  }).on("error", () => process.exit(1));
}, period * 1000);

Go: coreos/go-systemd

package main

import (
  "net/http"
  "os"
  "strconv"
  "time"
  daemon "github.com/coreos/go-systemd/v22/daemon"
)

func main() {
  daemon.SdNotify(false, "READY=1")
  usec, _ := strconv.ParseInt(os.Getenv("WATCHDOG_USEC"), 10, 64)
  period := time.Duration(usec) * time.Microsecond / 2
  if period == 0 {
    period = 5 * time.Second
  }

  ticker := time.NewTicker(period)
  for range ticker.C {
    resp, err := http.Get("http://127.0.0.1:8080/health")
    if err != nil || resp.StatusCode != 200 {
      os.Exit(1)
    }
    daemon.SdNotify(false, "WATCHDOG=1")
  }
}

PHP: отправка в NOTIFY_SOCKET

<?php
$socketPath = getenv('NOTIFY_SOCKET');
if ($socketPath) {
    $sock = @stream_socket_client("unixgram://$socketPath");
    if ($sock) {
        fwrite($sock, "READY=1");
        // Далее периодически: fwrite($sock, "WATCHDOG=1");
    }
}

Healthcheck: что именно проверять

Локальный healthcheck — не просто «порт открыт». Хорошая проверка должна покрывать:

  • Быстрый SQL/Ping к критичной БД или пулу соединений.
  • Проверка очереди/кеша (доступ к Redis/AMQP с таймаутом).
  • Минимальный внутриядерный маршрут (например, выполнение лёгкой бизнес‑функции).

Важно соблюдать баланс: healthcheck должен быть лёгким, с коротким таймаутом, и не должен падать из‑за внешних временных деградаций, если сервис всё ещё способен обслуживать трафик. Для различения уровней состояния полезно иметь два эндпойнта: readiness (для READY=1) и liveness (для WATCHDOG=1), с разной глубиной проверок.

Управление перезапусками: чтобы не получить цикл

  • Restart=on-failure — стартуем заново только при ненулевом коде выхода или таймауте watchdog.
  • RestartSec=5s — пауза перед перезапуском; не ставьте слишком мало, иначе усилите шторм.
  • StartLimitBurst и StartLimitIntervalSec — ограничение «перезапусков за окно»; при превышении сервис уходит в StartLimitHit.
  • RuntimeMaxSec=24h — опционально: мягко перезапускать раз в сутки для «омоложения» воркеров с вероятной фрагментацией памяти.

Поток sd_notify: READY, периодические WATCHDOG и рестарт при таймауте

Диагностика: как понять, что watchdog работает

# Проверить свойства юнита
systemctl show myapp.service -p WatchdogUSec,WatchdogTimestamp,ActiveState,SubState,StatusText

# Логи с отметками watchdog
journalctl -u myapp.service -g watchdog -e

# Верификация синтаксиса юнита
systemd-analyze verify /etc/systemd/system/myapp.service

# Онлайн-статус
systemctl status myapp.service

В логах увидите записи вида «Watchdog timeout» и сигнал, отправленный процессу. Если StatusText меняется (мы его передавали через --status/параметр библиотеки), это знак, что уведомления доходят.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Типичные ошибки и ловушки

  • Забыли Type=notify, включили WatchdogSec — получите постоянные таймауты.
  • Процесс пингует реже, чем требуется: ориентируйтесь на WATCHDOG_USEC, а не на «жёсткие» значения.
  • Обёртка шлёт пинги, даже если приложение «мертвое»: связывайте пинг с реальным healthcheck.
  • Нет NotifyAccess=all, а пинг идёт из дочернего процесса — уведомления игнорируются.
  • Слишком агрессивные таймауты: у приложения не остаётся времени на GC/компиляцию/прогрев — фиксируйте реальными метриками.
  • Не настроен TimeoutStopSec и аккуратные сигналы: сервис рвётся «жёстко», теряются запросы.

Рекомендованные интервалы и таймауты

  • Веб‑API на Python/Node/Go/PHP: WatchdogSec=30s, пинг каждые 10–15s, TimeoutStartSec=60s, TimeoutStopSec=30s.
  • Воркер очереди: WatchdogSec=60s с пингом каждые 20–30s; добавьте проверку «недвижущейся» метрики offset/lag.
  • «Тяжёлый» старт (JIT/кеш): поднимите TimeoutStartSec до 120–180s и отправляйте READY=1 после прогрева.

Всегда замеряйте реальные времена старта и стопа, моделируйте пиковую нагрузку и накладывайте 2–3× запас.

Плавное завершение и выкачка трафика

Стабильность — это не только restart, но и корректный stop. Советы:

  • KillMode=mixed или control-group — чтобы дочерние процессы не оставались зомби.
  • KillSignal=SIGTERM по умолчанию достаточен; убедитесь, что приложение ловит сигнал и перестаёт принимать новые запросы.
  • TimeoutStopSec=30s — дайте время на «дренаж» соединений; затем последует SIGKILL.
  • В Nginx/прокси настройте небольшой grace period и ретраи, чтобы перезапуск прошёл без 5xx.

Если поднимаете сервисы на VDS, совместите watchdog с ограничениями и изоляцией системных юнитов — это снижает blast radius сбоев (харденинг systemd на VDS).

Отделяем readiness от liveness

Хорошая практика — различать «готов к приёму трафика» и «жив»: отправляйте READY=1 только после прогрева (миграции, прогрев кеша, загрузка моделей и т.д.). А WATCHDOG=1 — регулярно, опираясь на лёгкий healthcheck. Так вы избежите фальш‑позитивов на старте и не будете залочены долгими readiness‑проверками в штатной работе.

Связка с веб‑стеком

В контуре Nginx/Apache держите локальный эндпойнт /health, недоступный извне, с короткими таймаутами и минимальной логикой. На стороне приложения фиксируйте статус в журналах: при каждом WATCHDOG=1 можно обновлять «пульс» в логах, что сильно помогает в разборе инцидентов.

Проверочный чек‑лист

  • Юнит: Type=notify, WatchdogSec задан, NotifyAccess=all при необходимости.
  • Приложение или обёртка отправляют READY=1 и регулярный WATCHDOG=1 с периодом WATCHDOG_USEC/2.
  • Пинг завязан на валидный healthcheck с коротким таймаутом.
  • Перезапуски ограничены StartLimit*, интервалы адекватны нагрузке.
  • Логи читаемы, статус обновляется, диагностика очевидна.

Итоги

Сервисный watchdog в systemd закрывает важный класс отказов: зависание без краша. Правильная связка Type=notify + sd_notify + healthcheck превращает «залипы» в контролируемые перезапуски, а чёткие таймауты и лимиты запуска добавляют предсказуемости. Это лёгкий способ поднять стабильность продакшна без лишней сложности.

Поделиться статьей

Вам будет интересно

Loki 429: too many outstanding requests при ingestion — причины и пошаговое исправление OpenAI Статья написана AI (GPT 5)

Loki 429: too many outstanding requests при ingestion — причины и пошаговое исправление

Loki 429 too many outstanding requests (ingestion) означает, что контур приёма логов не успевает: упёрлись в лимиты, взорвали labe ...
Nginx: как перестать отдавать старый контент после деплоя (cache, open_file_cache, CDN purge) OpenAI Статья написана AI (GPT 5)

Nginx: как перестать отдавать старый контент после деплоя (cache, open_file_cache, CDN purge)

После деплоя часть пользователей видит старые CSS/JS или HTML: виноваты open_file_cache, proxy/fastcgi cache, заголовки Cache-Cont ...
HTTPS timeout на мобильных: MTU/PMTUD, blackhole MTU и MSS clamping в Linux OpenAI Статья написана AI (GPT 5)

HTTPS timeout на мобильных: MTU/PMTUD, blackhole MTU и MSS clamping в Linux

Если HTTPS работает с ПК, но в LTE/5G «висит» или таймаутится, часто виноват MTU/PMTUD: крупные пакеты теряются, ICMP блокируют, п ...