SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-21

Настройка зависимостей и условий в systemd

Разберитесь в различиях между Requires, Wants, After и Before. Узнайте, почему юниты падают при загрузке из-за параллельного запуска и как правильно настроить Condition.

Requires не означает After

Зависимости и условия в systemd — это четыре отдельных механизма, которые в большинстве unit-файлов используются так, будто они являются одним целым. Requires= и Wants= определяют, какие другие юниты будут запущены. After= и Before= определяют порядок запуска юнитов. ExecStartPre= выполняет проверку, которая может привести к сбою юнита. Семейства Condition и Assert определяют, будет ли юнит запущен в принципе. Каждый механизм независим от остальных, поэтому юнит может требовать другой юнит и при этом запускаться с ним одновременно.

Последнее утверждение — это причина почти всех отчетов вида «все работает при ручном запуске, но падает при загрузке».

[Unit]
Description=Inventory API
Requires=postgresql.service

[Service]
ExecStartPre=/usr/bin/pg_isready -h 127.0.0.1 -t 5
ExecStart=/usr/local/bin/inventory-api

Requires=postgresql.service включает PostgreSQL в ту же транзакцию запуска. Это не означает ожидание завершения его работы. systemd запускает обе задачи параллельно, поэтому pg_isready выполняется в то время, когда PostgreSQL еще открывает свой каталог данных. Процесс завершается с кодом 2, так как на порту еще никто не слушает, и юнит переходит в состояние сбоя до того, как будет достигнут ExecStart. Запуск sudo systemctl start inventory-api через час работает, так как к этому моменту PostgreSQL уже запущен. В самом unit-файле ничего не менялось, поэтому он выглядит корректным.

Исправление состоит из одной строки.

[Unit]
Requires=postgresql.service
After=postgresql.service

Более тонкий нюанс скрывается в том же месте. Неудачная зависимость Requires= останавливает запуск вашего юнита только в том случае, если вы также установили для него After=. Без указания порядка запуска systemd уже запустит ваш юнит к тому моменту, когда другой юнит завершится с ошибкой, поэтому отменять будет уже нечего. Requires= сам по себе не дает той защиты, на которую рассчитывают пользователи. Добавляйте After= рядом с каждым Requires= и каждым Wants=, если у вас нет особых причин этого не делать.

Что обеспечивают параметры Requires, Wants, Requisite и BindsTo

Все эти параметры определяют зависимости. Ни один из них не задает порядок запуска.

  • Wants=: добавляет другой юнит в очередь на запуск. Если он завершится с ошибкой или не существует, текущий юнит все равно запустится. Именно это создает systemctl enable в виде символической ссылки внутри каталога .wants/.
  • Requires=: добавляет другой юнит в очередь на запуск. Если он завершится с ошибкой и вы также указали After=, текущий юнит не будет запущен. Если другой юнит будет принудительно остановлен позже, текущий юнит остановится вместе с ним.
  • Requisite=: не добавляет другой юнит в очередь на запуск. Если он еще не активен, текущий юнит немедленно завершится с ошибкой.
  • BindsTo=: аналогично Requires=, но текущий юнит также останавливается при любой остановке другого юнита, включая отключение оборудования.
  • PartOf=: команды остановки и перезапуска передаются от другого юнита к текущему. Команда запуска не передается.
  • Conflicts=: запуск текущего юнита останавливает другой.

Для взаимодействия двух демонов обычно правильной парой является Wants= и After=. Requires= связывает жизненные циклы: при остановке базы данных для обслуживания ваше приложение также завершит работу и не запустится автоматически после возвращения базы в строй. Wants= и After= обеспечивают порядок загрузки без жесткой привязки, а политика перезапуска (restart policy) позволяет обработать ситуацию, если зависимость исчезнет позже.

