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 по умолчанию, ссылка на один из двух выше