Перейти к содержанию

systemd: юнит сервиса

Юнит сервиса - Это файл *.service, по которому systemd запускает процесс, перезапускает его при падении и ограничивает в правах. Свои юниты кладут в /etc/systemd/system/. Как systemd устроен целиком - в systemd.

# /etc/systemd/system/myapp.service - минимальный рабочий юнит
[Unit]
Description=My app
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=myuser
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/java -jar /opt/myapp/app.jar
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo vim /etc/systemd/system/myapp.service
sudo systemctl daemon-reload              # перечитать юниты после правки
sudo systemctl enable --now myapp.service # включить в автозапуск и сразу запустить
systemctl status myapp                    # состояние и последние строки лога
journalctl -u myapp -f                    # лог сервиса в реальном времени
systemd-analyze security myapp            # оценка изоляции сервиса по директивам защиты

Warning

Комментарий в unit-файле - только отдельной строкой, которая начинается с # или ;. В строке After=network.target # стартовать после systemd прочитает # и всё после него как часть значения. В исходной версии справочник был одним юнитом с комментариями в конце строк - как настоящий файл он бы не заработал.


[Unit]

Описание юнита и его связи с другими юнитами.

Директива Что делает
Description= Описание сервиса
Documentation=man:nginx(8) Ссылки на man, URL или документацию
After=network.target Стартовать после указанных юнитов
Before=nginx.service Стартовать раньше указанных юнитов
Requires=postgresql.service Сильная зависимость: не поднялась зависимость - не поднимется и сервис
Wants=network-online.target Мягкая зависимость: systemd попробует поднять, но её падение сервис не остановит
BindsTo=dev-sda1.device Сильная привязка, обычно к устройству или точке монтирования: исчезло - сервис останавливается
PartOf=myapp.target Основной юнит перезапускается или останавливается - этот следует за ним
Conflicts=shutdown.target Взаимоисключающие юниты: запуск одного останавливает другой
ConditionPathExists=/etc/myapp.conf Условие запуска: путь существует
ConditionFileNotEmpty=/etc/myapp.conf Условие: файл не пустой
ConditionDirectoryNotEmpty=/var/lib/myapp Условие: каталог не пустой
AssertPathExists=/data Как Condition, но невыполненное условие - ошибка запуска
StartLimitIntervalSec=60, StartLimitBurst=3 Не больше 3 запусков за 60 секунд, дальше юнит считается упавшим
  • порядок и зависимость - разные вещи. After задаёт только порядок, а Requires и Wants - нужно ли поднимать другой юнит. Поэтому их обычно ставят парой: Wants=X + After=X
  • Condition и Assert. Невыполненный Condition* тихо пропускает запуск, юнит не считается упавшим. Невыполненный Assert* - ошибка

[Service]

Тип запуска

Type= - когда systemd считает сервис запущенным.

Тип Когда сервис считается запущенным
simple Сразу после старта ExecStart. Тип по умолчанию
exec После успешного execve(): ошибка «бинарник не найден» станет ошибкой запуска
forking Старый режим демонов: процесс форкается и уходит в фон
oneshot Выполнить команду и выйти, сервис запущен после её завершения
notify Сервис сам сообщает systemd о готовности через sd_notify
notify-reload notify + отдельная логика reload
dbus Сервис зарегистрировал имя на D-Bus
idle Как simple, но выполнение откладывается, пока не разгрузится очередь задач

Как выбирать:

  • обычный процесс на переднем плане - simple или exec
  • legacy-демон, который сам уходит в фон - forking
  • init-скрипт или разовая команда - oneshot
  • приложение умеет sd_notify - notify

Запуск, остановка, перезапуск

Директива Что делает
ExecStart=/usr/bin/myapp --config /etc/myapp.conf Главная команда запуска
ExecStartPre=/usr/bin/test -f /etc/myapp.conf Перед стартом
ExecStartPost=/usr/bin/logger started После старта
ExecReload=/bin/kill -HUP $MAINPID Что делать при systemctl reload
ExecStop=/usr/bin/myapp --stop Как останавливать
ExecStopPost=/usr/bin/rm -f /run/myapp.pid После остановки
Restart=on-failure Когда перезапускать (варианты ниже)
RestartSec=5 Пауза перед перезапуском
RestartSteps=, RestartMaxDelaySec= Пауза растёт от RestartSec до RestartMaxDelaySec за указанное число шагов
PIDFile=/run/myapp.pid PID-файл. Обычно нужен только для Type=forking, чтобы узнать главный PID

Restart= - при каком завершении перезапускать:

Значение Перезапуск
no Никогда, по умолчанию
on-success Только при чистом выходе (код 0)
on-failure При ненулевом коде, сигнале, таймауте, срабатывании watchdog
on-abnormal При сигнале, таймауте, watchdog, но не при ненулевом коде
on-watchdog Только при срабатывании watchdog
on-abort Только при сигнале, который не обработан
always Всегда

