Понимание юнитов systemd: глубокое погружение в настройку служб

Узнайте, как работают юниты служб systemd, включая Unit, Service, Install, переопределения, перезапуски и журналы.

Понимание юнитов systemd: глубокое погружение в настройку служб

Файлы юнитов systemd — это небольшие текстовые файлы, которые определяют, как запускаются службы, от чего они зависят, от имени какого пользователя они работают и что происходит при сбое. Если вы когда-нибудь задавались вопросом, почему systemctl restart myapp.service работает для одного приложения, но не для другого, ответ обычно кроется в файле юнита.

Это руководство фокусируется на юнитах .service, поскольку именно их администраторы и разработчики редактируют чаще всего. Та же система управляет сокетами, таймерами, точками монтирования, устройствами, путями и целевыми объектами, но файлы служб — это то место, где чаще всего возникают операционные ошибки.

Что такое файлы юнитов systemd?

Файлы юнитов systemd — это простые текстовые файлы, содержащие директивы конфигурации для конкретного юнита. Юнит представляет собой ресурс, управляемый systemd. Наиболее распространенным типом является юнит службы, который определяет, как запускать, останавливать, перезапускать фоновый процесс или приложение и управлять им.

Файлы юнитов организованы в разделы, каждый из которых обозначается квадратными скобками ([]). Наиболее важными разделами для юнитов служб являются:

  • [Unit]: Содержит метаданные о юните, зависимости и порядок.
  • [Service]: Определяет поведение самой службы, включая способ ее выполнения.
  • [Install]: Указывает, как юнит должен быть включен или отключен, обычно связывая его с целевыми юнитами.

Systemd ищет файлы юнитов в нескольких стандартных каталогах, наиболее распространенными из которых являются:

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

Когда вам нужно проверить юнит, не угадывайте путь. Используйте:

systemctl cat nginx.service
systemctl show -p FragmentPath nginx.service

systemctl cat особенно полезен, потому что он показывает базовый юнит вместе с любыми переопределениями drop-in. Это версия, которую systemd фактически использует.

Анатомия файла юнита .service

Давайте разберем типичный файл юнита .service, чтобы понять его компоненты.

Раздел [Unit]

Этот раздел предоставляет описательную информацию и определяет взаимосвязи между юнитами.

  • Description=: Понятное человеку описание службы.
  • Documentation=: URL-адреса или пути к документации для службы.
  • After=: Указывает, что этот юнит должен запускаться после завершения запуска перечисленных юнитов.
  • Requires=: Похож на After=, но также делает перечисленные юниты обязательными. Если обязательный юнит не запустится, этот юнит также завершится ошибкой.
  • Wants=: Более слабая форма зависимости. Этот юнит попытается запустить свои желаемые юниты, но их сбой не помешает запуску этого юнита.
  • Conflicts=: Указывает юниты, которые не могут работать одновременно с этим юнитом.

Пример раздела [Unit]:

[Unit]
Description=Мой пользовательский веб-сервер
Documentation=https://example.com/docs/my-web-server
After=network.target

Это указывает, что наш пользовательский веб-сервер должен запускаться после того, как станет доступна сеть.

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

Даже в этом случае зависимости не являются проверками работоспособности. After=network.target не гарантирует, что DNS работает, удаленный API доступен или база данных принимает соединения. Ваше приложение все равно должно иметь разумное поведение повторных попыток.

Раздел [Service]

Здесь находится основная логика запуска службы.

  • Type=: Определяет тип запуска процесса. Распространенные типы включают:
    • simple (по умолчанию): Основной процесс — это процесс, запущенный ExecStart=. Systemd считает службу запущенной сразу после разветвления процесса ExecStart=.
    • forking: Используется для традиционных демонов, которые создают дочерний процесс и завершаются. Systemd ожидает завершения родительского процесса.
    • oneshot: Для задач, которые выполняют одну команду и затем завершаются.
    • notify: Служба отправляет уведомление systemd, когда она завершила запуск.
    • dbus: Для служб, которые получают имя D-Bus.
  • ExecStart=: Команда для выполнения запуска службы.
  • ExecStop=: Команда для выполнения остановки службы.
  • ExecReload=: Команда для перезагрузки конфигурации службы без ее перезапуска.
  • Restart=: Определяет, когда следует перезапускать службу. Варианты включают no (по умолчанию), on-success, on-failure, on-abnormal, on-watchdog, on-abort и always.
  • RestartSec=: Время ожидания перед перезапуском службы.
  • User= / Group=: Пользователь и группа, от имени которых должна работать служба.
  • WorkingDirectory=: Рабочий каталог для выполняемых процессов.
  • Environment= / EnvironmentFile=: Устанавливает переменные окружения для службы.

Пример раздела [Service]:

