Как эффективно писать и управлять пользовательскими файлами модулей systemd
Освойте искусство управления службами Linux с помощью этого подробного руководства по пользовательским файлам модулей systemd. Научитесь создавать, настраивать и устранять неполадки файлов `.service`, используя ключевые директивы, такие как `ExecStart`, `WantedBy` и `Type`. В статье представлены пошаговые инструкции и практические примеры, которые помогут вам стандартизировать запуск приложений, обеспечить надежную работу и легко интегрировать пользовательские процессы в среду Linux. Незаменимо для разработчиков и администраторов, стремящихся к надежному управлению службами.
Как эффективно писать и управлять пользовательскими файлами модулей systemd
Пользовательские файлы модулей systemd превращают команду, работающую в терминале, в службу, которую операционная система может запускать, останавливать, перезапускать, вести журнал и контролировать. Разница важна. Команда в оболочке наследует ваше окружение и завершается при закрытии сеанса. Служба имеет явного пользователя, рабочий каталог, политику перезапуска, зависимости, ограничения ресурсов и журналы.
Это практический путь, который я использую для небольших внутренних API, рабочих процессов, вспомогательных скриптов и разовых демонов: написать простейший корректный модуль, запустить его от выделенного пользователя, направить журналы в systemd-journal и добавлять расширенные директивы только тогда, когда возникает реальная необходимость.
Понимание файлов модулей systemd
Systemd управляет различными системными ресурсами, называемыми модулями, которые определяются файлами конфигурации. Эти модули включают службы (.service), точки монтирования (.mount), устройства (.device), сокеты (.socket) и другие. Для управления приложениями и фоновыми процессами наиболее распространенным и актуальным является тип модуля .service.
Файлы модулей systemd — это обычные текстовые файлы, обычно хранящиеся в определенных каталогах. Основные расположения в порядке приоритета:
/etc/systemd/system/: Рекомендуемое место для пользовательских файлов модулей и переопределений, так как они имеют приоритет над системными настройками по умолчанию и сохраняются при обновлениях системы./run/systemd/system/: Используется для файлов модулей, созданных во время выполнения./usr/lib/systemd/system/: Содержит файлы модулей, предоставляемые установленными пакетами. Не изменяйте файлы в этом каталоге напрямую.
Размещая пользовательские файлы модулей в /etc/systemd/system/, вы гарантируете, что они будут правильно распознаны и управляться systemd.
Анатомия файла модуля .service
Файл модуля .service systemd структурирован в несколько разделов, каждый из которых обозначается [SectionName] и содержит различные директивы (пары ключ-значение). Три основных раздела для модуля службы: [Unit], [Service] и [Install].
Давайте разберем наиболее важные директивы, которые вы будете использовать:
Раздел [Unit]
Этот раздел содержит общие параметры модуля, его описание и зависимости.
Description: Человекочитаемая строка, описывающая службу. Отображается в выводеsystemctl status.Description=Мое пользовательское веб-приложение на PythonDocumentation: URL, указывающий на документацию службы (необязательно).Documentation=https://example.com/docs/my-appAfter: Указывает, что этот модуль должен запускаться после перечисленных модулей. Это помогает управлять порядком запуска. Для веб-приложений может потребоваться убедиться, что сеть активна.After=network.targetRequires: Сильная зависимость. Если требуемый модуль не запустится, этот модуль не запустится. Если требуемый модуль остановлен, этот модуль также может быть остановлен.Requires=docker.serviceWants: Более слабая зависимость. Если требуемый модуль не работает или не найден, этот модуль все равно попытается запуститься. Обычно это лучший вариант по умолчанию, чемRequires.Wants=syslog.target
Раздел [Service]
Этот раздел определяет параметры выполнения вашей службы, включая то, как она запускается, останавливается и ведет себя.
Type: Определяет тип запуска процесса. Критически важно для того, как systemd отслеживает вашу службу.simple(по умолчанию): КомандаExecStartявляется основным процессом службы. Systemd считает службу запущенной сразу после вызоваExecStart. Ожидается, что процесс будет работать бесконечно в интерактивном режиме.forking: КомандаExecStartпорождает дочерний процесс, а родительский завершается. Systemd считает службу запущенной после завершения родительского процесса. Используйте это, если ваше приложение демонизируется.oneshot: КомандаExecStart— это одноразовый процесс, который завершается после выполнения. Полезно для скриптов, выполняющих задачу и завершающихся (например, скрипт резервного копирования).notify: Аналогичноsimple, но служба сообщает systemd, когда она готова. Требует поддержки приложением уведомлений systemd.idle: КомандаExecStartвыполняется только после завершения всех задач, задерживая выполнение до тех пор, пока система не станет почти бездействующей.
Type=simpleExecStart: Команда для выполнения при запуске службы. Это самая важная директива в этом разделе. Всегда используйте абсолютный путь к вашему исполняемому файлу или скрипту.ExecStart=/usr/bin/python3 /opt/my_app/app.pyExecStop: Команда для выполнения при остановке службы (необязательно). Если не указано, systemd отправляетSIGTERMпроцессам.ExecStop=/usr/bin/pkill -f 'my_app/app.py'ExecReload: Команда для перезагрузки конфигурации службы (необязательно).ExecReload=/bin/kill -HUP $MAINPIDUser: Учетная запись пользователя, от имени которой будут запускаться процессы службы. Важно для безопасности; избегайтеroot, если это абсолютно необходимо.User=myappuserGroup: Учетная запись группы, от имени которой будут запускаться процессы службы.Group=myappgroupWorkingDirectory: Рабочий каталог для выполняемых команд.WorkingDirectory=/opt/my_appRestart: Определяет, когда служба должна автоматически перезапускаться.no(по умолчанию): Никогда не перезапускать.on-success: Перезапускать только в случае чистого завершения службы.on-failure: Перезапускать только в случае завершения службы с ненулевым кодом возврата или завершения по сигналу.always: Всегда перезапускать службу, независимо от статуса завершения.
Restart=on-failureRestartSec: Сколько ждать перед перезапуском службы (например,5sдля 5 секунд).RestartSec=5sEnvironment: Устанавливает переменные окружения для выполняемых команд.Environment="APP_ENV=production" "DEBUG=false"EnvironmentFile: Читает переменные окружения из файла. Каждая строка должна бытьKEY=VALUE.EnvironmentFile=/etc/default/my_appLimitNOFILE: Устанавливает максимальное количество открытых файловых дескрипторов, разрешенное для службы (например,100000). Важно для приложений с высокой степенью параллелизма.LimitNOFILE=65536
Раздел [Install]
Этот раздел определяет, как служба включается для автоматического запуска при загрузке.
WantedBy: Указывает целевой модуль, который "хочет" эту службу. Когда целевой модуль включен, эта служба будет символически связана с его каталогом.wants, что фактически заставляет ее запускаться вместе с целью.multi-user.target: Стандартная цель для большинства серверных служб, указывающая на систему с неграфическим многопользовательским входом.graphical.target: Для служб, требующих графического окружения.
WantedBy=multi-user.targetRequiredBy: АналогичноWantedBy, но более сильная зависимость. Если цель включена, этот модуль также включается, и если этот модуль выходит из строя, цель также выйдет из строя.
Для большинства пользовательских служб, предназначенных для работы в фоновом режиме на сервере, Type=simple и WantedBy=multi-user.target являются правильной отправной точкой. Если приложение уже демонизируется, либо отключите это поведение, либо используйте Type=forking с осторожностью. Процесс, работающий в интерактивном режиме, легче контролировать systemd.
Пошаговое руководство: Создание и управление пользовательской службой systemd
Давайте создадим практический пример: простой HTTP-сервер на Python, который обслуживает файлы из указанного каталога. Мы настроим его как службу systemd.
Шаг 1: Подготовка приложения/скрипта
Сначала создайте скрипт приложения. Для этого примера мы будем использовать простой HTTP-сервер на Python. Создайте каталог для вашего приложения, например /opt/my_app, и поместите в него app.py.
# /opt/my_app/app.py
import http.server
import socketserver
import os
PORT = int(os.environ.get("PORT", 8000))
DIRECTORY = os.environ.get("DIRECTORY", os.getcwd())
class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=DIRECTORY, **kwargs)
print(f"Обслуживание каталога {DIRECTORY} на порту {PORT}")
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print("Сервер запущен.")
httpd.serve_forever()
Создайте каталог и файл:
sudo mkdir -p /opt/my_app
sudo nano /opt/my_app/app.py
(Вставьте код Python)
Убедитесь, что скрипт является исполняемым (необязательно для команды python3, но хорошая практика):
sudo chmod +x /opt/my_app/app.py
Рассмотрите возможность создания выделенного пользователя для вашей службы из соображений безопасности:
sudo useradd --system --no-create-home myappuser
Установите соответствующие права владения для каталога вашего приложения:
sudo chown -R myappuser:myappuser /opt/my_app
Шаг 2: Создание файла модуля
Теперь создайте файл модуля systemd для нашего приложения на Python. Назовем его my_app.service.
sudo nano /etc/systemd/system/my_app.service
Вставьте следующее содержимое:
# /etc/systemd/system/my_app.service
[Unit]
Description=Мой пользовательский HTTP-сервер на Python
Documentation=https://github.com/example/my_app
After=network.target
[Service]
Type=simple
User=myappuser
Group=myappuser
WorkingDirectory=/opt/my_app
Environment="PORT=8080" "DIRECTORY=/var/www/html"
ExecStart=/usr/bin/python3 /opt/my_app/app.py
Restart=on-failure
RestartSec=10s
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Примечание: Мы установили
StandardOutput=journalиStandardError=journal, чтобы направить вывод службы в журнал systemd, что упрощает просмотр журналов с помощьюjournalctl.
Если вашему приложению требуются секреты, не помещайте их непосредственно в файл модуля. Используйте файл окружения с ограничительными правами доступа, менеджер секретов или поддержку учетных данных, специфичную для дистрибутива. Файлы модулей часто доступны для чтения большему количеству людей, чем вы ожидаете.
Шаг 3: Размещение файла модуля
Как указано, мы разместили файл модуля в /etc/systemd/system/. Это место, где должны находиться пользовательские файлы модулей.
Шаг 4: Перезагрузка демона systemd
После создания или изменения файла модуля необходимо сообщить systemd об изменениях. Это делается перезагрузкой демона systemd:
sudo systemctl daemon-reload
Шаг 5: Запуск службы
Теперь вы можете запустить вашу службу:
sudo systemctl start my_app.service
Шаг 6: Проверка статуса службы и журналов
Убедитесь, что ваша служба работает правильно:
systemctl status my_app.service
Пример вывода (сокращен):
● my_app.service - Мой пользовательский HTTP-сервер на Python
Loaded: загружен (/etc/systemd/system/my_app.service; отключен; настройки поставщика: включен)
Active: активен (работает) с Вт 2023-10-26 10:30:00 UTC; 5 сек назад
Docs: https://github.com/example/my_app
Main PID: 12345 (python3)
Tasks: 1 (лимит: 1100)
Memory: 6.5M
CPU: 45ms
CGroup: /system.slice/my_app.service
└─12345 /usr/bin/python3 /opt/my_app/app.py
Окт 26 10:30:00 yourhostname python3[12345]: Обслуживание каталога /var/www/html на порту 8080
Окт 26 10:30:00 yourhostname python3[12345]: Сервер запущен.
Чтобы просмотреть журналы службы, используйте journalctl:
journalctl -u my_app.service -f
Эта команда показывает журналы для my_app.service, а -f (следовать) будет показывать новые журналы в реальном времени.
Вы также можете протестировать сервер из браузера или с помощью curl по адресу http://localhost:8080 (при условии, что /var/www/html существует и содержит некоторые файлы).
Шаг 7: Включение службы для автоматического запуска
Чтобы ваша служба запускалась автоматически при каждой загрузке системы, вам необходимо включить ее:
sudo systemctl enable my_app.service
Эта команда создает символическую ссылку из /etc/systemd/system/multi-user.target.wants/my_app.service в /etc/systemd/system/my_app.service.
Шаг 8: Остановка и отключение службы
Чтобы остановить работающую службу:
sudo systemctl stop my_app.service
Чтобы предотвратить автоматический запуск службы при загрузке (оставляя ее включенной для ручного запуска):
sudo systemctl disable my_app.service
Если вы хотите полностью удалить службу, сначала disable ее, затем stop, и, наконец, удалите файл .service из /etc/systemd/system/ и выполните sudo systemctl daemon-reload.
Шаг 9: Обновление службы
Если вы измените скрипт app.py или файл модуля my_app.service, вам нужно будет обновить systemd и перезапустить службу:
- Отредактируйте
/opt/my_app/app.pyили/etc/systemd/system/my_app.service. - Если вы изменили файл модуля, выполните
sudo systemctl daemon-reload. - Перезапустите службу:
sudo systemctl restart my_app.service.
Более безопасные шаблоны для реальных служб
Модуль, который работает, не всегда является модулем, который вы захотите поддерживать годами. Эти шаблоны предотвращают распространенные ошибки:
- Запуск в интерактивном режиме. Позвольте systemd контролировать основной процесс. Избегайте
nohup,screen,tmux, фонового&или режимов демона приложения внутриExecStart. - Делайте
ExecStartпрямым. Если вам нужны функции оболочки, такие как каналы или подстановка переменных, намеренно вызывайте/bin/sh -c '...'. В противном случае запускайте исполняемый файл напрямую. - Используйте выделенного пользователя. Служба, которой нужно только читать
/opt/my_appи привязываться к непривилегированному порту, не должна работать от имениroot. - Перезагружайте после редактирования модуля.
sudo systemctl daemon-reloadтребуется при изменении файла модуля. - Разделяйте развертывание кода и изменения модуля. Если изменился только код Python, перезапустите службу. Если изменился модуль, сначала перезагрузите systemd.
Устранение неполадок нового модуля
Если служба не работает, начните с:
systemctl status my_app.service
journalctl -u my_app.service -n 100 --no-pager
systemctl cat my_app.service
Обычно распространенные сбои очевидны:
status=203/EXECчасто означает, что путь к исполняемому файлу неверен, файл отсутствует или файл не является исполняемым.Permission deniedобычно означает, что пользователь службы не может прочитать файл, войти в каталог, записать журналы или привязать запрошенный порт.address already in useозначает, что другой процесс владеет портом. Проверьте с помощьюsudo ss -tulpen | grep ':8080'.- Служба, которая запускается вручную, но не работает под управлением systemd, часто зависит от переменных окружения, другого рабочего каталога или файлов в вашем домашнем каталоге.
Вы можете протестировать команду от имени пользователя службы:
sudo -u myappuser /usr/bin/python3 /opt/my_app/app.py
Это не идеальное воспроизведение окружения systemd, но оно позволяет выявить очевидные ошибки приложения до того, как вы начнете разбираться с деталями файла модуля.
Более приближенный к production вариант
Для долго работающей внутренней службы я обычно добавляю несколько защитных ограничений:
[Unit]
Description=Мой пользовательский HTTP-сервер на Python
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=myappuser
Group=myappuser
WorkingDirectory=/opt/my_app
EnvironmentFile=-/etc/my_app/my_app.env
ExecStart=/usr/bin/python3 /opt/my_app/app.py
Restart=on-failure
RestartSec=10s
TimeoutStopSec=30s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/opt/my_app /var/log/my_app
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
ProtectSystem=full делает большую часть системы доступной только для чтения для службы, поэтому добавляйте ReadWritePaths= только для каталогов, в которые приложению действительно нужно писать. Тестируйте ужесточение по одной директиве за раз. Параметры безопасности полезны, но служба, которая не может прочитать свою конфигурацию или записать свои данные, выйдет из строя при запуске.
Лучшие практики и устранение неполадок
- Абсолютные пути: Всегда используйте абсолютные пути для
ExecStart,WorkingDirectoryи любых других путей к файлам в вашем файле модуля. Относительные пути могут привести к неожиданному поведению. - Выделенные пользователи: Запускайте службы от непривилегированных, выделенных учетных записей пользователей (например,
myappuser) для повышения безопасности и ограничения потенциального ущерба в случае компрометации. - Четкое ведение журнала: Используйте
StandardOutput=journalиStandardError=journalдля направления вывода службы в журнал systemd. Используйтеjournalctl -u <имя_службы>для просмотра журналов. - Зависимости: Тщательно продумайте
After,WantsиRequires, чтобы ваша служба запускалась в правильном порядке относительно своих зависимостей (например, сеть, базы данных). - Тестирование изменений: Перед включением службы для запуска при загрузке тщательно протестируйте ее, запуская и останавливая вручную. Проверьте ее статус и журналы.
- Ограничения ресурсов: Используйте директивы, такие как
LimitNOFILE,LimitNPROCиMemoryMax, когда служба имеет известные ограничения или режимы сбоев. - Переменные окружения: Используйте
Environment=илиEnvironmentFile=для значений конфигурации, которые могут меняться или различаться в зависимости от окружения, вместо их жесткого кодирования в файле модуля или скрипте. - Обработка ошибок в скриптах: Убедитесь, что скрипты вашего приложения корректно обрабатывают ошибки. Ненулевой код возврата вызовет
Restart=on-failure.
Предупреждение: Избегайте изменения файлов модулей непосредственно в
/usr/lib/systemd/system/. Любые изменения, скорее всего, будут перезаписаны при обновлении пакетов. Используйте/etc/systemd/system/для пользовательских модулей или переопределений.
Хороший пользовательский модуль скучен в лучшем смысле: команда явная, пользователь непривилегированный, поведение при перезапуске намеренное, а журналы легко найти. Как только это станет надежным, таймеры systemd, активация сокетов и более глубокий контроль cgroup станут естественными следующими шагами.