Выберите продукт

systemd unit: Type=, ExecStart, Restart= и watchdog на практике

Разбираем unit-файлы systemd на практике: как выбрать Type= (simple/notify/forking), написать ExecStart без shell-обвязок, настроить Restart=on-failure и лимиты перезапусков. Плюс WatchdogSec для зависаний и зависимости After/Wants с частыми ошибками.
systemd unit: Type=, ExecStart, Restart= и watchdog на практике

Зачем вообще разбираться в unit-файлах

В продакшене «просто запустить бинарник» почти никогда не достаточно. Обычно нужно, чтобы сервис:

  • стартовал в правильный момент (после сети, дисков, сокетов, базовых таргетов);
  • давал предсказуемый статус готовности (readiness), а не «процесс запущен»;
  • перезапускался при падениях, но не уходил в вечную restart-loop;
  • умел сигнализировать о зависаниях (watchdog), а не только о падениях;
  • оставлял диагностику в journald, чтобы разбор инцидентов был быстрым.

Всё это описывается в одном месте — в systemd unit-файле. Ниже — практический разбор ключевых директив: Type=, ExecStart, Restart=, WatchdogSec=, а также After/Wants и ограничения StartLimitIntervalSec.

База: из чего состоит systemd unit

Для сервисов используется файл вида /etc/systemd/system/myapp.service (или drop-in в /etc/systemd/system/myapp.service.d/*.conf). Структура стандартная:

  • [Unit] — зависимости, порядок запуска, лимиты на «шторм» перезапусков;
  • [Service] — как запускать процесс, тип, перезапуск, watchdog;
  • [Install] — подключение в автозагрузку через таргеты.

Убедиться, что systemd видит итоговую конфигурацию (с учётом drop-in), удобнее так:

systemctl cat myapp.service
systemctl show myapp.service
systemd-analyze verify /etc/systemd/system/myapp.service

Если вы переносите приложение на сервер и хотите минимум сюрпризов при рестартах и обновлениях, чаще всего удобнее работать на VDS: там полный контроль над systemd, journald и политиками запуска.

Type=: как systemd понимает «сервис запущен»

Параметр Type= — это договор между systemd и вашим сервисом о том, когда считать запуск успешным. Неверный выбор обычно проявляется так:

  • systemd считает сервис поднятым слишком рано, и зависимости стартуют до готовности;
  • юнит зависает в activating, потому что systemd ждёт сигнал готовности, который никто не отправляет;
  • таймауты старта выглядят «рандомно», хотя причина — в модели readiness.

Type=simple (по умолчанию)

Type=simple — systemd запускает ExecStart и считает сервис стартовавшим сразу после запуска процесса, без ожидания готовности.

Подходит для:

  • простых демонов без длительной инициализации;
  • сервисов, которые не форкаются;
  • кейсов, где readiness не критичен (или решён на уровне балансера или оркестратора).

Type=forking

Type=forking нужен для «классических» демонов, которые форкаются в фон и завершают родительский процесс. systemd ждёт завершения родителя и затем считает, что сервис стартовал.

Практический момент: почти всегда для forking нужен PIDFile=, иначе systemd может неверно определить главный PID и начнутся странности с рестартами и статусом.

Type=notify

Type=notify — лучший вариант, если приложение умеет отправлять systemd сигнал готовности через sd_notify (или совместимый механизм). Тогда systemd считает сервис поднятым только после сообщения READY=1.

Что это даёт на практике:

  • зависимости стартуют после реальной готовности;
  • watchdog встраивается естественно (тот же канал уведомлений);
  • статусы и таймауты становятся предсказуемыми.

Если вы поставили Type=notify, но приложение не отправляет notify, unit обычно будет висеть в activating до TimeoutStartSec=. Перед включением notify проверьте документацию приложения или наличие флагов и библиотек для systemd integration.

Сервис «долго стартует»: что выбрать

Если сервис поднимается 30–90 секунд (миграции, прогрев кэшей, подключение к кластеру), Type=simple часто приводит к гонкам. Самый чистый путь — Type=notify. Если notify недоступен, лучше разделить жизненный цикл: отдельная подготовка (oneshot) и отдельный старт основного процесса.

Пример unit-файла systemd с акцентом на Type=notify и ExecStart

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

ExecStart: как запускать процесс правильно

ExecStart — команда запуска. Звучит просто, но именно здесь чаще всего создают проблемы, которые потом проявляются в рестартах, PID’ах и «пропавших» логах.

Правило №1: один «главный» процесс

systemd должен понимать, какой процесс считать основным. Поэтому:

  • избегайте запуска через shell-цепочки;
  • не используйте ExecStart как мини-скрипт с пайпами;
  • подготовку выносите в ExecStartPre или отдельный oneshot.

Плохо (shell-логика скрывает реальный процесс и усложняет остановку и рестарт):

ExecStart=/bin/sh -c 'myapp --flag | tee /var/log/myapp.log'

Лучше: писать в stdout/stderr и забирать логи через journald (или отдельный лог-агент). Если нужен файл, это обычно решается вне ExecStart.

Правило №2: не демонизируйте процесс внутри unit

В systemd-мире процесс обычно должен оставаться в форграунде. Флаги вида --daemon, --background, -d чаще всего не нужны (исключение — legacy-демоны с Type=forking).

ExecStart и переменные окружения

Если нужны параметры окружения (DSN, флаги, пути), не «вшивайте» их в команду. Используйте Environment= или EnvironmentFile= — так проще сопровождать и меньше риск случайно засветить значения в истории и диагностике.

Важно: переменные окружения часто можно увидеть локально при наличии прав на просмотр процессов. Для секретов лучше применять специализированные механизмы хранения секретов и ограничение доступа, но это отдельная тема.

Минимальный пример unit с ExecStart

[Unit]
Description=MyApp
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/myapp --config /etc/myapp/config.yml

[Install]
WantedBy=multi-user.target

After/Wants: порядок и зависимости без самострела

Два часто путаемых параметра: After и Wants.

After= — только порядок

After= говорит systemd: «стартуй этот unit после указанного», но не делает его обязательным. Если указанный юнит не запускается, ваш сервис всё равно может стартовать (просто порядок применяется, когда оба участвуют).

Wants= — «хочу, но не критично»

Wants= добавляет мягкую зависимость: при старте вашего сервиса systemd попытается запустить и указанный unit, но его падение не остановит запуск вашего.

Когда нужен Requires=

Если без зависимости сервис бессмысленен (например, без смонтированного тома, без локального сокета, без обязательного сервиса-прокси), используйте Requires=. Но помните: жёсткие зависимости повышают риск каскадного отказа.

Практический шаблон для сети

Очень частый кейс: сервису нужна именно «поднятая сеть», а не просто network.target. Обычно используют:

[Unit]
After=network-online.target
Wants=network-online.target

Это не гарантия доступности внешних ресурсов (DNS, маршрут, конкретная БД), но заметно снижает гонки при буте.

Если у вас воркеры и очереди под управлением systemd, полезно свериться с типовыми паттернами из статьи про воркеры systemd вместо supervisor — там хорошо видно, как зависимости и рестарты влияют на стабильность.

Restart=: перезапуск без бесконечной петли

Restart= определяет, в каких случаях systemd перезапускает сервис. Это мощно, но легко сделать «вечный молоток», который только прячет проблему и превращает логи в шум.

Restart=on-failure

Restart=on-failure перезапустит сервис, если тот завершился с ошибкой: ненулевой код выхода, сигнал, timeout и т.п. Обычно это лучший стартовый выбор для веб-приложений и воркеров.

Рекомендуемые дополнения:

  • RestartSec= — пауза перед перезапуском, чтобы не устраивать шторм;
  • TimeoutStartSec= — адекватный таймаут на старт;
  • ограничители StartLimit, чтобы не долбиться в стену бесконечно.

Restart=always: используйте осознанно

Restart=always перезапускает почти всегда, включая «нормальный» выход. Это бывает полезно для некоторых воркеров, но опасно, если приложение завершилось намеренно (например, при фатальной конфигурации): вы получите restart-loop и замусоренные логи.

Restart=no — это тоже нормально

Для oneshot-задач, миграций и импортов, которые должны выполниться один раз, Restart=no часто правильнее. Иногда лучше упасть и дать понятный алерт, чем «лечить» проблему бесконечными перезапусками.

StartLimitIntervalSec и StartLimitBurst: защита от restart-loop

Директивы StartLimitIntervalSec и StartLimitBurst (в современных systemd — в секции [Unit]) ограничивают количество стартов за интервал времени. Типовая логика: «не более N стартов за M секунд».

Если лимит превышен, unit уходит в failed и перестаёт перезапускаться до вмешательства администратора (или до сброса состояния).

[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Restart=on-failure
RestartSec=5s

Как читать: за последние 300 секунд допускается до 5 попыток старта. Если приложение падает сразу после запуска и это повторяется — systemd остановится после 5 попыток.

Сброс «лимита стартов»

После исправления причины иногда нужно сбросить счётчики:

systemctl reset-failed myapp.service
systemctl start myapp.service

WatchdogSec=: обнаруживаем зависания, а не только падения

Restart=on-failure помогает, когда процесс упал. Но хуже, когда он не падает, а завис (deadlock, зависший I/O, заблокированная очередь). Для этого в systemd есть watchdog: WatchdogSec=.

Как работает watchdog в systemd

  • вы задаёте WatchdogSec=30s;
  • systemd ожидает, что сервис будет регулярно «пинговать» менеджер через sd_notify с WATCHDOG=1;
  • если пинга нет — systemd считает сервис неотзывчивым и применяет политику завершения и перезапуска.

На практике watchdog почти всегда завязан на Type=notify, потому что протокол один: systemd notification socket.

Минимальный пример с WatchdogSec

[Unit]
Description=MyApp with watchdog
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Type=notify
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/myapp --config /etc/myapp/config.yml
Restart=on-failure
RestartSec=5s
WatchdogSec=30s
TimeoutStartSec=60s

[Install]
WantedBy=multi-user.target

Важно: если приложение не отправляет watchdog-пинги, оно будет регулярно убиваться как «зависшее». Сначала подтвердите поддержку notify и watchdog самим приложением.

Как понять, что watchdog реально работает

Смотрите состояние и логи:

systemctl status myapp.service
journalctl -u myapp.service -b

Если сервис умеет читать параметры systemd (часто через окружение), полезно проверить, какое значение watchdog-интервала реально передаётся процессу: конкретика зависит от языка и фреймворка.

Логи journalctl и статус systemctl для сервиса с перезапусками и watchdog

Виртуальный хостинг FastFox
Виртуальный хостинг для сайтов
Универсальное решение для создания и размещения сайтов любой сложности в Интернете от 95₽ / мес

Связка Type=notify + readiness + зависимости: типовой продовый паттерн

В реальном мире чаще всего нужно: «сервис считается готовым, когда открыл порт, прогрел кэш и прошёл self-check». Тогда:

  • Type=notify даёт честный readiness;
  • After=/Wants= задают порядок запуска по инфраструктуре;
  • Restart=on-failure страхует падения;
  • WatchdogSec= ловит зависания;
  • StartLimitIntervalSec защищает от бесконечных циклов.

В стеке из нескольких сервисов это особенно заметно: «правильная» готовность одного компонента убирает большую часть флапающих стартов остальных.

Диагностика: что делать, если unit ведёт себя странно

1) Сервис вечно в activating

Частая причина — Type=notify без реального notify. Проверьте:

systemctl status myapp.service
journalctl -u myapp.service -b --no-pager

Для проверки гипотезы можно временно переключить на Type=simple (а затем вернуть корректный readiness).

2) Сервис перезапускается слишком часто

Смотрите причину завершения и коды:

systemctl show myapp.service -p ExecMainStatus -p ExecMainCode -p Result
journalctl -u myapp.service -b -n 200 --no-pager

Убедитесь, что RestartSec= не нулевой, а StartLimitIntervalSec/StartLimitBurst выставлены. Если хотите дополнительно поднять общий уровень «прочности» сервисов, пригодится материал про systemd sandboxing и hardening.

3) «Зависимости есть, но порядок не тот»

Напоминание: After= — это порядок, Wants= — мягкое притягивание. Если нужно «не стартовать без X», используйте Requires= (часто вместе с After=).

Чек-лист перед выкладкой unit в прод

  • Выбран правильный Type=: если есть notify — используйте Type=notify, иначе simple (или forking для legacy).
  • ExecStart запускает один главный процесс без shell-обвязок и самодемонизации.
  • Restart=on-failure плюс RestartSec= настроены осмысленно.
  • Анти-шторм: StartLimitIntervalSec и StartLimitBurst выставлены.
  • Watchdog включён только если приложение реально его поддерживает, и интервал не слишком агрессивный.
  • Зависимости выражены корректно: After/Wants для порядка и мягких связей, Requires — только когда действительно нужно.
  • Проверка: systemd-analyze verify и тестовый старт и стоп с просмотром journald.

Итог

Хороший systemd unit не просто «запускает бинарник», а описывает ожидаемое поведение сервиса при старте, сбоях и зависаниях. Правильно подобранные Type=, аккуратный ExecStart, разумный Restart=on-failure, watchdog через WatchdogSec= и ограничения StartLimitIntervalSec превращают запуск в предсказуемый механизм, а не в набор случайностей.

Дальше по сложности обычно идут ExecReload, RuntimeDirectory, status-строки через systemd-notify --status, шаблоны unit’ов и drop-in overrides под разные окружения.

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

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

Debian/Ubuntu: как исправить Device is busy у Docker network, volume и namespace OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить Device is busy у Docker network, volume и namespace

Если Docker на Debian или Ubuntu отвечает Device is busy при удалении сети, тома или namespace, причина обычно в живом процессе, о ...
Debian/Ubuntu: как исправить Host key verification failed в Ansible при смене IP OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить Host key verification failed в Ansible при смене IP

Ошибка Host key verification failed в Ansible на Debian и Ubuntu обычно возникает после переустановки сервера, смены IP или повтор ...
Debian/Ubuntu: duplicate address detected, DAD failed IPv6 — причины и исправление OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: duplicate address detected, DAD failed IPv6 — причины и исправление

Сообщения duplicate address detected и DAD failed в Debian/Ubuntu означают, что IPv6-адрес не прошёл проверку уникальности в локал ...