Вы также наследуете зависимости, которые не прописывали явно. При DefaultDependencies=yes, который является значением по умолчанию, обычная служба автоматически получает Requires=sysinit.target, After=sysinit.target basic.target и Conflicts=shutdown.target. Именно поэтому служба с почти пустым разделом [Unit] все равно запускается на поздних этапах загрузки и корректно останавливается при выключении системы.

Директивы After и Before определяют только порядок запуска, но не зависимости

After= и Before= отвечают исключительно за порядок выполнения. Они не накладывают никаких требований на запуск. Если After=redis.service указано в юните, который не требует Redis, то это действие не имеет эффекта: если redis.service не является частью транзакции запуска, то ждать нечего, и ваш юнит запустится немедленно.

Это стоит повторить дважды, так как именно в этом заключается суть ошибки network-online.target, описанной далее. Упорядочивание ожидает только те юниты, которые уже запускаются в рамках той же транзакции.

Эта пара директив симметрична. After=b.service, записанное в a.service, означает то же самое, что и Before=a.service, записанное в b.service. Используйте одну из них и размещайте её в юните, которым вы управляете. При выключении порядок упорядочивания автоматически меняется на обратный, поэтому After=b.service также означает, что ваш юнит будет остановлен до b.service.

Параметр After= ожидает «запуска», а Type= определяет, что это означает

After= ожидает завершения запуска другого юнита. То, что именно считается «завершением запуска», определяется исключительно параметром Type= этого юнита.

  • Type=simple: как только systemd выполнил fork процесса. Программа может еще не успеть прочитать конфигурацию, не говоря уже об открытии сокета.
  • Type=exec: как только успешно выполнился execve(). Немного более надежный вариант. Однако он всё ещё не гарантирует готовность к работе.
  • Type=forking: когда завершается исходный родительский процесс.
  • Type=oneshot: когда завершается сам процесс. В данном случае «запуск» действительно означает, что работа выполнена.
  • Type=notify: когда сервис отправляет READY=1 через свой сокет уведомлений. Это единственный тип, который сообщает о реальной готовности.

Таким образом, After= для демона типа Type=simple — это слабое обещание, и именно в этом заключается вторая часть проблемы состояния гонки в первом примере. Если юнит, от которого вы зависите, поставляется с Type=simple, то указание порядка запуска после него не означает, что он уже принимает соединения. Есть два честных решения. Укажите порядок запуска после его сокет-юнита: тогда ядро будет ставить входящие соединения в очередь, пока демон ещё запускается. Либо настройте свой сервис на повторные попытки подключения и позвольте политике перезапуска (restart policy) справиться с этим. Тип, используемый юнитом, можно увидеть в systemctl cat, и параметр Type= и значение каждого варианта для systemd стоит изучить, прежде чем полагаться на порядок запуска.

ExecStartPre — это шлюз, который может привести к сбою юнита

ExecStartPre= выполняется перед ExecStart=. Если команда завершается с ненулевым кодом, активация прерывается, а юнит переходит в состояние failed. ExecStart= при этом никогда не запускается. Именно по этой причине многие юниты завершаются с ошибкой, не выдавая сообщений от самой программы: программа просто не была запущена.

Факты, на которых часто ошибаются:

  • Это не оболочка (shell). Здесь не работают конвейеры, перенаправления, подстановка путей (globs) и &&. Первый токен должен быть абсолютным путем. Оберните строку в /bin/sh -c '...', если вам нужен синтаксис shell.
  • Префикс - делает ненулевой код завершения некритичным: ExecStartPre=-/usr/bin/optional-check.
  • Каждый ExecStartPre= должен завершиться до запуска следующего. Он не может запускать долгоживущий процесс.
  • Все строки ExecStartPre= используют общий TimeoutStartSec= с ExecStart=. Предварительная проверка, которая ожидает базу данных в цикле, расходует время ожидания запуска (timeout), после чего юнит завершается с ошибкой Result: timeout, а в журнале появляется start operation timed out. Terminating..

Строка с ошибкой указывает на управляющий процесс, а не на основной:

