Диагностика Systemd: Понимание зависимостей служб и директив упорядочивания
Исправляйте проблемы с зависимостями и упорядочиванием в systemd с помощью правильного использования Requires, Wants, After, Before и диагностических команд.
Диагностика Systemd: Понимание зависимостей служб и директив упорядочивания
Большинство запутанных ошибок зависимостей systemd возникают из-за смешивания двух отдельных понятий: требования и порядка. Requires= и Wants= отвечают на вопрос "какой другой модуль следует подключить?" After= и Before= отвечают на вопрос "когда этот модуль должен запускаться относительно другого?" Если вы запомните только одно из этого руководства, запомните, что After=postgresql.service не запускает PostgreSQL за вас. Он только говорит, что если оба модуля запускаются, ваш модуль должен ждать, пока не выполнится задание запуска PostgreSQL.
Это различие является источником многих инцидентов "работает, когда я запускаю вручную, но не работает после перезагрузки". Веб-приложение запускается до того, как сокет базы данных начинает принимать соединения. Рабочий процесс запускается до того, как смонтирован /mnt/jobs. Служба ждет network.target и все равно не работает, потому что IP-адрес еще не назначен. Это не экзотические проблемы systemd. Это обычные предположения о запуске, которые необходимо явно указать.
Основные директивы зависимостей: Requires и Wants
Systemd использует две основные директивы для определения прямых зависимостей между модулями: Requires и Wants. Эти директивы размещаются в разделе [Unit] файла модуля (например, файла .service).
Requires=
Директива Requires= устанавливает строгую зависимость. Если модуль A Requires= модуль B, systemd запускает B при запуске A. Если B не может быть запущен, задание запуска A также завершается ошибкой. Если B явно остановлен, пока A работает, A обычно также останавливается, поскольку требуемое отношение больше не выполняется.
Пример:
Рассмотрим службу веб-приложения (myapp.service), которая критически зависит от службы базы данных (mariadb.service). Файл модуля myapp.service может включать:
[Unit]
Description=Мое веб-приложение
Requires=mariadb.service
[Service]
ExecStart=/usr/bin/myapp
[Install]
WantedBy=multi-user.target
В этом сценарии, если mariadb.service не запустится или будет остановлен вручную, systemd также остановит myapp.service. Если вы попытаетесь запустить myapp.service, а mariadb.service не работает, systemd попытается сначала запустить mariadb.service. Если mariadb.service не запустится, myapp.service не запустится.
Wants=
Директива Wants= определяет более слабую, необязательную зависимость. Если модуль A Wants= модуль B, systemd попытается запустить модуль B при запуске модуля A, но модуль A все равно активируется, даже если B не запустится или не работает. Это полезно для служб, которые выигрывают от другой службы, но могут функционировать независимо, возможно, с ограниченными функциями или предупреждением.
Пример:
Предположим, агент мониторинга (monitoring-agent.service) может работать без конкретной службы ведения журнала (app-logger.service), но в идеале она должна быть доступна. Файл модуля monitoring-agent.service может выглядеть так:
[Unit]
Description=Агент мониторинга
Wants=app-logger.service
[Service]
ExecStart=/usr/bin/monitoring-agent
[Install]
WantedBy=multi-user.target
Здесь systemd попытается запустить app-logger.service при активации monitoring-agent.service. Однако, если app-logger.service не запустится, monitoring-agent.service все равно успешно запустится.
Requires= против Wants=
Requires=: Строгая зависимость. Если требуемый модуль выходит из строя, зависимый модуль выходит из строя или останавливается.Wants=: Слабая зависимость. Зависимый модуль пытается запустить желаемый модуль, но продолжает работу, даже если он не запускается.
Важно отметить, что Requires= подразумевает Wants=. Если модуль требует другой, он также неявно его желает.
Директивы упорядочивания: After и Before
В то время как Requires и Wants определяют, что должно работать, After и Before определяют, когда модули должны запускаться относительно друг друга. Эти директивы управляют последовательностью операций во время загрузки системы или при активации модулей по требованию. Они часто используются вместе с директивами зависимостей.
After=
Директива After= указывает, что задание запуска текущего модуля должно быть упорядочено после заданий запуска перечисленных модулей. Сама по себе она не подтягивает эти модули в транзакцию. Она также не доказывает, что зависимость логически готова для вашего приложения; она использует только представление systemd о состоянии активации модуля.
Пример:
Служба, зависящая от сети (custom-network-app.service), должна запускаться только после полной настройки сети. Обычно это обрабатывается путем обеспечения ее запуска после сетевой цели (network.target).
[Unit]
Description=Пользовательское сетевое приложение
Requires=network.target
After=network.target
[Service]
ExecStart=/usr/bin/custom-network-app
[Install]
WantedBy=multi-user.target
В этой конфигурации systemd упорядочит custom-network-app.service после network.target, если оба являются частью одной транзакции. Для служб, которым нужен адрес, DNS или маршрут к другому хосту, network-online.target часто ближе к намерению, но только если служба ожидания подключения дистрибутива включена и правильно настроена.
Before=
Директива Before= указывает, что текущий модуль должен быть запущен до модулей, перечисленных в Before=. Это полезно для служб, которые необходимо останавливать после других во время выключения, или запускать до определенных служб, чтобы обеспечить для них среду.
Пример:
Представьте сценарий, в котором почтовый сервер (postfix.service) должен работать до любых пользовательских служб, которые могут отправлять электронные письма. Вы можете использовать Before=, чтобы гарантировать, что postfix.service запускается рано.
[Unit]
Description=Почтовый транспортный агент Postfix
# ... другие директивы, такие как Conflicts=
Before=user-session.target
[Service]
ExecStart=/usr/lib/postfix/master
[Install]
WantedBy=multi-user.target
Эта настройка пытается запустить postfix.service до того, как что-либо, являющееся частью user-session.target, начнет свой запуск. Аналогично, во время выключения postfix.service будет одним из последних остановленных, если у него есть соответствующий After=user-session.target.
After= против Before=
After=: Гарантирует, что перечисленные модули активны до запуска текущего модуля.Before=: Гарантирует, что текущий модуль запускается до перечисленных модулей.
After= и Before= являются взаимодополняющими. Если модуль A говорит Before=B, порядок: сначала A, затем B. Если модуль B говорит After=A, результат тот же. Обычно вам нужно выразить отношение только в одном файле модуля. При редактировании собственной службы обычно понятнее указать, после чего ваша служба должна запускаться, потому что это локализует рассуждения.
Комбинирование директив для надежных конфигураций
В реальных сценариях вы часто будете комбинировать эти директивы для создания сложных графов зависимостей. multi-user.target — это общая цель, которая означает, что система готова к многопользовательской работе. Многие службы настроены как WantedBy=multi-user.target и After=multi-user.target (или, точнее, After=basic.target и After=getty.target и т.д., от которых зависит multi-user.target).
Распространенный шаблон:
Служба, которая требует базу данных и должна запускаться после настройки сети, может выглядеть так:
[Unit]
Description=Моя служба приложения
Requires=mariadb.service
Wants=other-optional-service.service
After=network.target mariadb.service
[Service]
ExecStart=/usr/local/bin/my_app
[Install]
WantedBy=multi-user.target
Объяснение шаблона:
Requires=mariadb.service: Гарантирует, чтоmariadb.serviceдолжна работать для функционированияmy_app.service. Еслиmariadb.serviceвыйдет из строя,my_app.serviceостановится.Wants=other-optional-service.service: Пытается запуститьother-optional-service.service, ноmy_app.serviceпродолжит работу, даже если он не запустится.After=network.target mariadb.service: Упорядочиваетmy_app.serviceпосле сетевой цели и задания запуска MariaDB. Если приложению необходимо подключиться по TCP к базе данных на другом хосте, используйте соответствующую настройку network-online вместо предположения, чтоnetwork.targetозначает "сеть доступна для использования".WantedBy=multi-user.target: При включении (systemctl enable my_app.service) эта директива добавляет символическую ссылку, так чтоmy_app.serviceзапускается, когда система достигает состоянияmulti-user.target.
Продвинутые соображения и лучшие практики
WantedByпротивRequiredBy: АналогичноWantsпротивRequires,WantedBy— это слабое упорядочивание, аRequiredBy— строгое упорядочивание. Большинство служб используютWantedBy=multi-user.target.Conflicts=: Эта директива указывает модули, которые не должны работать одновременно с текущим модулем. Если текущий модуль запущен, конфликтующие модули будут остановлены, и наоборот.- Транзитивные зависимости: Зависимости являются транзитивными. Если A требует B, а B требует C, то A косвенно требует C. Systemd обрабатывает эти цепочки автоматически.
- Директивы
Condition*=: ИспользуйтеConditionPathExists=,ConditionFileNotEmpty=,ConditionVirtualization=и т.д., чтобы сделать активацию модуля условной в зависимости от состояния системы, что еще больше повышает надежность. - Используйте
systemctl list-dependencies <unit>: Эта команда бесценна для визуализации дерева зависимостей модуля, включая прямые и косвенные зависимости. - Используйте
systemctl status <unit>: Всегда проверяйте состояние вашей службы после внесения изменений в конфигурацию. Она часто показывает причины сбоев, включая проблемы с зависимостями. - Избегайте циклических зависимостей: Хотя systemd пытается их разрешить, прямые циклические зависимости (
A Requires B,B Requires A) могут привести к циклам запуска или сбоям. Тщательно проектируйте свои зависимости, чтобы избежать этого.
Практический проход по отладке
Когда появляется ошибка зависимости, начните с просмотра транзакции, которую фактически построил systemd:
systemctl status my_app.service
journalctl -b -u my_app.service --no-pager
systemctl list-dependencies my_app.service
systemctl show my_app.service -p Wants -p Requires -p After -p Before -p BindsTo -p PartOf
systemctl status сообщает вам, не удалось ли systemd запустить модуль, убил ли он его позже или считает его активным, даже если приложение нездорово. journalctl -b удерживает вас в пределах текущей загрузки, что важно, потому что проблемы с зависимостями часто возникают только при загрузке. systemctl show — это грубый, но полезный инструмент: он показывает окончательные объединенные свойства модуля после применения drop-in файлов, файлов поставщика и сгенерированных зависимостей.
Если вы не уверены, откуда взялась зависимость, проверьте полный модуль:
systemctl cat my_app.service
Это показывает упакованный модуль и любые файлы переопределения в /etc/systemd/system/my_app.service.d/. Я видел производственные службы, где базовый модуль был правильным, но старое переопределение все еще содержало After=mysql.service от предыдущей миграции. Служба ждала модуль, который больше не существовал, и журналы создавали впечатление, что приложение сломано.
Для вопросов времени загрузки используйте:
systemd-analyze critical-chain my_app.service
systemd-analyze blame
critical-chain лучше, чем просмотр временных меток, потому что он показывает, какие модули задержали путь к вашей службе. blame может вводить в заблуждение, если рассматривать его как рейтинг "плохих" служб, но он полезен, когда одна зависимость занимает гораздо больше времени, чем ожидалось.
Шаблоны, которые работают в производстве
Для зависимости от локальной базы данных это разумная отправная точка:
[Unit]
Description=Служба API
Requires=postgresql.service
After=postgresql.service
[Service]
ExecStart=/usr/local/bin/api
User=api
Restart=on-failure
[Install]
WantedBy=multi-user.target
Это означает, что PostgreSQL должен быть запущен вместе с API, а запуск API должен быть упорядочен после PostgreSQL. Это не гарантирует, что все миграции завершены или что база данных принимает точные учетные данные, которые использует ваше приложение. Если это важно, добавьте проверку готовности на уровне приложения. Systemd может упорядочивать процессы, но он не может понять состояние вашей схемы, если вы не научите свою службу проверять его.
Для смонтированного пути предпочитайте RequiresMountsFor= ручному указанию модулей монтирования:
[Unit]
RequiresMountsFor=/srv/uploads
[Service]
ExecStart=/usr/local/bin/upload-worker
User=uploads
Systemd выведет необходимые модули монтирования для пути. Это проще поддерживать, чем помнить, что /srv/uploads соответствует srv-uploads.mount.
Для необязательных помощников используйте Wants=:
[Unit]
Wants=metrics-agent.service
After=metrics-agent.service
Если агент метрик выходит из строя, основная служба все равно может запуститься. Обычно это то, что вам нужно для боковых контейнеров ведения журнала, необязательных экспортеров и локальных помощников уведомлений. Не используйте Requires= только потому, что две службы связаны. Используйте его только тогда, когда зависимая служба действительно не может выполнять полезную работу без другого модуля.
Для тесно связанных служб, которые должны останавливаться вместе, посмотрите на BindsTo= и PartOf=. BindsTo= сильнее, чем Requires=, и полезен, когда служба должна исчезнуть, если связанный модуль исчезает, например, служба, привязанная к конкретному модулю устройства. PartOf= часто полезен для групп: перезапуск или остановка родительского модуля может распространяться на дочерние модули. Это не директивы первого выбора, но они решают проблемы, которые Requires= не может чисто выразить.
Распространенные ловушки
Не добавляйте After=multi-user.target к обычной долго работающей службе, которая включена с помощью WantedBy=multi-user.target. Это часто создает странное упорядочивание и редко говорит о том, что имел в виду автор. Большинство служб подтягиваются multi-user.target; им не нужно запускаться после того, как он уже достигнут.
Не предполагайте, что network.target означает "интернет доступен". Это точка синхронизации для управления сетью, а не тест на связность. Если ваше приложение общается с удаленным API, все равно добавьте логику повторных попыток внутри приложения. Сетевое упорядочивание при загрузке уменьшает шум, но не может защитить вас от сбоя DNS, изменений маршрутизации или недоступности удаленной зависимости.
Не прячьте длинные задержки в ExecStartPre=/bin/sleep 30, если у вас нет лучшего варианта. Задержки замедляют загрузку, когда зависимость готова быстро, и все равно не работают, когда зависимость занимает больше времени, чем ожидалось. Небольшой цикл проверки готовности, который проверяет фактический сокет, файл или API, обычно понятнее.
Когда вы изменяете директивы зависимостей, выполните systemctl daemon-reload, перезапустите службу и проверьте журнал из той же загрузки. Самое быстрое исправление обычно то, которое доказывает, что точное предположение об упорядочивании было неверным.