Понимание зависимостей systemd: предотвращение и устранение конфликтов модулей
Узнайте, как работают зависимости, порядок запуска, цели и конфликты systemd, чтобы службы запускались надежно, а сбои было легче отлаживать.
Понимание зависимостей systemd: предотвращение и устранение конфликтов модулей
Зависимости systemd легко понять наполовину. В файл модуля добавляется Requires=postgresql.service, приложение все равно запускается слишком рано, и все задаются вопросом, почему systemd проигнорировал зависимость. Он ничего не игнорировал. Requires= и After= отвечают на разные вопросы.
Это различие лежит в основе большинства проблем с зависимостями systemd. Одна директива управляет тем, будет ли другой модуль включен в ту же транзакцию. Другая директива управляет порядком. Другие директивы связывают поведение при остановке, привязывают время жизни одного модуля к другому или делают два модуля взаимоисключающими. Как только вы разделите эти понятия, конфликты модулей станут гораздо менее загадочными.
Основа: директивы зависимостей модулей systemd
Systemd использует определенные директивы в файлах модулей (обычно расположенных в /etc/systemd/system/ или /lib/systemd/system/), чтобы указать, когда один модуль должен запускаться, останавливаться или ожидать другой. Понимание этих директив — первый шаг к правильному управлению зависимостями.
Основные директивы зависимостей
Эти директивы управляют отношениями между модулями. Сами по себе они не всегда определяют порядок запуска:
Requires=:- Устанавливает строгую зависимость. Если требуемый модуль не запустится, текущий модуль также завершится ошибкой.
- Не подразумевает
PartOf=и не означает автоматически "запустить после этого модуля".
Wants=:- Слабая зависимость. Если желаемый модуль не запустится, текущий модуль все равно попытается запуститься. Используется для необязательных зависимостей.
BindsTo=:- Похоже на
Requires=, но сильнее в отношении остановки. Если связанный модуль останавливается (по любой причине), текущий модуль также останавливается.
- Похоже на
PartOf=:- Указывает, что текущий модуль является подчиненной частью другого модуля (например, активация определенного сокета, связанная с основным сервисом). Если вышестоящий модуль останавливается, подчиненный модуль также останавливается.
Conflicts=:- Указывает, что два модуля не должны быть активны одновременно. Запуск одного заставляет systemd остановить другой в рамках той же транзакции.
Основные директивы синхронизации запуска
Эти директивы определяют, когда зависимый модуль должен запускаться относительно требуемого модуля:
After=:- Указывает, что задание на запуск текущего модуля упорядочено после задания на запуск указанного модуля. Само по себе оно не подтягивает этот модуль.
Before=:- Указывает, что текущий модуль должен запускаться до указанного модуля.
Лучшая практика: Для типичного упорядочивания запуска служб комбинация
Wants=сAfter=является наиболее распространенным и безопасным шаблоном.Requires=следует использовать для зависимостей, где сбой зависимости должен привести к сбою зависимой службы.
Пример: определение зависимостей в файле службы
Рассмотрим пользовательскую службу приложения myapp.service, которая должна взаимодействовать с базой данных, управляемой PostgreSQL (postgresql.service).
# /etc/systemd/system/myapp.service
[Unit]
Description=Мое пользовательское приложение
# Убедиться, что PostgreSQL запущен, прежде чем пытаться запустить меня
Requires=postgresql.service
After=postgresql.service
[Service]
ExecStart=/usr/bin/myapp
[Install]
WantedBy=multi-user.target
Этот пример намеренно строгий. Если PostgreSQL не может запуститься, myapp.service также должен завершиться ошибкой. Для службы, которая может работать в ухудшенном режиме без базы данных, используйте Wants=postgresql.service с тем же порядком After=postgresql.service. Приложение все равно попросит systemd сначала запустить PostgreSQL, но ему будет разрешено продолжить работу, если PostgreSQL недоступен.
Нет ничего постыдного в выборе более слабой связи, когда это соответствует реальности. Экспортер метрик, пересыльщик логов или прогреватель кэша могут быть полезны, когда их зависимость присутствует, но не должны блокировать загрузку основной системы. Жесткие зависимости лучше всего резервировать для случаев, когда запуск без другого модуля был бы неправильным или опасным.
Для сетевых приложений будьте более осторожны. network.target часто означает, что базовая сетевая подсистема присутствует, но не то, что DHCP завершен или DNS доступен. Если вашему приложению действительно нужно настроенное сетевое подключение при запуске, используйте:
[Unit]
Wants=network-online.target
After=network-online.target
Затем убедитесь, что в вашем дистрибутиве включена соответствующая служба ожидания подключения, такая как systemd-networkd-wait-online.service или модуль ожидания NetworkManager. Без этого network-online.target может не ждать того, что вы думаете.
Диагностика проблем с зависимостями
Когда служба не запускается, systemd обычно предоставляет достаточно информации в журналах, но цепочки зависимостей могут скрывать первопричину. Вот основные инструменты и команды для устранения неполадок.
1. Проверка статуса модуля и журналов
Основная отправная точка — проверка статуса службы и просмотр ее журналов сразу после неудачной попытки запуска.
# Проверка общего статуса, который часто упоминает ошибки зависимостей
systemctl status myapp.service
# Просмотр подробных журналов, относящихся к модулю
journalctl -u myapp.service --since "5 minutes ago"
2. Анализ дерева зависимостей
Systemd предоставляет мощные инструменты визуализации, чтобы точно увидеть, что ждет чего.
systemctl list-dependencies
Эта команда показывает модули, которые требуются или желательны для указанного модуля, проходя по всей цепочке зависимостей.
Чтобы увидеть, что требуется для запуска myapp.service:
# Прямые зависимости (что должно запуститься до меня)
systemctl list-dependencies --after myapp.service
# Обратные зависимости (что зависит от меня)
systemctl list-dependencies --before myapp.service
Используйте --plain, когда форматирование дерева мешает, и добавьте --all, когда нужно увидеть также неактивные модули:
systemctl list-dependencies --plain --all myapp.service
systemd-analyze dot
Для более крупных вопросов о зависимостях создайте граф и просмотрите его визуально:
systemd-analyze dot 'myapp.service' | dot -Tsvg > myapp-deps.svg
На сервере без установленного Graphviz текстовый вывод все еще полезен, потому что вы можете искать имена модулей и видеть, какие ребра известны systemd. Для проблем со временем загрузки systemd-analyze critical-chain myapp.service часто проще читать, чем полный граф.
3. Обнаружение конфликтов и проблем с порядком
Конфликты зависимостей часто проявляются как сбои служб из-за того, что они были запущены слишком рано или неожиданно остановились.
Циклические зависимости: Это самый опасный конфликт, когда модуль A требует B, а модуль B требует A. Systemd пытается разрешить это, но часто это приводит к тому, что один или оба модуля остаются в состоянии failed или activating на неопределенный срок.
Чтобы найти потенциальные проблемы во всей системе, вы можете поискать в журналах конкретные сообщения об ошибках, связанные с упорядочиванием:
journalctl -b | grep -E "failed|refused to start|dependency was not satisfied"
Также запустите systemd-analyze verify для пользовательских модулей, прежде чем предполагать, что проблема в поведении во время выполнения:
systemd-analyze verify /etc/systemd/system/myapp.service
Это может выявить циклы упорядочивания, неизвестные директивы и недопустимые ссылки на модули на раннем этапе. Это не докажет, что ваш дизайн правильный, но может избавить вас от погони за опечаткой, как за сложной проблемой зависимости.
Исправление распространенных проблем с зависимостями
После выявления проблемы с зависимостями можно устранить, отрегулировав директивы в соответствующих файлах модулей.
Сценарий 1: Служба запускается до того, как ее предварительное условие готово
Симптом: Журналы вашего приложения показывают ошибки подключения к базе данных, но postgresql.service отображается как active в systemctl status.
Диагноз: Службе может не хватать After=postgresql.service, или PostgreSQL может быть активен до того, как конкретная база данных, сокет, учетные данные или схема, необходимые вашему приложению, будут готовы. Systemd может упорядочивать модули, но не может автоматически понимать все условия готовности на уровне приложения.
Исправление: Начните с простого отношения модулей:
[Unit]
Requires=postgresql.service
After=postgresql.service
Если приложение все еще "обгоняет" базу данных, исправьте проблему готовности на правильном уровне. Некоторые службы поддерживают Type=notify и сообщают о готовности только после инициализации. Некоторым приложениям нужна логика повторных попыток, потому что зависимости могут перезапускаться в любое время, а не только во время загрузки. Для узкой локальной проверки команда ExecStartPre= может быть разумной:
[Service]
ExecStartPre=/usr/bin/pg_isready -q -h 127.0.0.1 -p 5432
ExecStart=/usr/local/bin/myapp
Используйте такие предварительные проверки экономно. Если они превращаются в shell-скрипт с паузами и циклами, приложению, вероятно, требуется правильное поведение с повторными попытками.
Избегайте sleep 30 как исправления зависимости. Это может скрыть состояние гонки на тихой машине разработчика и снова проявиться на более медленном хранилище, загруженной виртуальной машине или хосте, ожидающем DNS. Настоящая директива упорядочивания, уведомление о готовности, активация сокета или цикл повторных попыток приложения дают вам причину, по которой служба готова, а не надежду, что прошло достаточно времени.
Сценарий 2: Конфликт порядка запуска/остановки
Симптом: Остановка системы приводит к зависанию или внезапному сбою критических процессов.
Диагноз: Это часто указывает на неправильное использование BindsTo= или сложное взаимодействие между директивами Before= и After= в родственных службах.
Исправление: Просмотрите службы, которые являются родственными (например, службы, запускаемые одной целью). Убедитесь, что если служба A должна работать, пока работает служба B, вы используете BindsTo= или Requires=. Если служба A должна завершить свои задачи до того, как служба B начнет очистку, проверьте правильность порядка After=.
Помните, что порядок остановки обратен порядку запуска. Если app.service имеет After=database.service, то при остановке systemd остановит app.service перед database.service. Обычно это то, что нужно: приложение перестает принимать работу до того, как база данных исчезнет. Многие ошибки остановки возникают из-за отсутствия упорядочивания запуска, а не из-за отдельной настройки только для остановки.
Сценарий 3: Удаление ненужных зависимостей
Симптом: Загрузка системы происходит медленно, потому что в цепочку запуска подтягиваются ненужные службы.
Диагноз: Возможно, вы использовали Requires=, когда требовалось только необязательное соединение.
Исправление: Замените Requires= на Wants=. Если службе не обязательно нужна зависимость для функционирования, Wants= позволяет системе продолжить работу, даже если зависимость не удалась или замаскирована.
# Было (Слишком строго)
Requires=optional_logging.service
# Стало (Лучше)
Wants=optional_logging.service
After=optional_logging.service
Это особенно полезно для агентов мониторинга, экспортеров метрик, вспомогательных контейнеров и необязательных локальных кэшей. Если основная служба может выполнять полезную работу без них, Requires= превращает частичный сбой в полный. Используйте строгие зависимости для того, что действительно необходимо: локальная база данных для приложения, которое не может запуститься без нее, точка монтирования, содержащая данные приложения, или модуль сокета, который является единственным поддерживаемым путем активации.
Сценарий 4: Монтирование или устройство не готово
Симптом: Служба не запускается при загрузке с ошибкой No such file or directory, но путь существует после входа в систему. Это часто происходит со службами, которые читают из /mnt/data, /srv/app, сменных дисков, зашифрованных томов или сетевых файловых систем.
Диагноз: Служба запускается до того, как модуль монтирования станет активным, или монтирование является необязательным и не удалось без остановки службы.
Исправление: Найдите имя модуля монтирования:
systemd-escape -p --suffix=mount /mnt/data
Для /mnt/data это обычно дает mnt-data.mount. Затем упорядочьте вашу службу после него и сделайте его обязательным, если служба не может работать без данных:
[Unit]
Requires=mnt-data.mount
After=mnt-data.mount
Если монтирование происходит из /etc/fstab, такие параметры, как nofail, x-systemd.automount и _netdev, влияют на поведение при загрузке. Не добавляйте жесткое Requires= к необязательному монтированию, если вы действительно не хотите, чтобы сбой этого монтирования блокировал службу.
Сценарий 5: Цель подтягивает слишком много
Симптом: Включение службы, кажется, запускает несвязанные модули, или отключение одной службы не мешает ей появляться во время загрузки.
Диагноз: Служба может подтягиваться целью через [Install] WantedBy=..., другой службой через Wants=, или модулем сокета, таймера, пути или монтирования. enable создает символические ссылки для активации при загрузке; это не единственный способ запуска модуля.
Исправление: Проверьте как сам модуль, так и обратные зависимости:
systemctl cat myapp.service
systemctl list-dependencies --reverse myapp.service
systemctl list-timers --all | grep myapp
systemctl list-sockets --all | grep myapp
Если модуль сокета активирует службу, отключение только myapp.service может быть недостаточным. Возможно, потребуется также отключить или замаскировать myapp.socket, в зависимости от желаемого поведения.
Применение изменений и перезагрузка
Всякий раз, когда вы изменяете файл модуля, вы должны указать systemd перезагрузить свою конфигурацию перед тестированием изменений.
# 1. Перезагрузить конфигурацию менеджера systemd
sudo systemctl daemon-reload
# 2. Перезапустить затронутую службу
sudo systemctl restart myapp.service
# 3. Проверить статус
systemctl status myapp.service
Небольшой мысленный контрольный список
Когда проблема с зависимостями кажется запутанной, сведите ее к нескольким простым проверкам.
Во-первых, спросите, должен ли другой модуль вообще запускаться. Если да, используйте Wants= или Requires=. Если текущая служба может работать без него, предпочтите Wants=. Если сбой этого модуля означает, что эта служба должна завершиться ошибкой, используйте Requires=.
Во-вторых, спросите, важен ли порядок запуска. Если да, добавьте After= или Before=. Не ожидайте, что директивы зависимостей сами обработают упорядочивание.
В-третьих, спросите, важна ли связь времени жизни после запуска. Если остановка одного модуля должна остановить другой, посмотрите на BindsTo= или PartOf=. Не используйте их небрежно; они могут сделать рутинные перезапуски каскадными дальше, чем ожидалось.
Наконец, проверьте, что systemd фактически загрузил:
systemctl cat myapp.service
systemctl show myapp.service -p Wants -p Requires -p After -p Before -p Conflicts
Этот вывод часто более полезен, чем файл, который вы, как вам кажется, редактировали. Файлы drop-in, модули поставщика, сгенерированные модули, сокеты, таймеры и цели — все это может изменить конечное поведение.