inventory-api.service: Control process exited, code=exited, status=2/INVALIDARGUMENT
inventory-api.service: Failed with result 'exit-code'.

Внимательно читайте это символьное имя. systemd сопоставляет небольшие коды завершения с фиксированной таблицей, поэтому 2 всегда выводится как INVALIDARGUMENT, независимо от того, что именно имела в виду программа. status=203/EXEC — это код, который несет реальную информацию: systemd вообще не смог выполнить бинарный файл, так как путь указан неверно или файл не является исполняемым.

Не используйте ExecStartPre= для создания директорий. RuntimeDirectory=, StateDirectory=, LogsDirectory= и CacheDirectory= создают их с правильным владельцем и правами доступа, а RuntimeDirectory= очищается при остановке сервиса. Они также корректно работают с DynamicUser=, чего нельзя сказать о написанном вручную mkdir.

Условие завершается тихо. Assert завершается с ошибкой.

Семейства Condition и Assert выполняют одни и те же проверки. Разница заключается лишь в том, что происходит при неудачном прохождении теста.

Неудачное условие Condition...= пропускает юнит. Задача запуска помечается как успешная. Юнит остается в состоянии inactive (dead), ничего не помечается как сбой, оповещения не отправляются, а в журнале появляется одна строка:

Condition check resulted in Inventory API being skipped.

В systemd версии 250 и новее systemctl status выводит причину напрямую:

     Active: inactive (dead)
  Condition: start condition unmet at Thu 2026-08-20 09:14:02 UTC; 2min ago

Строка с отступом под ней указывает на конкретную директиву, которая вызвала сбой, например ConditionPathExists=/etc/inventory/api.conf was not met.

Неудачное условие Assert...= приводит к сбою юнита. В журнале появляется сообщение Assertion failed for Inventory API., а юнит переходит в состояние failed (Result: assert), что достаточно заметно для систем мониторинга.

Выбирайте между ними, ответив на вопрос, что означает неудачный тест. Condition означает «этот юнит не применим на данной машине». Assert означает «это условие должно быть истинным, и если это не так, сообщите об этом». Для большинства юнитов требуется Condition. Используйте Assert только в том случае, если бездействие хуже, чем сбой юнита.

При использовании семейства Condition есть две ловушки.

Во-первых, неудачное условие не приводит к сбою юнитов, которые от него зависят. Если a.service имеет Requires=b.service, а b.service пропускается из-за условия, задача запуска для b.service всё равно считается выполненной, поэтому a.service запускается в обычном режиме, когда b не работает. Условие защищает только тот юнит, в котором оно прописано.

Во-вторых, условия проверяются каждый раз при запуске юнита, в момент выполнения задачи. Юнит, запускаемый systemd-таймером на VPS, может пропускаться сотни раз подряд и ни разу не выглядеть как сбойный. Это тот же класс «тихих» операций, что и cron-задача, которая выполняется, но ничего не делает, и искать проблему нужно так же: читайте журнал юнита, вместо того чтобы доверять его коду завершения.

Полезные условия для сервера:

  • ConditionPathExists=/etc/inventory/api.conf и его отрицание ConditionPathExists=!/etc/inventory/api.conf.
  • ConditionFileNotEmpty= и ConditionDirectoryNotEmpty= — для конфигурационного файла или каталога данных, который был создан пакетом, но остался пустым.
  • ConditionVirtualization= — чтобы юнит, требующий реального интерфейса ядра, мог содержать ConditionVirtualization=!container. Проверьте, что сообщает ваша система с помощью systemd-detect-virt.
  • ConditionHost= соответствует имени хоста или machine ID; так один и тот же файл юнита может вести себя по-разному на двух серверах.
  • ConditionKernelCommandLine= и ConditionKernelVersion= — для юнитов, привязанных к параметру загрузки или минимальной версии ядра.

Пустое присваивание очищает список; так с помощью drop-in файла можно удалить условие, заданное в пакете:

[Unit]
ConditionPathExists=
ConditionPathExists=/srv/inventory/api.conf

Почему network.target не означает, что сеть готова

network.target — это точка синхронизации, а не состояние. При загрузке указание зависимости после этой цели означает лишь то, что программное обеспечение для управления сетью было запущено. Это не гарантирует, что интерфейс получил IP-адрес или что существует маршрут до интернета. Эта цель в основном существует для обратного процесса: юниты, для которых указано After=network.target, останавливаются до того, как сеть будет отключена при выключении системы.

network-online.target — это цель, которая действительно ожидает готовности. Она обеспечивается службой ожидания сети (wait-online), соответствующей вашему сетевому менеджеру:

  • systemd-networkd-wait-online.service, если systemd-networkd управляет линками (стандарт для серверов Ubuntu, настроенных через netplan).
  • NetworkManager-wait-online.service при использовании NetworkManager.

В старых конфигурациях ifupdown тот же эффект достигается с помощью networking.service. Какой бы вариант у вас ни был, правильное использование цели требует двух строк, а не одной.

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

network-online.target не является частью стандартной транзакции загрузки, и система не активирует её автоматически. Если вы укажете только After=, вы создадите зависимость от юнита, который не был добавлен в очередь, поэтому порядок запуска не изменится. Это та самая бесполезная операция, описанная ранее, в её наиболее затратном виде. Строка Wants= добавляет цель в транзакцию, чтобы строке After= было чего ждать.

Второе, что нужно знать: понятие «online» определяется реализацией службы ожидания, а не systemd. systemd-networkd-wait-online завершает работу, как только управляемые ею линки достигают настроенного состояния. Она не проверяет работоспособность DNS и доступность удаленных хостов.

Такое определение часто приводит к сбоям на VPS. Если в netplan объявлен второй интерфейс для частной сети, но он не получает адрес, служба ожидания будет ждать его до истечения тайм-аута:

systemd-networkd-wait-online[612]: Timeout occurred while waiting for network connectivity.
systemd-networkd-wait-online.service: Failed with result 'exit-code'.

Загрузка затягивается на две лишние минуты, так как стандартный таймаут составляет 120 секунд. Есть два способа исправить это. Укажите optional: true для неиспользуемого интерфейса в файле netplan, чтобы networkd перестал его ждать. Либо создайте drop-in файл для службы ожидания, указав нужный линк через --interface=, или передайте --any, чтобы служба завершалась, как только поднимется хотя бы один интерфейс.

Еще лучше — избегать необходимости в этой цели. Многие службы указывают зависимость от network-online.target только потому, что они привязываются к конкретному адресу и при загрузке выдают ошибку:

nginx: [emerg] bind() to 203.0.113.10:443 failed (99: Cannot assign requested address)

Ядро отклоняет привязку, так как адрес еще не назначен. Параметр net.ipv4.ip_nonlocal_bind=1 позволяет процессу привязаться к адресу, которого еще нет на устройстве, а политика перезапуска (restart policy) обработает последующий запуск. Задержка всей загрузки системы в ожидании сети — слишком радикальный инструмент для проблемы, которая обычно сводится к одному сокету.

Как прочитать реальные зависимости systemd на работающем сервере

Никогда не делайте выводы только на основе файла юнита. Файлы drop-in, символические ссылки .wants/ и неявные зависимости по умолчанию добавляют связи, которые не видны в самом файле.

systemctl cat inventory-api.service

Эта команда выводит содержимое файла юнита и всех drop-in файлов в порядке их применения, указывая путь к источнику над каждым блоком. Запускайте её в первую очередь. Переопределение из пяти строк в /etc/systemd/system/inventory-api.service.d/ имеет приоритет над упакованным файлом и иначе остаётся незаметным.

systemctl show inventory-api.service -p Requires -p Wants -p After -p Before -p ConditionResult -p AssertResult

