Распространенные ошибки конфигурации Systemd и способы их исправления

Исправьте типичные ошибки в файлах модулей systemd: неверные пути, неправильные типы служб, отсутствующие переменные окружения, проблемы с правами доступа и порядком зависимостей.

Распространенные ошибки конфигурации Systemd и способы их исправления

Ошибки конфигурации systemd обычно выглядят более драматично, чем есть на самом деле. Служба отказывается запускаться, развертывание откатывается или загрузка зависает на имени модуля, которое вы едва помните, что создавали. Затем настоящая причина оказывается пропущенным слешем в ExecStart=, процессом, работающим от имени не того пользователя, или изменением файла модуля, которое так и не дошло до менеджера systemd, потому что никто не выполнил daemon-reload.

Самый быстрый способ решения этих проблем — рассматривать файл модуля как контракт. Он сообщает systemd, какой процесс запускать, от имени какого пользователя, что должно существовать сначала, как сообщается о готовности и что должно произойти после сбоя. Когда одна из этих деталей неверна, systemd обычно делает именно то, что ему сказали. Задача состоит в том, чтобы найти несоответствие между тем, что нужно приложению, и тем, что на самом деле указано в файле модуля.

1. Ошибки синтаксиса и путей в файлах модулей

Одна из самых частых причин сбоя службы — простая опечатка или неправильно указанный путь в файле модуля.

Неверные или неабсолютные пути в командах Exec

Systemd не запускает вашу службу из той же сессии оболочки, которую вы использовали для тестирования. Он запускает процесс в контролируемой среде, поэтому предположения об алиасах, функциях оболочки, активации virtualenv и пользовательском PATH часто не оправдываются. Используйте абсолютные пути к исполняемому файлу в ExecStart= и явно указывайте каждый каталог или файл, необходимый службе.

Ошибка:

Использование имени команды без указания ее расположения.

[Service]
ExecStart=my-app-server --config /etc/config.yaml

Если my-app-server находится в /usr/local/bin, systemd, скорее всего, не найдет его.

Исправление:

Всегда используйте полный абсолютный путь к исполняемому файлу.

[Service]
ExecStart=/usr/local/bin/my-app-server --config /etc/config.yaml

Перед настройкой ExecStart= проверьте путь с помощью command -v my-app-server или which my-app-server. Если приложение находится в специфическом для языка месте, например, в виртуальном окружении Python по пути /opt/myapp/venv/bin/gunicorn, указывайте непосредственно на этот бинарный файл, не полагаясь на скрипты активации.

Орфографические ошибки и чувствительность к регистру

Директивы конфигурации systemd чувствительны к регистру и должны быть размещены в правильных разделах ([Unit], [Service], [Install]). Опечатки или неправильный регистр приведут к тому, что служба не загрузится или будет вести себя неожиданно.

Пример ошибки:

[Service]
ExecStart=/usr/bin/python3 app.py
RestartAlways=true  ; Должно быть Restart=always

Исправление:

Используйте systemd-analyze verify <unit_file> перед перезагрузкой демона. Он не поймает все ошибки времени выполнения, но обнаружит многие неправильно написанные директивы, неверное размещение разделов и ошибки парсинга до того, как вы потратите время на изучение журналов приложения.

$ systemd-analyze verify /etc/systemd/system/my-service.service

2. Неправильное управление зависимостями служб и порядком их запуска

Зависимости определяют, какие ресурсы нужны службе, а порядок определяет, когда эти ресурсы должны быть доступны.

Путаница между Requires и Wants

Эти директивы используются для определения зависимостей, но по-разному обрабатывают сбои:

  • Wants=: Слабая зависимость. Если требуемый модуль не запускается или выходит из строя, текущий модуль все равно попытается запуститься. Используйте для некритичных зависимостей.
  • Requires=: Сильная зависимость. Если требуемый модуль не может быть запущен, текущий модуль также завершается ошибкой. Если требуемый модуль явно остановлен, зависимый модуль также останавливается.

Использование Requires без правильного порядка

Определение зависимости, например Requires=network.target, добавляет зависимость в транзакцию. Само по себе оно не создает порядок запуска, и network.target не означает "сеть готова для исходящих соединений". Если вашей службе требуется настроенная сеть, используйте network-online.target и убедитесь, что служба ожидания подключения дистрибутива включена, когда требуется такое поведение.

Ошибка:

Веб-сервер запускается, но соединение с базой данных не удается, потому что сетевой стек все еще инициализируется.

Исправление: Использование After= и Before=

Для обеспечения порядка необходимо использовать After= (или Before=). Часто требуется убедиться, что сеть полностью поднята и настроена перед продолжением.

[Unit]
Description=Служба моего веб-приложения
Wants=network-online.target
After=network-online.target

[Service]
...

Для большинства служб приложений сочетайте намерение зависимости с намерением порядка. Wants=postgresql.service означает "пожалуйста, запусти также PostgreSQL". After=postgresql.service означает "запусти меня после завершения задания запуска PostgreSQL". Они решают разные проблемы.

