[Перевод] systemd: как писать юниты с элегантной перезагрузкой
Разработка системы с элегантным завершением работы может оказаться той ещё пляской с бубном. В идеальном мире каждый сервис управлялся бы юнитом systemd
. ExecStart
запускала бы процесс, обрабатывающий SIGTERM
, а ExecStop
оповещало бы процесс и осуществляло блокировку, которая бы корректно завершала процесс и его ресурсы.
Однако многие программы завершаются некорректно, а то и вовсе сбивают все настройки при закрытии. В этой статье мы рассмотрим поведение systemd
при завершении работы и методы написания юнитов systemd
для выборочной очистки (custom cleanup) перед закрытием. Подробности — к старту нашего курса по DevOps.
systemd
Как и система init
, systemd
наряду со многими другими задачами управляет сервисами — от запуска до завершения работы. При загрузке она запускается первой, а при закрытии останавливается последней. В отличие от более ранних последовательных скриптов (sequential scripts) сервисы systemd
ориентированы на юниты systemd
. Их отношения построены на зависимости и упорядочивании. Это позволяет запускать (или закрывать) многие сервисы параллельно. Всё это важно для статьи в дальнейшем:
- Сервисы запускаются (и закрываются) параллельно, если не предусмотрено иное.
- Процессы завершаются через
SIGTERM
илиSIGKILL
по истечении времени ожидания, если не настроено иначе. - При выключении сервиса с зависимостью от упорядочивания (ordering dependency) останавливаются в порядке, обратном запуску.
Завершение работы
Что происходит при завершении работы? Несколько подчинённых команд systemctl
(subcommands, указаны ниже) завершают работу системы, при этом они активируют специальные юниты systemd
: reboot.target
, poweroff.target
и halt.target
.
systemctl halt # Завершение и остановка работы системы
systemctl poweroff # Завершение работы системы и отключение питания
systemctl reboot # Завершение работы системы и перезагрузка
Команда Requires
этих целевых юнитов запрашивает извлечение таких зависимостей, как systemd-reboot.service
, systemd-poweroff.service
, и systemd-halt.service
(соответственно), а те в обратном порядке запрашивают shutdown.target
.
# reboot.target
[Unit]
Description=System Reboot
Documentation=man:systemd.special(7)
DefaultDependencies=no
Requires=systemd-reboot.service
After=systemd-reboot.service
AllowIsolate=yes
JobTimeoutSec=30min
JobTimeoutAction=reboot-force
[Install]
Alias=ctrl-alt-del.target
sudo systemctl list-dependencies --all --recursive reboot.target
reboot.target
○ └─systemd-reboot.service
● ├─system.slice
● │ └─-.slice
○ ├─final.target
○ ├─shutdown.target
○ └─umount.target
Для всех юнитов и областей действия (scopes) по умолчанию установлено DefaultDependencies=yes
, таким образом, для них в явном виде добавлены выражения Before=shutdown.target и Conflicts=shutdown.target. Conflicts запускает операцию shutdown.target
и останавливает конфликтующие юниты. Запуск shutdown.target
параллельно останавливает все конфликтующие юниты (если не предусмотрено иное).
При прекращении работы юниты systemd
должны корректно завершить запущенные процессы, освободить ресурсы и дождаться завершения работы. Система распределения нагрузки (load balancer) может перестать принимать новые соединения и отключить свою конечную точку готовности (readiness endpoint). База данных может сбрасывать данные на диск, а агент может сообщить кластеру о своём выходе из группы. Любые процессы, которые продолжают работать после выполнения ExecStop
завершаются некорректно (то есть принудительно) командой SIGKILL
в systemd (при отсутствии иных настроек).
Многие программы завершаются некорректно, а то и вовсе сбивают все свои настройки, отклоняются от модели systemd
. Некоторые задачи по завершению работы требуют согласования с кластерной системой. Инструменты типа systemd-analyze при отключении не помогают. А многие сервисы могут даже не являться вашим программным обеспечением. В этих случаях могут помочь действия юнитов по очистке на ранних стадиях выключения (early shutdown units). Рассмотрим некоторые стратегии написания юнитов systemd
, которые выполняют заданные пользователем действия по очистке перед завершением работы.
Скрипт Cleanup
Начнём с простого скрипта cleanup
, который симулирует задачу по очистке. Команда echo
записывает сообщения, цикл показывает прогресс, а sleep
гарантирует, что задача выполняется достаточно долго, чтобы наверняка дождаться последующих действий. Заметим, что этот скрипт не зависит от сети, контейнеров и других системных компонентов:
#!/bin/bash
echo "cleaning..."
for i in {1..3}; do
sleep 5s
echo "waiting..."
done
echo "done"
Поместим этот скрипт в каталог /usr/local/bin/cleanup
и сделаем его исполняемым. Если позднее вы увидите ошибку systemd 203/EXEC
, вернитесь к этому разделу.
Не помещайте этот скрипт в домашний каталог. Политика SELinux по умолчанию не даёт устройствам systemd
выполнять «домашние» скрипты. И это корректно.
Скрипт ExecStart
Теперь рассмотрим блок systemd oneshot, который запускает cleanup
как скрипт ExecStart
. Это будет наша первая и, как мы скоро увидим, ошибочная попытка.
# /etc/systemd/system/clean.service
[Unit]
Description=Clean on shutdown
Before=shutdown.target # implicit
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/cleanup
[Install]
WantedBy=shutdown.target
Этот юнит создаёт зависимость shutdown.target
от clean.service
, использует Before=shutdown.target
при отдаче команды clean.service
перед shutdown.target
, использует Type=oneshot, чтобы сервис считался запущенным при выходе из скрипта (чтобы отложить выключение компьютера до завершения очистки). RemainAfterExit
оставляет сервис активным даже после завершения скрипта.
По умолчанию юниты systemd
(Type=simple
) считаются запущенными одновременно с процессом. Это, к примеру, позволяет последующим юнитам начать работу немедленно.
Активируем clean.service
, systemctl reboot
, потом посмотрим журнал истории операций…
Sep 28 23:55:01 ip-10-0-13-150 systemd[1]: Starting clean.service - Clean on shutdown...
Sep 28 23:55:01 ip-10-0-13-150 cleanup[6796]: cleaning...
-- Boot 8e4734a82c754e549c9a9292ca5988fb --
Это неверный подход. Мы не видим даже истории. Сервис очистки может запуститься, но это не отсрочит завершение работы. У нас shutdown.target
вступает в конфликт со всеми юнитами, поэтому clean.service
прерывается до завершения. Вы могли бы добавить DefaultDependencies=no
, но, как мы может прочесть в systemd.unit man pages, это тоже не отсрочит завершение работы: «Given two units with any ordering dependency between them, if one unit is shut down and the other is started up, the shutdown is ordered before the start-up» («При любой зависимости между двумя юнитами, один из которых начинает свою работу, а другой завершает её, команда завершения работы будет выполняться раньше»).
Скрипт ExecStop
Теперь рассмотрим юнит oneshot в systemd
, который запускает очистку скриптом ExecStop
.
# /etc/systemd/system/clean.service
[Unit]
Description=Clean on shutdown
After=multi-user.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/true
ExecStop=/usr/local/bin/cleanup
[Install]
WantedBy=multi-user.target
Этот юнит извлекается multi-user.target
, но в процессе запуска получает команду на выполнение After=multi-user.target
довольно поздно. Поскольку командно управляемые (ordered) юниты закрываются в обратном запуску порядке, юнит clean
должен начать закрываться раньше других юнитов.
Активируем и запускаем clean.service
. Подтверждаем, что он active
(хотя мы и вышли из него).
● .service - Clean on shutdown
Loaded: loaded (/etc/systemd/system/clean.service; enabled; vendor preset: disabled)
Active: active (exited) since Thu 2022-09-29 20:21:17 UTC; 2min 49s ago
Process: 1383 ExecStart=/bin/true (code=exited, status=0/SUCCESS)
Main PID: 1383 (code=exited, status=0/SUCCESS)
CPU: 2ms
Sep 29 20:21:17 ip-10-0-13-150 systemd[1]: Starting clean.service - Clean on shutdown...
Sep 29 20:21:17 ip-10-0-13-150 systemd[1]: Finished clean.service - Clean on shutdown.
Обратите внимание на Boot ID в журнале операций, затем запустите systemctl reboot
:
...
-- Boot 0e40d519972b4cd7bc09374b3072788d --
Sep 28 20:18:24 ip-10-0-13-150 systemd[1]: Starting clean.service - Clean on shutdown...
Sep 28 20:18:24 ip-10-0-13-150 systemd[1]: Finished clean.service - Clean on shutdown.
Проверим журнал после перезагрузки. Когда была запущена systemctl reboot
, я указал пометкой