Автоматизация        03 сентября 2026        58         0

Сервис в systemd: unit-файл, автоперезапуск, журнал

Бот, сборщик данных или торговый робот обычно начинают жизнь одинаково: скрипт запущен руками в сессии tmux или через nohup. Работает это до первого события из короткого списка: сервер перезагрузился после обновления ядра, скрипт упал ночью из-за сетевой ошибки, кончилось место под файл nohup.out. Во всех трёх случаях вы узнаёте о проблеме от пользователей или по дыре в данных.

systemd решает все три задачи одним текстовым файлом на двадцать строк. Он запускает программу при старте сервера, поднимает её после сбоя и собирает вывод в журнал с отметками времени. Ниже полный пример для Ubuntu Server 26.04 LTS (systemd 259), от пользователя до разбора ошибок. Те же директивы работают в Debian, РЕД ОС и других системах с systemd.

Этим механизмом пользуются и готовые программы. Установщик Ollama, например, создаёт системного пользователя ollama и кладёт файл /etc/systemd/system/ollama.service. Docker после установки тоже работает как служба docker.service. Свой скрипт заслуживает того же обращения.

Подготовка: пользователь, каталоги, файл с переменными

Сервис в systemd: unit-файл, автоперезапуск, журналВозьмём условный сборщик котировок: программа на Python в каталоге /opt/collector, виртуальное окружение в /opt/collector/venv, запуск файлом main.py. Токены и адреса она читает из переменных окружения, а накопленные данные пишет в свой рабочий каталог.

Службе нужен собственный пользователь без права входа в систему. Если в программе найдётся уязвимость, взломщик получит права этого пользователя, а не root и не ваши.

sudo useradd --system --home-dir /opt/collector --shell /usr/sbin/nologin collector
sudo chown -R collector:collector /opt/collector

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

sudo mkdir -p /etc/collector
sudo nano /etc/collector/collector.env
sudo chmod 600 /etc/collector/collector.env

Формат файла простой: по одной паре ИМЯ=значение в строке, без слова export и без пробелов вокруг знака равенства.

API_TOKEN=YOUR_TOKEN
DB_DSN=postgresql://collector@localhost/quotes

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

Unit-файл: что означает каждая строка

Описания своих служб кладут в /etc/systemd/system/. Каталог /usr/lib/systemd/system/ принадлежит пакетам, и править файлы там не нужно: обновление пакета вернёт всё назад.

sudo nano /etc/systemd/system/collector.service
[Unit]
Description=Quotes collector
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Type=simple
User=collector
Group=collector
WorkingDirectory=/opt/collector
EnvironmentFile=/etc/collector/collector.env
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/collector/venv/bin/python /opt/collector/main.py
Restart=on-failure
RestartSec=10
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
StateDirectory=collector

[Install]
WantedBy=multi-user.target

Секция [Unit] описывает место службы среди остальных. Пара Wants и After с network-online.target означает «запускать после того, как сеть поднята». Без неё сборщик на старте сервера может обратиться к API раньше, чем появится маршрут, и упасть. Две строки с StartLimit задают предел: не больше пяти запусков за 300 секунд.

В секции [Service] главное - ExecStart. Путь должен быть абсолютным, systemd не ищет программы по PATH так, как это делает оболочка. Указан интерпретатор из виртуального окружения, поэтому активировать окружение не требуется. Строка Environment=PYTHONUNBUFFERED=1 отключает буферизацию вывода Python. Без неё сообщения print появляются в журнале пачками с задержкой в минуты, а при аварийном завершении последние строки теряются вовсе.

Короткий пример: изменить одну настройку чужой службы, не трогая её файл, можно командой sudo systemctl edit имя.service. Она создаёт файл-дополнение в /etc/systemd/system/имя.service.d/, который переживает обновление пакета. Так, например, для Ollama задают переменную OLLAMA_HOST. Источник: документация systemd (systemd.unit), руководство по установке Ollama.

Type=simple подходит для программы, которая работает на переднем плане и не уходит в фон. Таких большинство. Если ваша программа сама «демонизируется» через fork, либо отключите это поведение её ключом, либо используйте Type=forking с PIDFile.

StateDirectory=collector создаёт каталог /var/lib/collector, принадлежащий пользователю службы. Он нужен из-за следующих строк. ProtectSystem=strict делает всю файловую систему для этой службы доступной только на чтение, ProtectHome=true скрывает домашние каталоги, PrivateTmp=true даёт собственный /tmp. Писать программа сможет только в свой StateDirectory. Если данные лежат в другом месте, добавьте строку ReadWritePaths=/путь.

Секция [Install] отвечает за автозапуск: WantedBy=multi-user.target означает «включать при обычной загрузке сервера».

Запуск, проверка и чтение журнала

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

sudo systemd-analyze verify /etc/systemd/system/collector.service

Проверка ловит неверные значения директив. Если написать, например, Restart=sometimes, она ответит строкой Failed to parse service restart specifier, ignoring: sometimes. Обратите внимание на слово ignoring: systemd не откажется запускать службу с такой ошибкой, он молча пропустит строку, и перезапуска после сбоя просто не будет.

Теперь запуск:

sudo systemctl daemon-reload
sudo systemctl enable --now collector.service
systemctl status collector.service