Эта команда выводит разрешённые значения после применения drop-in файлов и добавления systemd неявных зависимостей. ConditionResult=no — это прямой ответ на вопрос «юнит отчитался об успехе, но ничего не сделал».

systemctl list-dependencies inventory-api.service
systemctl list-dependencies --reverse inventory-api.service
systemctl list-dependencies --after inventory-api.service
systemctl list-dependencies --before inventory-api.service

Обычная форма команды обходит Requires= и Wants= сверху вниз. --reverse показывает, какие юниты вызывают ваш, что позволяет найти цель, запускающую его при загрузке. --after и --before показывают порядок запуска; это пара команд, которую нужно изучить, если стоит вопрос, дождался ли кто-то завершения работы другого юнита.

journalctl -b -u inventory-api.service --no-pager
journalctl -b -o short-precise -u inventory-api.service -u postgresql.service

Вторая команда объединяет вывод для двух юнитов с миллисекундными метками времени. Так можно доказать наличие состояния гонки (race condition) при запуске, а не гадать о нём. Ошибка pg_isready возникает раньше, чем PostgreSQL записывает database system is ready to accept connections в лог, и временной разрыв между ними будет виден в выводе.

systemd-analyze verify /etc/systemd/system/inventory-api.service
systemd-analyze critical-chain inventory-api.service

verify загружает юнит так, как это сделал бы systemd, и сообщает о неизвестных директивах, зависимостях от несуществующих юнитов, циклах в порядке запуска и синтаксисе, который невозможно разобрать. Команда не вносит изменений в систему. critical-chain выводит цепочку порядка запуска, которая задержала юнит, с указанием времени активации каждого шага; работает только для юнитов, запущенных в текущей сессии загрузки.

После редактирования любого файла юнита выполните sudo systemctl daemon-reload. Чтобы изменить упакованный юнит, используйте sudo systemctl edit inventory-api.service — эта команда создаст для вас drop-in файл. Редактирование файла поставщика в /usr/lib/systemd/system/ работает только до следующего обновления пакета, которое перезапишет изменения. Тот же механизм drop-in используется для применения ограничений памяти и CPU к сервису без изменения файлов, принадлежащих пакету.

Циклы упорядочивания и их следы в журнале

Добавление зависимостей упорядочивания в обоих направлениях приводит к тому, что systemd разрывает цикл, удаляя одно из заданий:

systemd[1]: Found ordering cycle on inventory-api.service/start
systemd[1]: Job postgresql.service/start deleted to break ordering cycle starting with inventory-api.service/start

systemd выбирает, какое задание удалить, и этот выбор может не совпадать с вашим. В результате сервис может отсутствовать после одной перезагрузки и присутствовать после другой, что крайне затрудняет отладку извне. Большинство циклов возникает из-за юнитов, которые устанавливают DefaultDependencies=no, а затем всё равно выстраиваются в очередь относительно basic.target, либо из-за добавления Before= в юнит, у которого уже была ссылка After=, указывающая обратно на вас. systemd-analyze verify позволяет обнаружить их без перезагрузки.

Исправленный юнит

[Unit]
Description=Inventory API
Wants=postgresql.service network-online.target
After=postgresql.service network-online.target
ConditionPathExists=/etc/inventory/api.conf

[Service]
Type=notify
StateDirectory=inventory
ExecStart=/usr/local/bin/inventory-api
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

Каждая строка выполняет одну задачу. Wants= добавляет обе зависимости в транзакцию, не привязывая время жизни этого юнита к ним. After= обеспечивает ожидание, и в этой директиве необходимо повторить оба имени, так как зависимости и порядок запуска — это разные настройки. ConditionPathExists= означает, что если на машине есть пакет, но нет конфигурации, юнит будет пропущен без вывода предупреждений, что является правильным поведением для сервиса, управляемого конфигурацией. Type=notify означает, что любой юнит, запуск которого следует за этим, будет ждать реальной готовности, а не просто завершения процесса-родителя (fork). Restart=on-failure учитывает ситуацию, когда база данных отключается спустя долгое время после загрузки, так как порядок запуска применяется только к первому старту. Насколько агрессивным должен быть перезапуск, определяют параметры Restart= и RestartSec=.