[Service]
Type=simple
ExecStart=/usr/local/bin/my-web-server --config /etc/my-web-server.conf
User=www-data
Group=www-data
Restart=on-failure
RestartSec=5

Эта конфигурация запускает наш веб-сервер, запускает его от имени пользователя и группы www-data и автоматически перезапускает его в случае сбоя с задержкой в 5 секунд.

Type= заслуживает особого внимания. Многие сломанные юниты используют Type=forking, потому что старый init-скрипт использовал режим демона. Для современного приложения, которое остается на переднем плане, Type=simple обычно правильный. Если ваш процесс разветвляется в фоновый режим, но systemd не сообщается, как идентифицировать реальный основной процесс, отчеты о состоянии и перезапуски могут вводить в заблуждение.

Для разовой задачи используйте Type=oneshot и часто RemainAfterExit=yes, если выполненное действие должно считаться активным. Например, юнит, который подготавливает правило брандмауэра или монтирует специальный ресурс, может успешно завершиться, но все равно представлять состояние, которое вас волнует.

Раздел [Install]

Этот раздел используется при включении или отключении юнита. Он определяет, как юнит интегрируется с целевыми юнитами systemd.

  • WantedBy=: Указывает цель(и), которая должна "желать" этот юнит, когда он включен. Для служб, которые должны запускаться при загрузке, обычно используется multi-user.target.

Пример раздела [Install]:

[Install]
WantedBy=multi-user.target

Когда вы запускаете systemctl enable my-custom-service.service, systemd создает символическую ссылку из /etc/systemd/system/multi-user.target.wants/ на ваш файл юнита, гарантируя, что он запускается при достижении системой многопользовательского уровня запуска.

Если у юнита нет раздела [Install], он все равно может быть вполне допустимым. Просто его нельзя напрямую включить с помощью systemctl enable, если не существует другого механизма установки. Некоторые юниты предназначены для запуска зависимостями, сокетами, таймерами или целями, а не для включения вручную.

Создание пользовательских юнитов служб и управление ими

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

Шаг 1: Создайте файл юнита

Создайте новый файл в /etc/systemd/system/ с расширением .service. Для нашего примера создадим /etc/systemd/system/my-app.service.

[Unit]
Description=Служба моего пользовательского приложения
After=network.target

[Service]
Type=simple
ExecStart=/opt/my-app/bin/run-app --port 8080
User=appuser
Group=appgroup
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Важные соображения:

  • Убедитесь, что команда ExecStart указывает на исполняемый скрипт или двоичный файл, который доступен и имеет права на выполнение.
  • Создайте указанных User и Group, если они не существуют (sudo useradd -r -s /bin/false appuser, sudo groupadd appgroup, sudo usermod -a -G appgroup appuser).
  • Убедитесь, что приложение можно правильно запускать и останавливать с помощью указанных команд.

Прежде чем помещать команду в ExecStart=, по возможности запустите ее вручную от имени того же пользователя:

sudo -u appuser /opt/my-app/bin/run-app --port 8080

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

Шаг 2: Перезагрузите конфигурацию systemd

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

sudo systemctl daemon-reload

Эта команда сканирует новые или измененные файлы юнитов и обновляет внутреннее состояние systemd.

Шаг 3: Включите и запустите службу

Чтобы запустить службу немедленно и настроить ее запуск при загрузке:

sudo systemctl enable my-app.service  # Создает символические ссылки для запуска при загрузке
sudo systemctl start my-app.service   # Запускает службу сейчас

Шаг 4: Управление службой

Используйте команды systemctl для управления вашей службой:

  • Проверка статуса:

    sudo systemctl status my-app.service
    

    Это покажет, активна ли служба, ее идентификатор процесса, последние записи журнала и многое другое.

  • Остановка службы:

    sudo systemctl stop my-app.service
    
  • Перезапуск службы:

    sudo systemctl restart my-app.service
    
  • Перезагрузка службы (если определен ExecReload=):

    sudo systemctl reload my-app.service
    
  • Отключение службы (предотвращение запуска при загрузке):

    sudo systemctl disable my-app.service
    

Шаг 5: Просмотр журналов с помощью journalctl

Systemd тесно интегрирован с journald для ведения журнала. Вы можете просматривать журналы для вашей службы с помощью journalctl:

  • Просмотр журналов для конкретной службы:

    sudo journalctl -u my-app.service
    
  • Просмотр журналов в реальном времени:

    sudo journalctl -f -u my-app.service
    
  • Просмотр журналов с последней загрузки:

    sudo journalctl -b -u my-app.service
    