Таймауты

Директива Что делает
TimeoutStartSec=30 Сколько ждать старта
TimeoutStopSec=20 Сколько ждать остановки
TimeoutAbortSec=15 Сколько ждать при аварийной остановке (watchdog)
RuntimeMaxSec=1h Максимальное время жизни сервиса
RuntimeRandomizedExtraSec=30 Случайная добавка к RuntimeMaxSec, чтобы копии не останавливались одновременно
WatchdogSec=30 Интервал watchdog для сервисов, которые умеют общаться с systemd

Пользователь, окружение, журнал

Директива Что делает
User=myuser, Group=mygroup От чьего имени запускать
SupplementaryGroups=docker Дополнительные группы
UMask=0027 Маска прав для создаваемых файлов
WorkingDirectory=/opt/myapp Текущий каталог процесса
Environment=APP_ENV=prod Переменная окружения, директив может быть несколько
EnvironmentFile=/etc/myapp.env Читать переменные из файла
PassEnvironment=HTTP_PROXY HTTPS_PROXY Пробросить переменные из окружения самого systemd
UnsetEnvironment=DEBUG Удалить переменные из окружения процесса
StandardOutput=journal, StandardError=journal stdout и stderr - в журнал
SyslogIdentifier=myapp Имя сервиса в журнале
LogLevelMax=info Отбрасывать сообщения ниже уровня

Завершение процессов

Директива Что делает
KillMode=control-group Как убивать: все процессы группы сервиса (по умолчанию), только главный и т.д.
KillSignal=SIGTERM Основной сигнал остановки
FinalKillSignal=SIGKILL Если не завершился вовремя
SendSIGKILL=yes Слать ли SIGKILL в конце
SendSIGHUP=no Слать ли SIGHUP вместе с основным сигналом

Изоляция и права

Минимизация привилегий:

Директива Что делает
NoNewPrivileges=yes Процесс не может повысить права, например через setuid-бинарник
PrivateTmp=yes Свой изолированный /tmp
PrivateDevices=yes Нет доступа к реальным устройствам в /dev
ProtectSystem=full /usr, /boot, /etc только для чтения. strict - вся ФС, кроме разрешённых путей
ProtectHome=yes Нет доступа к /home, /root, /run/user
ProtectKernelTunables=yes /proc/sys, /sys только для чтения
ProtectKernelModules=yes Запрет загрузки модулей ядра
ProtectControlGroups=yes Файловая система cgroup только для чтения
RestrictSUIDSGID=yes Запрет создавать setuid и setgid файлы
LockPersonality=yes Запрет менять execution domain
MemoryDenyWriteExecute=yes Запрет памяти, которая одновременно доступна на запись и исполнение
RestrictRealtime=yes Запрет realtime-планирования

Warning

MemoryDenyWriteExecute=yes ломает JIT-компиляцию: JVM, Node.js, PHP с JIT на нём падают или работают без JIT. Для Java-сервисов эту директиву не ставят.

Capabilities и системные вызовы:

Директива Что делает
CapabilityBoundingSet=CAP_NET_BIND_SERVICE Максимальный набор capabilities процесса
AmbientCapabilities=CAP_NET_BIND_SERVICE Выдать capability процессу не от root, например слушать порт < 1024
SystemCallFilter=@system-service Разрешить только системные вызовы из набора для обычных сервисов
SystemCallArchitectures=native Только системные вызовы родной архитектуры
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX Разрешённые типы сокетов

Доступ к файловой системе:

Директива Что делает
ReadOnlyPaths=/etc Только чтение
ReadWritePaths=/var/lib/myapp /var/log/myapp Разрешить запись, в том числе при ProtectSystem=strict
InaccessiblePaths=/home Скрыть путь целиком
TemporaryFileSystem=/tmp:size=100M Пустая tmpfs вместо каталога
BindPaths=/srv/data:/data Примонтировать каталог хоста в другой путь для процесса

Ресурсы

Директива Что делает
MemoryMax=500M Жёсткий лимит памяти: выше - OOM-kill внутри сервиса
MemoryHigh=300M Мягкий лимит: выше - память активно отбирается, процесс тормозит
CPUQuota=50% Не больше половины одного ядра. 200% - два ядра
CPUWeight=200 Относительный вес CPU при конкуренции, по умолчанию 100
TasksMax=512 Максимум процессов и потоков
IOWeight=200 Относительный вес дискового ввода-вывода, по умолчанию 100

[Install]

Что происходит при systemctl enable.

Директива Что делает
WantedBy=multi-user.target При enable сервис становится мягкой зависимостью target
RequiredBy=some.target То же, но сильная зависимость
Also=myapp-worker.service Вместе с этим юнитом включать и выключать дополнительные
Alias=myapp.service Альтернативное имя юнита

Target для WantedBy:

  • multi-user.target - обычный серверный режим
  • graphical.target - графический режим
  • default.target - target по умолчанию, ссылка на один из двух выше