Проверьте юнит перед тем, как доверить ему работу:

sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/inventory-api.service
systemctl list-dependencies --after inventory-api.service
sudo systemctl start inventory-api.service
systemctl show inventory-api.service -p ConditionResult -p ActiveState -p Result

Исправный юнит считывает ConditionResult=yes с помощью ActiveState=active, а Result=success подтверждает, что при последнем запуске не было ошибок. ConditionResult=no вместе с ActiveState=inactive означают, что юнит был пропущен, а строка в журнале с указанием условия подскажет, какая именно проверка не была пройдена.

FAQ

Ожидает ли Requires= запуска другого юнита?

Нет. Requires= и After= — это разные настройки. Requires= включает другой юнит в ту же транзакцию, после чего systemd запускает оба задания параллельно. Чтобы дождаться завершения, добавьте After= с указанием того же юнита. Есть и вторая причина для этого: зависимость Requires=, которая завершилась с ошибкой, предотвратит запуск вашего юнита только при наличии After=, так как без указания порядка ваш юнит будет запущен раньше, чем другой юнит успеет завершиться с ошибкой.

Что использовать для порядка запуска: network.target или network-online.target?

При загрузке network.target означает лишь то, что ПО для управления сетью было запущено, поэтому оно не гарантирует наличие IP-адресов или маршрутов. Используйте network-online.target, если вашему сервису при запуске необходима работающая сеть, и обязательно укажите оба параметра: Wants=network-online.target и After=network-online.target. Это нужно, так как данный target не входит в транзакцию загрузки по умолчанию, а один лишь After= будет ожидать юнит, который никто не поставил в очередь. Если сервис завершается с ошибкой только из-за привязки к конкретному IP, используйте net.ipv4.ip_nonlocal_bind=1 с Restart=on-failure — это менее ресурсоемко, чем задержка загрузки системы.

Почему юнит сообщает об успешном завершении, но не запускается?

Неудачный тест Condition...= приводит к пропуску юнита, при этом задание считается выполненным успешно, поэтому состояние ошибки не устанавливается. Выполните systemctl show <unit> -p ConditionResult, а ConditionResult=no подтвердит это. Затем изучите journalctl -b -u <unit> на наличие строки Condition check resulted in <description> being skipped. В systemd версии 250 и новее systemctl status <unit> также указывает на конкретную директиву, условие которой не было выполнено.

В чем разница между Condition и Assert?

Они выполняют идентичные проверки. Неудачный тест Condition тихо пропускает юнит, и задание считается успешным. Неудачный тест Assert переводит юнит в состояние ошибки, записывает в лог Assertion failed for <description>. и оставляет его в статусе failed (Result: assert). Используйте Condition для случаев «этот юнит не применим на данной машине», что покрывает почти все реальные ситуации. Используйте Assert только тогда, когда отсутствие предварительного условия должно быть заметно для систем мониторинга сбойных юнитов.

Почему ExecStartPre завершается с ошибкой status=203/EXEC?

203/EXEC означает, что systemd вообще не смог выполнить команду. Типичные причины: путь не является абсолютным, исполняемый файл отсутствует на машине, у файла нет прав на выполнение или в первой строке скрипта #! указан несуществующий интерпретатор. Другие короткие коды systemd берутся из фиксированной таблицы, поэтому status=2/INVALIDARGUMENT просто означает, что команда завершилась с кодом 2, и ничего не говорит об аргументах. Помните, что ExecStartPre= не запускается через оболочку (shell), поэтому для использования конвейеров (pipes) и подстановочных знаков (globs) необходимо использовать /bin/sh -c '...'.

#systemd#units#dependencies#ordering#troubleshooting