Лучшие практики и советы

  • Используйте Type=notify для современных приложений: Если ваше приложение поддерживает это, Type=notify обеспечивает лучшую интеграцию с systemd, позволяя ему точно отслеживать готовность службы.
  • Запускайте службы от имени непривилегированных пользователей: Всегда указывайте User= и Group= в разделе [Service], чтобы минимизировать риски безопасности.
  • Тщательно определяйте зависимости: Используйте After=, Requires= и Wants=, чтобы гарантировать, что службы запускаются в правильном порядке и выполняются критические зависимости.
  • Используйте Restart=: Настройте соответствующие политики перезапуска, чтобы обеспечить доступность службы.
  • Делайте файлы юнитов простыми: Для сложных последовательностей запуска рассмотрите возможность использования скриптов-оберток, вызываемых ExecStart=, вместо сложных команд непосредственно в файле юнита.
  • Используйте systemctl cat <unit>: Для просмотра полного содержимого файла юнита так, как его видит systemd, включая любые переопределения.
  • Используйте systemctl edit <unit>: Эта команда открывает редактор для создания файла переопределения для существующего юнита, что является более чистым способом изменения файлов юнитов по умолчанию, чем их прямое редактирование.

Безопасное редактирование существующих юнитов

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

sudo systemctl edit nginx.service

Это создает drop-in в /etc/systemd/system/nginx.service.d/. Например, чтобы добавить политику перезапуска:

[Service]
Restart=on-failure
RestartSec=5s

Некоторые директивы можно указывать несколько раз. Другие необходимо очищать перед заменой. ExecStart= — классический пример:

[Service]
ExecStart=
ExecStart=/usr/local/bin/my-nginx-wrapper

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

После любого изменения юнита или drop-in используйте тот же цикл проверки:

sudo systemctl daemon-reload
systemctl cat my-app.service
sudo systemctl restart my-app.service
journalctl -u my-app.service -n 50 --no-pager

Файлы юнитов не сложны, как только вы разделите три задачи: [Unit] описывает взаимосвязи, [Service] описывает поведение процесса, а [Install] описывает включение. Большинство отладок в реальном мире сводится к выяснению того, какая из этих задач была настроена с неверным предположением.

Пошаговый разбор реалистичного файла службы

Вот небольшой, но реалистичный файл службы для Python веб-приложения:

[Unit]
Description=API инвентаризации
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=inventory
Group=inventory
WorkingDirectory=/srv/inventory-api
EnvironmentFile=/etc/inventory-api/env
ExecStart=/srv/inventory-api/.venv/bin/gunicorn app:app --bind 127.0.0.1:9000
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

В этом файле есть несколько неявных решений. Служба работает от имени inventory, а не root. Команда использует абсолютный путь к gunicorn из виртуального окружения, поэтому она не зависит от PATH интерактивной оболочки. Приложение привязывается к localhost, потому что обратный прокси-сервер будет предоставлять его публично. Файл окружения находится вне юнита, чтобы развертывание могло обновлять конфигурацию без перезаписи метаданных службы, принадлежащих пакету.

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

Если эта служба выйдет из строя, первые проверки предсказуемы:

systemctl status inventory-api.service
journalctl -u inventory-api.service -b --no-pager
systemctl cat inventory-api.service
sudo -u inventory /srv/inventory-api/.venv/bin/gunicorn app:app --bind 127.0.0.1:9000

Последняя команда не предназначена для работы в продакшене. Это диагностическая проверка, которая спрашивает: "Может ли настроенный пользователь вообще выполнить эту команду?" Если он не может импортировать приложение, прочитать файл окружения или записать в свой каталог журналов, systemd это за вас не исправит.

Директивы ресурсов и безопасности, которые вы будете часто видеть

Многие продакшен-юниты включают hardening или контроль ресурсов. Несколько распространенных примеров:

[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
MemoryMax=512M
CPUQuota=80%

Эти директивы могут быть очень полезными, но они также могут нарушить предположения. PrivateTmp=true предоставляет службе частный /tmp, поэтому другой процесс может не увидеть файлы, которые она туда записывает. ProtectHome=true может заблокировать доступ к /home, /root и /run/user. ProtectSystem=full делает большую часть системы доступной только для чтения с точки зрения службы. Если приложение внезапно не может записывать туда, куда раньше, проверьте настройки hardening, прежде чем обвинять приложение.

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

Самые полезные команды отладки

Держите их под рукой при работе с юнитами служб:

systemctl status my-app.service
systemctl cat my-app.service
systemctl show my-app.service
systemd-analyze verify /etc/systemd/system/my-app.service
journalctl -u my-app.service -b --no-pager

systemctl show многословен, но он показывает свойства, которые systemd вычислил после разбора юнита. Это может выявить неожиданное значение, унаследованное от значения по умолчанию, drop-in или директивы сброса. systemd-analyze verify выявляет некоторые синтаксические ошибки и ошибки зависимостей до того, как вы перезапустите службу. Это не замена тестированию приложения, но он выявляет достаточно ошибок, чтобы его стоило запускать.