Неправильное управление типом службы

Службы systemd имеют несколько типов выполнения, управляемых директивой Type=. Неправильная настройка этого параметра является частой причиной того, что службы запускаются на мгновение и сразу же выходят из строя.

Ошибка: Неправильное использование Type=forking

Если ваше приложение предназначено для работы на переднем плане и поддержания одного основного процесса, установка Type=forking сообщает systemd, что ожидается поведение демона старого образца. Это может привести к путанице: systemd может ждать выхода родительского процесса, не сможет определить реальный основной процесс или пометит службу как активную, хотя приложение на самом деле не готово.

Исправления:

  1. Для современных приложений: Используйте Type=simple. Это значение по умолчанию, и оно ожидает, что процесс ExecStart будет основным процессом.
  2. Для устаревших приложений, которые демонизируются (создают форк): Установите Type=forking и, что крайне важно, определите директиву PIDFile=, чтобы systemd мог отслеживать дочерний процесс, переживший форк.
[Service]
Type=forking
PIDFile=/var/run/legacy-app.pid
ExecStart=/usr/sbin/legacy-app

Существует еще одна распространенная ловушка готовности: использование Type=simple для приложения, которому требуется много времени, чтобы стать работоспособным. При Type=simple systemd считает службу запущенной, как только процесс порожден. Если другая служба запускается сразу после этого и подключается к ней, вы можете увидеть перемежающиеся сбои. Для приложений, которые могут уведомить systemd о своей готовности, Type=notify чище. Для приложений, которые не могут этого сделать, избегайте притворяться, что модуль полностью готов только потому, что процесс существует; используйте реальную проверку работоспособности в зависимом приложении, активацию сокета или проверку ExecStartPre=, когда предварительное условие достаточно простое для тестирования.

Будьте осторожны с oneshot. Служба Type=oneshot предназначена для команды, которая выполняет задачу и завершается, например, создание каталога, загрузка правила брандмауэра или запуск миграции. Если вы используете ее для долго работающего демона, systemd не будет контролировать его так, как вы ожидаете. Если команда завершается успешно и вы хотите, чтобы модуль оставался "активным" для целей зависимостей, добавьте RemainAfterExit=yes; в противном случае зависимые модули могут не увидеть то состояние, которое вы предполагали.

3. Проблемы с окружением и контекстом пользователя

Сбои служб часто возникают из-за того, что служба работает в контексте, отличном от того, который ожидает приложение, обычно это связано с правами доступа или переменными окружения.

Отказ в доступе или отсутствующие файлы

При ручном тестировании приложение обычно работает под вашей учетной записью с соответствующими разрешениями. При запуске через systemd оно по умолчанию работает от имени пользователя root или пользователя, указанного в файле модуля.

Ошибка:

Типичные симптомы очевидны: Permission denied, No such file or directory, Failed to open log file или специфическая для приложения ошибка, сообщающая, что оно не может создать сокет, записать PID-файл или прочитать файл конфигурации. Модуль может работать, когда вы запускаете команду вручную от имени root, а затем выходить из строя при User=app.

Исправление:

  1. Определите непривилегированного пользователя: Всегда указывайте выделенного пользователя и группу с низкими привилегиями для вашей службы.

    [Service]
    User=www-data
    Group=www-data
    ...
    
  2. Проверьте права собственности: Убедитесь, что рабочий каталог службы, файлы журналов и файлы конфигурации принадлежат указанным User= и Group=.

    sudo chown -R www-data:www-data /var/www/my-app
    
  3. Проверьте каждый путь, к которому обращается служба: Не останавливайтесь на каталоге приложения. Проверьте WorkingDirectory=, каталоги журналов, каталоги загрузки, каталоги кэша, файлы ключей TLS, Unix-сокеты и любые пути, указанные в файле окружения.

    sudo -u www-data test -r /etc/my-app/config.yml
    sudo -u www-data test -w /var/lib/my-app
    sudo -u www-data /usr/local/bin/my-app --check-config
    

Если службе нужно привязаться к порту 80 или 443, не запускайте ее автоматически от имени root. Во многих системах вы можете поместить перед ней обратный прокси-сервер, использовать активацию сокета или предоставить бинарному файлу конкретную необходимую возможность. Правильный выбор зависит от службы, но широкий процесс root не должен быть ответом по умолчанию.

Еще одна деталь, касающаяся прав доступа, которая часто упускается: родительские каталоги должны иметь право на выполнение. Файл может выглядеть читаемым, но служба все равно не может получить к нему доступ, потому что /opt, /opt/myapp или другой родительский каталог блокирует обход для пользователя службы. namei -l /opt/myapp/config.yml полезен, потому что показывает разрешения для каждого компонента пути, а не только для конечного файла.

Отсутствующие переменные окружения

Службы systemd работают в минимальном окружении. Любые важные переменные окружения (например, ключи API, строки подключения к базе данных или пользовательские пути к библиотекам) должны быть явно переданы.

Исправление: Использование Environment= или EnvironmentFile=

Для простых переменных используйте Environment=:

[Service]
Environment="APP_PORT=8080"
Environment="API_KEY=ABCDEFG"

Для сложных или многочисленных переменных используйте EnvironmentFile=, указывающий на стандартный файл .env:

[Service]
EnvironmentFile=/etc/default/my-app.conf

Храните секреты в файлах модулей, недоступных для чтения всем. Файл модуля в /etc/systemd/system обычно доступен для чтения локальным пользователям. Если вы помещаете ключи API непосредственно в Environment=, считайте, что они раскрыты любому, кто может читать файлы модулей или просматривать метаданные процессов. Предпочтительнее использовать файл окружения, принадлежащий root, с ограничительными правами доступа, менеджер секретов или учетные данные systemd в дистрибутивах, которые их поддерживают.

Также помните, что EnvironmentFile= — это не скрипт оболочки. Строки типа export APP_PORT=8080 или подстановки команд, такие как TOKEN=$(cat /run/token), не интерпретируются так, как в Bash. Используйте простые присваивания:

APP_PORT=8080
APP_ENV=production

Если приложению требуется настройка оболочки входа, это обычно признак проблемы. Поместите реальное окружение в файл модуля, файл окружения или собственную конфигурацию приложения вместо того, чтобы полагаться на .bashrc.

Для сред выполнения языков программирования указывайте systemd на ту среду, которую вы фактически тестировали. Служба Python обычно должна вызывать бинарный файл виртуального окружения напрямую, например /opt/myapp/venv/bin/python или /opt/myapp/venv/bin/gunicorn. Служба Node, установленная через менеджер версий, может работать в вашем терминале, но выходить из строя в systemd, потому что nvm или asdf изменили только вашу интерактивную оболочку. В производственных модулях явные пути лучше, чем магия запуска оболочки.

4. Важнейший рабочий процесс отладки

Самая распространенная ошибка конфигурации — забыть о важнейшем шаге между редактированием файла модуля и попыткой перезапустить службу.

Забывание перезагрузить демон

Systemd не отслеживает изменения файлов модулей автоматически. После любого изменения файла в /etc/systemd/system/ необходимо дать менеджеру systemd указание перезагрузить кэш своей конфигурации.

Ошибка:

Вы редактируете файл, запускаете systemctl restart my-service, но используется старая конфигурация.

Исправление: Выполните daemon-reload

Всегда выполняйте эту команду сразу после сохранения изменения файла модуля:

sudo systemctl daemon-reload
sudo systemctl restart my-service

Если вы редактировали переопределение с помощью systemctl edit my-service, применяется то же правило. Сгенерированное переопределение сохраняется в /etc/systemd/system/my-service.service.d/, и systemd все равно необходимо перезагрузить кэш модулей, прежде чем новые настройки вступят в силу.

Когда перезапуск ведет себя странно, проверьте точный объединенный модуль, который видит systemd:

systemctl cat my-service.service
systemctl show my-service.service -p FragmentPath -p DropInPaths -p User -p ExecStart

Это позволяет выявить распространенную ошибку: редактирование модуля поставщика в /usr/lib/systemd/system, в то время как переопределение в /etc/systemd/system все еще изменяет настройку, или редактирование копии модуля, который не является тем, который загрузил systemd.

Эффективное использование инструментов журналирования

Когда служба выходит из строя, полагайтесь на официальные инструменты для точной диагностики.

  1. Проверьте статус службы: Это дает вам немедленное состояние, коды выхода и последние несколько строк журнала.

    systemctl status my-service.service
    
  2. Просмотрите журнал: Журнал содержит полный вывод (stdout/stderr) службы. Ищите подсказки, такие как "Permission denied" или "No such file or directory".

    # Просмотр последних журналов, относящихся к вашему модулю
    journalctl -u my-service.service --since '1 hour ago' 
    
    # Просмотр журналов и отслеживание вывода в реальном времени
    journalctl -f -u my-service.service
    

Практический проход по устранению неполадок

Когда я проверяю неработающий модуль, я обычно делаю один проход в таком порядке:

systemctl status my-service.service
journalctl -u my-service.service --since "15 minutes ago"
systemctl cat my-service.service
systemd-analyze verify /etc/systemd/system/my-service.service

Затем я задаю простые вопросы. Указывает ли ExecStart= на реальный исполняемый файл? Может ли настроенный User= запустить его? Существует ли WorkingDirectory=? Присутствуют ли переменные окружения без необходимости полагаться на оболочку? Честен ли Type= относительно поведения процесса? Присутствуют ли Wants= и After= оба, когда мне нужно, чтобы другой модуль был запущен и упорядочен перед этим?

После каждого редактирования перезагрузите и проверьте одну вещь:

sudo systemctl daemon-reload
sudo systemctl restart my-service.service
systemctl status my-service.service --no-pager

Если служба все еще не работает, удержитесь от желания продолжать слепо изменять файл модуля. Журнал обычно сообщает вам, является ли следующая проблема конфигурацией systemd, конфигурацией приложения, правами доступа или зависимостью, которая выходит из строя сама по себе.