Самое популярное заблуждение о systemd звучит примерно так: "Requires= означает, что сервис B должен запуститься перед сервисом A". Это неверно. Requires= не говорит ничего об очерёдности. Он говорит о том, что если B не запустился, A тоже не запустится. Но оба они при этом могут стартовать одновременно, и A вполне может оказаться в активном состоянии раньше B, если успеет быстрее. Смешение зависимостей требования и зависимостей порядка в одну сущность, пожалуй, главный источник проблем при написании unit-файлов. Systemd разделяет эти два понятия намеренно и жёстко, и понять эту границу важнее, чем выучить наизусть список директив.
Речь не только о теории. Сервис, у которого After= есть, а Wants= или Requires= нет, запустится в правильном порядке только если зависимость уже активна. Если её нет, systemd просто стартует ваш сервис без неё. И наоборот: Requires= без After= может привести к состоянию гонки, когда оба сервиса стартуют параллельно, ваш быстрее, а база данных, от которой он зависит, ещё не готова. Обе ошибки реальны, обе распространены и обе молча приводят к падению сервиса в неожиданный момент.
Требование и порядок как два независимых измерения в графе зависимостей
Systemd строит граф зависимостей в двух совершенно независимых плоскостях. Первая плоскость, требование: должен ли юнит B вообще запускаться, и что произойдёт с A, если B упадёт. Директивы этой плоскости: Wants=, Requires=, Requisite=, BindsTo=, PartOf=. Вторая плоскость, порядок: если оба юнита запускаются, кто из них стартует первым. Директивы этой плоскости: After= и Before=.
Директива After=b.service в файле a.service означает одно и только одно: если оба юнита стартуют в одной транзакции, B будет запущен до A. Если B не входит в транзакцию запуска, After= ни на что не влияет. Это не ошибка проектирования, это осознанная архитектура: независимость двух плоскостей даёт гибкость, но требует явности. Нельзя написать After= и рассчитывать, что B запустится автоматически.
Практически это означает, что для корректной зависимости почти всегда нужны обе директивы вместе:
[Unit]
Description=Мой веб-сервис
# Требование: запустить postgresql, если он не запущен
Requires=postgresql.service
# Порядок: стартовать только после того, как postgresql активен
After=postgresql.service
Только такая комбинация даёт реальную гарантию: postgresql будет запущен И будет активен к моменту старта нашего сервиса.
Шесть директив требования и когда каждая из них нужна
Wants=, мягкая зависимость. Если B не запустился или упал, A всё равно стартует. Это правильный выбор для опциональных компонентов: экспортёр метрик, агент логирования, дополнительный кэш. Если такой компонент недоступен, основной сервис должен работать, а не падать.
Requires=, жёсткая зависимость на старте. Если B не запустился, A не запустится тоже. Но если B остановится уже после того, как A активен, A продолжит работу. Это поведение удивляет больше всего: Requires= не означает постоянного мониторинга состояния зависимости. Он срабатывает только в момент транзакции запуска.
Requisite=, ещё более жёсткий вариант. Разница с Requires= тонкая, но принципиальная: если B не активен в момент запуска A, A немедленно падает с ошибкой, не пытаясь B запустить. Это полезно для юнитов, которые должны обнаружить проблему, а не скрыть её автоматическим запуском зависимости. Requisite= без After= почти бессмысленен: без порядка systemd может проверить состояние B в тот момент, когда он ещё активируется.
BindsTo=, самая сильная директива требования. Не только запускает B вместе с A, но и останавливает A при любой остановке B, в том числе неожиданной. Это именно то поведение, которое многие ожидают от Requires=, но не получают. BindsTo= в паре с After= означает: A активен тогда и только тогда, когда активен B. Классическое применение, привязка сервиса к сетевому интерфейсу или устройству:
[Unit]
Description=Сервис, жёстко привязанный к VPN-интерфейсу
BindsTo=sys-subsystem-net-devices-tun0.device
After=sys-subsystem-net-devices-tun0.device
Если интерфейс tun0 исчезнет, сервис немедленно остановится. Когда интерфейс появится снова, можно настроить автоматический перезапуск через Restart=on-failure.
PartOf=, однонаправленная зависимость. Когда B останавливается или перезапускается, A следует за ним. Но A может останавливаться и перезапускаться независимо. Это модель "часть целого": компоненты составного сервиса, которые должны следовать жизненному циклу родителя, но не наоборот.
Conflicts=, отрицательная зависимость. Запуск A останавливает B и наоборот. Классический пример из самого systemd: poweroff.target и reboot.target конфликтуют между собой.
# Пример unit-файла с полным набором зависимостей
[Unit]
Description=Production API сервер
Documentation=https://internal-docs.example.com/api
# Жёсткие зависимости: без них старт невозможен
Requires=postgresql.service
Requires=redis.service
After=postgresql.service redis.service
# Мягкие зависимости: падение не критично
Wants=prometheus-exporter.service
After=prometheus-exporter.service
# Сервис не должен работать одновременно со staging-версией
Conflicts=api-staging.service
# Следовать жизненному циклу родительского target
PartOf=app-stack.target
Как After= работает при остановке и почему порядок инвертируется
Здесь кроется важная деталь, которую легко пропустить. Директива After= влияет не только на порядок запуска, но и на порядок остановки, и влияет инверсно. Если a.service содержит After=b.service, то при одновременной остановке обоих: A будет остановлен раньше B. Порядок запуска: B первый, A второй. Порядок остановки: A первый, B второй. Это логично: корректное завершение зависимого сервиса до завершения того, от чего он зависит.
При неправильном проектировании это правило срабатывает неожиданно. Если написать After=network.target в сервисе, который выполняет финальные HTTP-запросы при остановке, systemd остановит сервис до того, как сеть уйдёт. Но если After= нет, а сеть уходит первой, финальные запросы провалятся. Explicit After= с правильным network target это не просто "чтобы стартовать после сети", это ещё и "чтобы завершиться до того, как сеть уйдёт".
network.target, network-online.target и та самая ловушка с сетью
Одна из самых частых ошибок в unit-файлах для сетевых сервисов, использование network.target там, где нужен network-online.target. Разница принципиальная. network.target означает, что networkd или NetworkManager запущены, то есть началась настройка сети. Это не означает, что интерфейсы подняты, адреса назначены и маршруты добавлены. Для большинства сервисов, которым нужна реальная связность, правильный target другой:
[Unit]
Description=Сервис который реально ждёт сети
Wants=network-online.target
After=network-online.target
network-online.target заблокирует запуск до тех пор, пока сетевой менеджер не сообщит о готовности всех managed-интерфейсов. Обратная сторона: это замедляет загрузку системы. Именно поэтому директива здесь Wants=, а не Requires=: если сеть не поднялась, сервис всё равно попробует стартовать, а не заблокирует всю систему.
# Проверить что именно обеспечивает network-online.target на конкретной системе
systemctl show network-online.target -p Wants -p After
# Посмотреть сколько времени занял network-online.target при последней загрузке
systemd-analyze blame | grep network
Диагностика и как увидеть реальный граф зависимостей
Зависимости в unit-файлах не всегда очевидны: они могут приходить из drop-in файлов, из .wants/ и .requires/ директорий, из зависимостей установленных пакетов. Видеть реальный граф для конкретного юнита:
# Полное дерево зависимостей сервиса (рекурсивно)
systemctl list-dependencies nginx.service
# Только то, что должно запуститься ПЕРЕД nginx (After= зависимости)
systemctl list-dependencies --after nginx.service
# Только то, что запустится ПОСЛЕ nginx (Before= зависимости и что от него зависит)
systemctl list-dependencies --before nginx.service
# Обратные зависимости: кто зависит от postgresql
systemctl list-dependencies --reverse postgresql.service
# Полный граф в формате dot для визуализации (нужен graphviz)
systemd-analyze dot nginx.service | dot -Tsvg > nginx-deps.svg
# Ограничить граф двумя уровнями глубины
systemd-analyze dot --to-pattern='nginx.service' \
--from-pattern='*.service' | dot -Tsvg > nginx-incoming.svg
Отдельно стоит проверять фактические свойства запущенного юнита, а не только файл конфигурации: drop-in файлы и зависимости, добавленные при установке пакетов, могут менять картину:
# Все свойства юнита включая унаследованные зависимости
systemctl show nginx.service | grep -E "^(Wants|Requires|After|Before|PartOf|BindsTo)"
# Откуда пришла конкретная зависимость (drop-in или основной файл)
systemctl cat nginx.service
# Проверить наличие .wants и .requires директорий
ls /etc/systemd/system/nginx.service.wants/ 2>/dev/null
ls /lib/systemd/system/multi-user.target.wants/ | grep nginx
Циклические зависимости и отладка транзакций
Systemd обнаруживает циклические зависимости при построении транзакции запуска и пытается их разрешить, удаляя наименее критичные рёбра. Если цикл не удаётся разрешить, оба юнита остаются в состоянии failed. Цикл не всегда очевиден: он может возникнуть через цепочку из трёх-четырёх юнитов, каждый из которых выглядит разумно по отдельности.
# Обнаружить проблемы при запуске конкретного юнита в изоляции
systemd-analyze verify myapp.service
# Полный анализ загрузки с выявлением узких мест
systemd-analyze critical-chain myapp.service
# Пример вывода:
# myapp.service +1.243s
# └─postgresql.service +3.891s
# └─network-online.target +12.543s
# └─NetworkManager-wait-online.service +12.102s
# Поиск ошибок зависимостей в журнале текущей загрузки
journalctl -b | grep -E "(dependency|ordering|cycle)" | head -20
systemd-analyze critical-chain показывает критический путь, то есть цепочку зависимостей, которая определяет общее время загрузки системы. Если network-online.target стоит в начале этой цепочки и занимает двенадцать секунд, добавление After=network-online.target к ещё одному сервису сделает загрузку ещё медленнее. Иногда правильный ответ, убрать эту зависимость и реализовать логику ожидания сети внутри самого сервиса через retry-механизм.
Пользовательские targets для группировки сервисов
Targets в systemd, это не только аналог runlevels. Это полноценный инструмент группировки сервисов с собственными зависимостями. Если нужно гарантировать, что три компонента стека всегда стартуют и останавливаются как единое целое:
# /etc/systemd/system/app-stack.target
[Unit]
Description=Полный стек приложения
Requires=api.service worker.service scheduler.service
After=api.service worker.service scheduler.service
[Install]
WantedBy=multi-user.target
# /etc/systemd/system/api.service
[Unit]
Description=API сервер
PartOf=app-stack.target
After=postgresql.service
Requires=postgresql.service
[Service]
ExecStart=/usr/bin/api-server
Restart=on-failure
[Install]
WantedBy=app-stack.target
# Запустить весь стек одной командой
systemctl start app-stack.target
# Остановить весь стек
systemctl stop app-stack.target
# Проверить состояние стека
systemctl status app-stack.target
PartOf=app-stack.target в каждом компоненте означает: остановка target остановит компонент, но компонент может останавливаться и перезапускаться независимо. ConsistsOf= в target генерируется автоматически как обратная ссылка и не настраивается вручную.
Граф зависимостей systemd устроен так, что его легко испортить, добавив Requires= вместо Wants= или After= без пары. Но именно его явность и инспектируемость через systemctl list-dependencies и systemd-analyze dot делают его значительно лучше того, чем были пронумерованные init-скрипты: зависимости видны, граф можно нарисовать, а критический путь загрузки измерить до миллисекунды.