Если веб‑приложение подвисло, но процесс не упал — классический перезапуск по 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*— защита от перезапусков по кругу.

Готовность и живость — разные сигналы. Сначала сервис инициализируется и отправляет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— опционально: мягко перезапускать раз в сутки для «омоложения» воркеров с вероятной фрагментацией памяти.

Диагностика: как понять, что 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/параметр библиотеки), это знак, что уведомления доходят.
Типичные ошибки и ловушки
- Забыли
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 превращает «залипы» в контролируемые перезапуски, а чёткие таймауты и лимиты запуска добавляют предсказуемости. Это лёгкий способ поднять стабильность продакшна без лишней сложности.


