Половина инцидентов «сервис не поднимается после перезагрузки» — это не баги, а кривые зависимости в systemd-юнитах. Я насмотрелся столько ошибок типа Requires=network.target без After=network.target, что решил написать памятку для себя и для тех, кто только разбирается.
Главное, что надо понять
Зависимости делятся на две категории, и их часто путают:
- Порядок запуска —
After=иBefore=. Говорит systemd, в каком порядке стартовать юниты. - Жёсткость связи —
Requires=,Wants=,BindsTo=,PartOf=,Requisite=. Говорит, насколько критична зависимость и что делать, если зависимый юнит упал.
Эти два аспекта независимы. Если вы пишете Requires=foo.service — это значит «foo должен быть запущен», но НЕ значит «foo стартанёт раньше нас». Если хотите гарантировать порядок — добавляйте After=foo.service отдельно.
Простой пример, как ломается
Есть сервис, который пишет в логи через сеть. Юнит:
[Unit]
Description=Network logger
Requires=network.target
[Service]
ExecStart=/usr/local/bin/netlogger
Restart=on-failure
[Install]
WantedBy=multi-user.target
После ребута сервис в 50% случаев падает, потому что стартует одновременно с network.target, и сеть ещё не готова. Чинится так:
[Unit]
Description=Network logger
Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/usr/local/bin/netlogger
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Тут важно: network.target означает «сетевой стек поднят», а не «у нас есть IP и работает DNS». Если нужна реальная сетевая связность — используйте network-online.target и Wants= (потому что Requires= на network-online.target может затянуть весь boot).
Что значит каждая директива
After / Before
Это порядок, и только порядок. Если в After=foo.service, мы стартуем после foo — но если foo не существует или не запускается, мы всё равно стартанём. Это не зависимость, это очерёдность.
Requires
Жёсткая зависимость. Если зависимый юнит остановили — нас тоже остановят. Если зависимый юнит упал при старте — мы не стартуем. Без After= порядок не гарантирован.
[Unit]
Requires=postgresql.service
After=postgresql.service
Это «postgres обязан быть жив, иначе нас тоже выключают».
Wants
Мягкая версия Requires. Если зависимый юнит не запустился — мы всё равно стартуем. Используется чаще всего, потому что более устойчиво к каскадным сбоям.
BindsTo
Похоже на Requires, но строже: если зависимый юнит перестал быть активен по любой причине (включая нормальную остановку), нас тоже остановят. Полезно для tightly coupled пар, например, контейнер и его сетевой namespace.
PartOf
Однонаправленная привязка к перезапуску: если зависимый юнит рестартовали или остановили — нас тоже. Но если упали мы — на него это не влияет. Полезно для группировки.
Requisite
Жёстко требует, чтобы зависимый юнит был уже запущен на момент старта. Не пытается стартовать его — просто фейлится, если не активен. Редко используется.
Реальный пример с docker и WireGuard
Допустим, есть docker-контейнер с прокси, который должен слать трафик через wg-туннель. Если docker.service стартует раньше, чем wg-quick@wg0.service, то контейнер поднимется без маршрутов через туннель, и трафик пойдёт мимо.
# /etc/systemd/system/docker.service.d/override.conf
[Unit]
Wants=wg-quick@wg0.service
After=wg-quick@wg0.service network-online.target
Override через systemctl edit docker.service — лучше, чем править основной юнит. Можно делать systemctl daemon-reload без перетаскивания docker’а целиком.
Пример с conditional-сервисом
Хотим, чтобы наш сервис запускался только если есть определённый файл (например, конфиг):
[Unit]
Description=My app
ConditionPathExists=/etc/myapp/config.yml
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yml
Restart=on-failure
ConditionPathExists не вызовет ошибку, если файла нет — он просто скажет «нечего запускать, юнит skipped».
Отладка зависимостей
Самые полезные команды:
systemctl list-dependencies myservice.service
systemctl list-dependencies myservice.service --reverse
systemctl show myservice.service | grep -E '^(After|Before|Wants|Requires|BindsTo)='
systemd-analyze critical-chain myservice.service
systemd-analyze plot > boot.svg
critical-chain показывает цепочку зависимостей с таймингами — где именно бутстрап тормозит. plot рисует SVG с диаграммой загрузки — иногда красивее, чем смотреть в текст.
Самые частые ошибки
Ошибка 1: Requires= без After=.
Юнит зависит от postgres, но стартует одновременно с ним. В половине случаев фейлится.
Ошибка 2: After=network.target вместо After=network-online.target.
Сетевой стек поднят, но интерфейс ещё не получил IP. Сервис стартует, не может забиндиться, падает.
Ошибка 3: Restart=always без RestartSec=.
Сервис падает, рестартует через 100 мс, опять падает, опять рестарт. systemd через 5 рестартов забивает и помечает юнит как failed. Лечится RestartSec=5 и StartLimitBurst=10.
Ошибка 4: BindsTo=foo.target на пользовательский target.
Если этот target не существует, юнит не стартует с криптическим сообщением. Проверяйте, что target определён.
Итог
Запомните три вещи: After — это про порядок, Requires/Wants — это про обязательность, и эти две вещи всегда нужно указывать вместе. Если что-то не стартует после ребута — первым делом смотрите systemd-analyze critical-chain и проверяйте, что зависите от network-online.target, а не network.target.