Зависимости и упорядочение
Wants/Requires объявляют что должно существовать; After/Before — что должно быть первым. Они ортогональны — можно иметь упорядочение без зависимости и зависимость без упорядочения. Большинство багов возникает от путаницы между ними.
Твой API-сервис стартует при загрузке, подключается к Postgres и сразу падает — потому что Postgres ещё инициализируется. Добавляешь After=postgresql.service — всё равно падает, потому что After= не заставляет systemd ждать, пока Postgres готов, а только пока его юнит активирован. Добавляешь Requires=postgresql.service — теперь когда Postgres падает в 2 ночи, твой API молча убивается вместе с ним, что тоже не то, чего ты хотел. Система зависимостей — один абзац в man-странице и неделя production-инцидентов, если ошибиться.
После этого урока ты сможешь объяснить разницу между директивами зависимостей (Wants, Requires) и директивами упорядочения (After, Before), описать что происходит при сбое required-юнита, понять как WantedBy подключает сервис к target при включении, и диагностировать типичное состояние гонки от использования After= без директивы зависимости.
Директивы зависимостей (Wants, Requires) отвечают на вопрос “должен ли этот юнит быть активным?” Директивы упорядочения (After, Before) — “какой юнит стартует первым?” Они полностью ортогональны.
Можно иметь упорядочение без зависимости: After=network.target означает “запустить после достижения сетевого target”, но если network.target завершится с ошибкой, твой сервис всё равно попытается стартовать. Можно иметь зависимость без упорядочения: Requires=postgresql.service означает “если postgres неактивен — завершиться с ошибкой”, но без After=postgresql.service systemd может попытаться запустить оба одновременно (параллельная активация).
На практике почти всегда нужны оба:
[Unit]
# Зависимость: если postgres не запущен, этот сервис не может работать
Requires=postgresql.service
# Упорядочение: запустить этот сервис ПОСЛЕ активации postgres
After=postgresql.serviceПропуск After= при наличии Requires= — это состояние гонки, которое бьёт чаще всего: оба сервиса стартуют параллельно, твой сервис подключается до того как postgres начал слушать, и падает — хотя зависимость технически объявлена.
Wants= vs Requires= — это про устойчивость к сбоям. Это важнейшее различие в системе зависимостей:
| Директива | Если зависимость падает при старте | Если зависимость останавливается позже |
|---|---|---|
Wants= | Этот юнит всё равно стартует | Этот юнит продолжает работать |
Requires= | Этот юнит тоже падает | Этот юнит останавливается |
BindsTo= | Этот юнит тоже падает | Этот юнит останавливается немедленно |
Wants= подходит для необязательных зависимостей — экспортёр метрик, который работает лучше с Redis, но деградирует корректно без него. Requires= — для жёстких зависимостей — сервис, который вообще не может работать без базы данных. BindsTo= — для тесно связанных пар, например сервис и сопутствующий юнит сетевого пространства имён.
[Unit]
# Мягкая зависимость: попробовать запустить redis, но продолжить даже при сбое
Wants=redis.service
After=redis.service
# Жёсткая зависимость: если postgres падает — остановить этот сервис тоже
Requires=postgresql.service
After=postgresql.serviceAfter= и Before= управляют упорядочением в параллельной активации. По умолчанию systemd запускает юниты параллельно — граф зависимостей разворачивается, и всё, чьи зависимости удовлетворены, стартует одновременно. After= вносит ограничение последовательности:
[Unit]
# Не стартовать пока не достигнут network-online.target
After=network-online.target
# Гарантировать что этот сервис останавливается до отключения сети (при завершении)
Before=network.targetnetwork.target vs network-online.target — распространённая ловушка: network.target достигается в момент старта сетевого менеджера — почти мгновенно. network-online.target достигается когда сеть маршрутизируема — DNS резолвится, есть маршрут по умолчанию. Сервис, делающий исходящие соединения, нуждается в After=network-online.target, а не в After=network.target.
# Посмотреть от чего реально зависит network-online.target
systemctl list-dependencies network-online.targetWantedBy= в [Install] — это то, на что воздействует systemctl enable. Он не создаёт зависимость от твоего сервиса к target — он создаёт её в обратном направлении: когда запускается systemctl enable myapp.service, создаётся симлинк в /etc/systemd/system/multi-user.target.wants/myapp.service. Когда systemd активирует multi-user.target при загрузке, он читает эту директорию wants/ и запускает всё в ней.
[Install]
WantedBy=multi-user.targetВот почему секция [Install] не имеет эффекта до выполнения systemctl enable. Секция описывает как установить юнит, а не живую зависимость. multi-user.target — правильный выбор для любого сервиса, который должен работать в обычной (неграфической, сетевой) системе. Для сервисов, которые имеют смысл только с GUI, используй graphical.target.
# Посмотреть что multi-user.target будет запускать (после включения твоего сервиса)
ls /etc/systemd/system/multi-user.target.wants/Как target подтягивает свои зависимости — и почему это важно для твоих собственных target’ов. Target — просто именованная точка синхронизации. Когда systemd активирует multi-user.target, он:
- Читает директивы
Wants=иRequires=(статические, в unit-файле) - Читает drop-in директории
wants/иrequires/(динамические, заполняемыеsystemctl enable) - Запускает все найденные юниты, соблюдая их ограничения
After=
# Посмотреть полное дерево зависимостей для multi-user.target
systemctl list-dependencies multi-user.target
# Только юниты, напрямую wanted этим target'ом
systemctl list-dependencies --plain multi-user.target | head -20Понимание этой цепочки — target → директория wants → твой сервис — делает systemctl enable менее магическим. Enable просто добавляет симлинк в директорию, которую systemd уже читает.
Отладка сервиса, который гонится с базой данных при загрузке.
Симптом: myapi.service падает при загрузке с “connection refused” к PostgreSQL, но нормально запускается если выполнить systemctl restart myapi через тридцать секунд.
# Проверить объявления зависимостей юнита
systemctl cat myapi.service
# [Unit]
# After=postgresql.service ← только упорядочение, нет зависимости
# (нет строки Requires=)
# Посмотреть как выглядит postgresql.service во время сбоя
journalctl -b -u postgresql.service --since "boot" | head -20
# postgresql.service: запущен (Type=notify), но ещё не принимает соединенияГонка: After=postgresql.service означает что systemd ждёт перехода юнита postgresql в состояние active. Для Type=notify active достигается когда процесс отправляет READY=1 — что postgres делает после fork, но до того как начинает принимать TCP-соединения. API пытается подключиться в этот промежуток.
Исправление:
[Unit]
Description=My API
Requires=postgresql.service
After=postgresql.serviceЗатем добавить цикл повтора подключения в само приложение — сервис должен быть устойчив к кратковременной недоступности зависимостей даже при корректном упорядочении юнитов, потому что упорядочение гарантирует последовательность, но не готовность. Альтернативно, использовать ExecStartPre для опроса:
[Service]
ExecStartPre=/bin/sh -c 'until pg_isready -h localhost; do sleep 1; done'
ExecStart=/opt/myapi/server▸Частая ошибка
Распространённая ошибка: писать Requires=network.target в ожидании что сеть будет полностью поднята. network.target достигается в момент старта сетевого менеджера — почти мгновенно, задолго до настройки интерфейсов. Используй After=network-online.target (и Wants=network-online.target) для сервисов, которым нужна реальная маршрутизируемая сеть. На Ubuntu именно systemd-networkd-wait-online.service или NetworkManager-wait-online.service реально заполняет network-online.target.
▸Почему это работает
В SysV init не было эквивалента этой системы — нумерация скриптов (S20postgresql, S25myapp) была единственным механизмом упорядочения, и не было концепции “если postgres падает — остановить myapp”. Упорядочение было глобальным и последовательным; параллелизма не было и распространения сбоев не было. Граф зависимостей systemd обеспечивает и более быструю загрузку (параллелизм), и более безопасную обработку сбоев (распространяемые остановки) — но требует явного объявления намерений.
В твоём unit-файле есть `After=postgresql.service`, но нет `Requires=` или `Wants=`. PostgreSQL не запускается при загрузке. Что произойдёт с твоим сервисом?
Директивы зависимостей (Wants=, Requires=) объявляют должен ли юнит быть активным. Директивы упорядочения (After=, Before=) объявляют что стартует первым. Они ортогональны — использование одного без другого вызывает два классических бага: состояние гонки (упорядочение без зависимости) или каскадная остановка (жёсткая зависимость без понимания что Requires= распространяет сбой). Wants= — для необязательных зависимостей; Requires= — для жёстких. After=network-online.target (не network.target) — то что нужно сервисам с исходящими соединениями. WantedBy=multi-user.target в [Install] — то на что воздействует systemctl enable: он добавляет симлинк, который target читает при загрузке.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.