[Перевод] 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, я указал пометкой

© Habrahabr.ru