daemon-reload заставляет systemd перечитать файлы описаний. Его нужно повторять после каждой правки unit-файла, иначе в памяти останется старая версия. enable --now включает автозапуск и сразу стартует службу. В выводе status ищите строку Active: active (running) и ниже неё последние строки вывода программы.

Журнал службы читается через journalctl:

journalctl -u collector.service -f
journalctl -u collector.service --since "1 hour ago" -p err

Первая команда показывает вывод в реальном времени, вторая - только ошибки за последний час. Остальные полезные сочетания ключей собраны в шпаргалке по journalctl и systemctl.

Перезапуск стоит проверить руками, не дожидаясь настоящего сбоя. Узнайте номер процесса из status и завершите его сигналом, имитирующим аварию:

sudo kill -9 $(systemctl show -p MainPID --value collector.service)
sleep 12
systemctl status collector.service

Через десять секунд, заданных в RestartSec, служба должна снова быть в состоянии active (running) с новым номером процесса.

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

Restart=on-failure поднимает службу, если процесс завершился с ненулевым кодом, был убит сигналом или превысил тайм-аут. Штатный выход с кодом 0 перезапуска не вызывает. Для программы, которая должна работать всегда, а код возврата у неё ненадёжен, ставят Restart=always.

Таблица 1. Варианты директивы Restart

Значение Перезапуск при коде 0 При ненулевом коде При завершении сигналом
no нет нет нет
on-failure нет да да
always да да да

Автоперезапуск лечит случайные сбои и маскирует постоянные. Если программа падает сразу после старта из-за ошибки в коде или неверного токена, она будет падать снова и снова. Здесь срабатывает предел из секции [Unit]: после пятого падения за пять минут systemd прекращает попытки, и служба переходит в состояние failed с причиной start-limit-hit. Это защита от бесконечного цикла, который забивает журнал и грузит процессор. После исправления причины счётчик сбрасывают вручную:

sudo systemctl reset-failed collector.service
sudo systemctl start collector.service

Отсюда следует вывод для эксплуатации: автоперезапуск не избавляет от наблюдения. Служба, которая за ночь перезапустилась сорок раз, формально «работает», но данные собирает с дырами. Число перезапусков показывает команда systemctl show -p NRestarts collector.service, и на это значение стоит повесить оповещение.

Вторая особенность касается самой программы. При остановке systemd посылает процессу сигнал SIGTERM и ждёт 90 секунд, после чего убивает его сигналом SIGKILL. Если ваш сборщик держит открытую транзакцию или недописанный файл, обработайте SIGTERM в коде и завершайте работу аккуратно. Иначе каждый systemctl restart будет оставлять за собой испорченные данные.

Ошибки, которые встречаются у всех

Ошибки, которые встречаются у всехКогда служба не стартует, причина почти всегда видна в строке status с кодом вида status=203/EXEC. Коды стоит знать в лицо.

Таблица 2. Частые коды ошибок запуска службы

Код Что случилось Что проверить
203/EXEC systemd не смог запустить программу путь в ExecStart, бит исполнения, первую строку скрипта
217/USER пользователь не найден имя в User=, создан ли пользователь
200/CHDIR не удалось перейти в каталог путь в WorkingDirectory, права на него
226/NAMESPACE не удалось собрать изолированное окружение пути в ReadWritePaths, существуют ли они

Ещё две ситуации кодом не обозначены. Программа стартует и тут же падает с Permission denied или Read-only file system при записи: значит, она пишет вне StateDirectory, и сработал ProtectSystem=strict. Либо перенесите данные в /var/lib/collector, либо добавьте путь в ReadWritePaths. И вторая: в терминале скрипт работает, а службой нет. Почти всегда дело в окружении. У службы нет ваших переменных из ~/.bashrc, нет вашего PATH и другой текущий каталог. Всё, от чего зависит программа, должно быть прописано в unit-файле или в файле переменных.

Заметка из эксплуатации: журнал journald по умолчанию ограничен долей диска, а не сроком хранения. Сколько он занимает сейчас, показывает journalctl --disk-usage. Жёсткий предел задаётся параметром SystemMaxUse в /etc/systemd/journald.conf. Источник: документация systemd (journald.conf).

Насколько служба изолирована, оценивает встроенная проверка systemd-analyze security collector.service. Она выводит список из нескольких десятков параметров с пометками и общий балл. Гнаться за идеальным баллом для своего скрипта не нужно. Четыре директивы из примера закрывают главное: служба не получит новых привилегий, не изменит систему и не прочитает чужие домашние каталоги.

Есть случаи, когда служба systemd - не лучший выбор. Задаче, которая запускается раз в сутки и завершается, нужен таймер, а не постоянно работающий процесс. Программе в контейнере перезапуск обеспечивает Docker своей политикой restart, и оборачивать docker run в unit-файл обычно лишнее. А если на сервере десяток взаимосвязанных служб, порядок их запуска лучше описывать зависимостями Requires и After между ними, чем надеяться на паузы в RestartSec.

Проверка команд:

  • Запущено в тестовой среде (systemd 255): unit-файл из статьи проверен командой systemd-analyze verify без замечаний; строка об ошибке Failed to parse service restart specifier, ignoring: sometimes - настоящий вывод этой команды.
  • Сверено с документацией (запуск невозможен без systemd как PID 1): systemctl enable --now, status, kill и перезапуск, reset-failed, show -p NRestarts, systemd-analyze security.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

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