Разработка системы с элегантным завершением работы может оказаться той ещё пляской с бубном. В идеальном мире каждый сервис управлялся бы юнитом 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, я указал пометкой ????.


...
Sep 28 20:18:24 ip-10-0-13-150 systemd[1]: Finished clean.service - Clean on shutdown.
????
Sep 29 20:20:38 ip-10-0-13-150 systemd[1]: Stopping clean.service - Clean on shutdown...
Sep 29 20:20:38 ip-10-0-13-150 cleanup[367051]: cleaning...
Sep 29 20:20:43 ip-10-0-13-150 cleanup[367077]: waiting...
Sep 29 20:20:48 ip-10-0-13-150 cleanup[367080]: waiting...
Sep 29 20:20:53 ip-10-0-13-150 cleanup[367132]: waiting...
Sep 29 20:20:53 ip-10-0-13-150 cleanup[367134]: done
Sep 29 20:20:53 ip-10-0-13-150 systemd[1]: clean.service: Deactivated successfully.
Sep 29 20:20:53 ip-10-0-13-150 systemd[1]: Stopped clean.service - Clean on shutdown.
-- Boot 8e97b43271024c64b0775c43dc519c5b --
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.

Юнит прекращает работу, но скрипт cleanup в это время работает до победного, что задерживает завершение работы. После этого происходит перезагрузка. При следующей загрузке юнит запускает (/bin/true), после чего его работа завершается.


Всё работает, как и ожидалось, но с одной оговоркой. Работа командно управляемых юнитов завершается в обратном запуску порядке. И вот тут стоит вспомнить, сколько сервисов прекращает работу единовременно. Наш скрипт cleanup не даёт гарантий, что те или иные сервисы будут работать при выполнении в ExecStop. Сам же cleanup работает только потому, что не зависит от других компонентов системы. Если бы он, например, зависел от сети, нам бы пришлось добавить After=network.target, чтобы при завершении работы его работа закончилась раньше, чем перестанут работать сетевые сервисы.


Теперь перейдём к задаче очистки в контейнерах, которая в наши дни становится обычным делом.


Контейнер ExecStop


Рассмотрим блок systemd, который запускает контейнерный процесс в ExecStop перед выключением. В отличие от минималистичного скрипта, которым мы пользовались ранее, запуск этого контейнера требует работы множества системных компонентов. ExecStop запускается одновременно с другими юнитами, прекращающими работу. Успешного выполнения это не сулит.


# /etc/systemd/system/clean.service
[Unit]
Description=Clean on shutdown
After=multi-user.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/true
ExecStopPre=-/usr/bin/podman rm clean
ExecStop=/usr/bin/podman run \
  --name clean \
  --log-driver=k8s-file \
  --rm \
  -v /usr/local/bin:/scripts \
  --stop-timeout=60 \
  --entrypoint /scripts/cleanup \
  docker.io/fedora:36

[Install]
WantedBy=multi-user.target

Активируем и запустим clean.service. При ручной деактивации clean.service ExecStop создаст контейнер, монтирует путь /usr/local/bin и нормально запустит тот же cleanup.


Sep 29 21:36:41 ip-10-0-13-150 systemd[1]: Starting clean.service - Clean on shutdown...
Sep 29 21:36:41 ip-10-0-13-150 systemd[1]: Finished clean.service - Clean on shutdown.
Sep 29 21:37:35 ip-10-0-13-150 systemd[1]: Stopping clean.service - Clean on shutdown...
Sep 29 21:37:35 ip-10-0-13-150 podman[10701]: 2022-09-29 21:37:35.32232541 +0000 UTC m=+0.076846159 container create 2e0a21b085113ad6b5eab83a1f4b85081045727b711c89909dc7d204abb25e61 (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 21:37:35 ip-10-0-13-150 podman[10701]: 2022-09-29 21:37:35.292987568 +0000 UTC m=+0.047508358 image pull  docker.io/fedora:36
Sep 29 21:37:35 ip-10-0-13-150 podman[10701]: 2022-09-29 21:37:35.516155501 +0000 UTC m=+0.270676209 container init 2e0a21b085113ad6b5eab83a1f4b85081045727b711c89909dc7d204abb25e61 (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 21:37:35 ip-10-0-13-150 podman[10701]: 2022-09-29 21:37:35.5276157 +0000 UTC m=+0.282136425 container start 2e0a21b085113ad6b5eab83a1f4b85081045727b711c89909dc7d204abb25e61 (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 21:37:35 ip-10-0-13-150 podman[10701]: 2022-09-29 21:37:35.527985526 +0000 UTC m=+0.282506267 container attach 2e0a21b085113ad6b5eab83a1f4b85081045727b711c89909dc7d204abb25e61 (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 21:37:35 ip-10-0-13-150 podman[10701]: cleaning...
Sep 29 21:37:40 ip-10-0-13-150 podman[10701]: waiting...
Sep 29 21:37:45 ip-10-0-13-150 podman[10701]: waiting...
Sep 29 21:37:50 ip-10-0-13-150 podman[10701]: waiting...
Sep 29 21:37:50 ip-10-0-13-150 podman[10701]: done
Sep 29 21:37:50 ip-10-0-13-150 podman[10701]: 2022-09-29 21:37:50.536310798 +0000 UTC m=+15.290831522 container died 2e0a21b085113ad6b5eab83a1f4b85081045727b711c89909dc7d204abb25e61 (image=docker.io/library/fedora:36, name=clean, health_status=)
Sep 29 21:37:50 ip-10-0-13-150 podman[10873]: 2022-09-29 21:37:50.707597051 +0000 UTC m=+0.115808702 container remove 2e0a21b085113ad6b5eab83a1f4b85081045727b711c89909dc7d204abb25e61 (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 21:37:50 ip-10-0-13-150 systemd[1]: clean.service: Deactivated successfully.
Sep 29 21:37:50 ip-10-0-13-150 systemd[1]: Stopped clean.service - Clean on shutdown.

Но в ходе реального завершения работы ExecStop не сможет создать контейнер.


Sep 29 21:41:21 ip-10-0-13-150 systemd[1]: Stopping clean.service - Clean on shutdown...
Sep 29 21:41:21 ip-10-0-13-150 podman[12002]: 2022-09-29 21:41:21.263765577 +0000 UTC m=+0.192250288 container create 214a91497691b33a2ee77a0ad6dc3b3894e102abf84d641f5df2abfc842d1cd0 (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 21:41:21 ip-10-0-13-150 podman[12002]: 2022-09-29 21:41:21.190543212 +0000 UTC m=+0.119027932 image pull  docker.io/fedora:36
Sep 29 21:41:21 ip-10-0-13-150 podman[12002]: time="2022-09-29T21:41:21Z" level=error msg="Unable to clean up network for container 214a91497691b33a2ee77a0ad6dc3b3894e102abf84d641f5df2abfc842d1cd0: \"error tearing down network namespace configuration for container 214a91497691b33a2ee77a0ad6dc3b3894e102abf84d641f5df2abfc842d1cd0: netavark: failed to delete if podman0: Received a netlink error message Operation not supported (os error 95)\""
Sep 29 21:41:21 ip-10-0-13-150 podman[12072]: 2022-09-29 21:41:21.689689685 +0000 UTC m=+0.185177533 container remove 214a91497691b33a2ee77a0ad6dc3b3894e102abf84d641f5df2abfc842d1cd0 (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 21:41:21 ip-10-0-13-150 podman[12002]: Error: OCI runtime error: crun: sd-bus call: Transaction for libpod-214a91497691b33a2ee77a0ad6dc3b3894e102abf84d641f5df2abfc842d1cd0.scope/start is destructive (shutdown.target has 'start' job queued, but 'stop' is included in transaction).: Resource deadlock avoided
Sep 29 21:41:21 ip-10-0-13-150 systemd[1]: clean.service: Control process exited, code=exited, status=126/n/a
Sep 29 21:41:21 ip-10-0-13-150 systemd[1]: clean.service: Failed with result 'exit-code'.
Sep 29 21:41:21 ip-10-0-13-150 systemd[1]: Stopped clean.service - Clean on shutdown.
-- Boot 332c28360e38479c91f5cab4898413b4 --
Sep 29 21:41:45 ip-10-0-13-150 systemd[1]: Starting clean.service - Clean on shutdown...
Sep 29 21:41:45 ip-10-0-13-150 systemd[1]: Finished clean.service - Clean on shutdown.

Я совершал эту ошибку сам и видел, как пользователи борются с ней, не осознавая её. В современных бездемонных контейнерных движках трудно определить упорядочивание зависимостей, чтобы гарантировать, что контейнер можно надёжно запустить в ходе завершения работы. Это открытая проблемная область, которая зависит от движка контейнеров.


ExecStop для существующего контейнера


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


#!/bin/bash

cleanup() {
  echo "cleaning..."
  for i in {1..3}; do
    sleep 5s
    echo "waiting..."
  done

  echo "done"
}

trap cleanup SIGINT SIGTERM

echo "Awaiting signals"
sleep infinity & wait $!

Позаботимся о том, чтобы bash-скрипт await запускался в контейнере как процесс №1 и обрабатывал сигналы. Невстроенные команды (например, sleep) могут откладывать обработку сигналов в неинтерактивных контекстах. Поэтому важно сделать sleep фоновым и использовать встроенную (прерываемую) команду wait для ожидания предыдущей команды.


Создаём, активируем и запускаем новый юнит clean.service в systemd.


# /etc/systemd/system/clean.service
[Unit]
Description=Clean on shutdown
After=multi-user.target

[Service]
Type=simple
ExecStartPre=-/usr/bin/podman rm clean
ExecStart=/usr/bin/podman run \
  --name clean \
  --log-driver=k8s-file \
  --rm \
  -v /usr/local/bin:/scripts \
  --stop-timeout=60 \
  --entrypoint /scripts/await \
  docker.io/fedora:36
ExecStop=/usr/bin/podman stop clean
TimeoutStopSec=180

[Install]
WantedBy=multi-user.target

Это типичный юнит с длительным временем выполнения и Type=simple. Podman запускает контейнер и проксирует сигналы (такие как SIGTERM при завершении работы) первому процессу контейнера. При прекращении работы юнита podman перестаёт отправлять сигнал SIGTERM первому процессу контейнера (и SIGKILL после --stop-timeout, что по умолчанию требует 10 секунд).


Как и ранее, юнит извлекается multi-user.target, но получает команду на запуск After (после) multi-user.target, на довольно поздней стадии запуска. Командно управляемые (ordered) юниты прекращают работу в обратном запуску порядке, поэтому прекращение работы для clean начинается раньше, чем для других юнитов. Сервис TimeoutStopSec определяет, как долго systemd должна ждать выполнения ExecStop до выполнения SIGKILL.


Подтверждаем активность юнита, дожидаемся сигнала.


systemctl status clean.service
● clean.service - Clean on shutdown
     Loaded: loaded (/etc/systemd/system/clean.service; enabled; vendor preset: disabled)
     Active: active (running) since Fri 2022-10-21 16:34:41 UTC; 1min 12s ago
   Main PID: 239609 (podman)
      Tasks: 9 (limit: 4427)
     Memory: 18.6M
        CPU: 275ms
     CGroup: /system.slice/clean.service
             ├─ 239609 /usr/bin/podman run --name clean --log-driver=k8s-file --rm -v /usr/local/bin:/scripts --stop-timeout=60 --entrypoint /scripts/await docker.io/fedora:36
             └─ 239680 /usr/bin/conmon --api-version 1 -c 21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70 -u 21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70 -r /usr/bin/crun -b /var/lib/containers/storage/overlay-containers/21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70/userdata -p /run/containers/storage/overlay-containers/21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70/userdata/pidfile -n clean --exit-dir /run/libpod/exits --full-attach -s -l k8s-file:/var/lib/containers/storage/overlay-containers/21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70/userdata/ctr.log --log-level warning --runtime-arg --log-format=json --runtime-arg --log --runtime-arg=/run/containers/storage/overlay-containers/21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70/userdata/oci-log --conmon-pidfile /run/containers/storage/overlay-containers/21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70/userdata/conmon.pid --exit-command /usr/bin/podman --exit-command-arg --root --exit-command-arg /var/lib/containers/storage --exit-command-arg --runroot --exit-command-arg /run/containers/storage --exit-command-arg --log-level --exit-command-arg warning --exit-command-arg --cgroup-manager --exit-command-arg systemd --exit-command-arg --tmpdir --exit-command-arg /run/libpod --exit-command-arg --network-config-dir --exit-command-arg "" --exit-command-arg --network-backend --exit-command-arg netavark --exit-command-arg --volumepath --exit-command-arg /var/lib/containers/storage/volumes --exit-command-arg --runtime --exit-command-arg crun --exit-command-arg --storage-driver --exit-command-arg overlay --exit-command-arg --storage-opt --exit-command-arg overlay.mountopt=nodev,metacopy=on --exit-command-arg --events-backend --exit-command-arg journald --exit-command-arg container --exit-command-arg cleanup --exit-command-arg --rm --exit-command-arg 21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70

Sep 29 16:34:41 ip-10-0-0-27 systemd[1]: Started clean.service - Clean on shutdown.
Sep 29 16:34:42 ip-10-0-0-27 podman[239609]: 2022-10-21 16:34:42.02827346 +0000 UTC m=+0.073162795 container create 21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70 (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 16:34:42 ip-10-0-0-27 podman[239609]: 2022-10-21 16:34:41.999309821 +0000 UTC m=+0.044199164 image pull  docker.io/fedora:36
Sep 29 16:34:42 ip-10-0-0-27 podman[239609]: 2022-10-21 16:34:42.253726035 +0000 UTC m=+0.298615370 container init 21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70 (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 16:34:42 ip-10-0-0-27 podman[239609]: 2022-10-21 16:34:42.263454996 +0000 UTC m=+0.308344331 container start 21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70 (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 16:34:42 ip-10-0-0-27 podman[239609]: 2022-10-21 16:34:42.263872042 +0000 UTC m=+0.308761624 container attach 21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70 (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 16:34:42 ip-10-0-0-27 podman[239609]: Awaiting signals

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

Обратите внимание на идентификатор загрузки и запустите systemctl reboot (с пометкой ????). После перезагрузки проверьте журнал операций.


Sep 29 16:34:42 ip-10-0-0-27 podman[239609]: Awaiting signals
????
Sep 29 16:42:06 ip-10-0-0-27 podman[239609]: cleaning...
Sep 29 16:42:06 ip-10-0-0-27 podman[239609]: Terminated
Sep 29 16:42:06 ip-10-0-0-27 systemd[1]: Stopping clean.service - Clean on shutdown...
Sep 29 16:42:11 ip-10-0-0-27 podman[239609]: cleaning...
Sep 29 16:42:16 ip-10-0-0-27 podman[239609]: waiting...
Sep 29 16:42:21 ip-10-0-0-27 podman[239609]: waiting...
Sep 29 16:42:26 ip-10-0-0-27 podman[239609]: waiting...
Sep 29 16:42:26 ip-10-0-0-27 podman[239609]: done
Sep 29 16:42:26 ip-10-0-0-27 podman[239609]: waiting...
Sep 29 16:42:31 ip-10-0-0-27 podman[239609]: waiting...
Sep 29 16:42:36 ip-10-0-0-27 podman[239609]: waiting...
Sep 29 16:42:36 ip-10-0-0-27 podman[239609]: done
Sep 29 16:42:36 ip-10-0-0-27 podman[239609]: 2022-10-21 16:42:36.44559863 +0000 UTC m=+474.490487974 container died 21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70 (image=docker.io/library/fedora:36, name=clean, health_status=)
Sep 29 16:42:36 ip-10-0-0-27 podman[241961]: 2022-10-21 16:42:36.80223871 +0000 UTC m=+30.342432111 container cleanup 21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70 (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 16:42:36 ip-10-0-0-27 podman[241961]: clean
Sep 29 16:42:36 ip-10-0-0-27 podman[239609]: 2022-10-21 16:42:36.907856847 +0000 UTC m=+474.952746174 container remove 21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70 (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 16:42:36 ip-10-0-0-27 podman[239609]: time="2022-10-21T16:42:36Z" level=error msg="forwarding signal 15 to container 21009bcf34c2fbd3810f7301d1693dba084d9d098cdcbd9e3b15d5dc22e3bd70: container has already been removed"
Sep 29 16:42:36 ip-10-0-0-27 systemd[1]: clean.service: Deactivated successfully.
Sep 29 16:42:36 ip-10-0-0-27 systemd[1]: Stopped clean.service - Clean on shutdown.
-- Boot d0b5eba20ebd452aae5dbffaee19eff8 --

Задача очистки выполняется и задерживает завершение работы, однако эта задача выполняется дважды. Запущенный контейнерный процесс получает SIGTERM, и podman stop также отправляет SIGTERM. Воспроизведём это при запуске скрипта await напрямую. Нажмём Ctrl-C много раз — и увидим многократный вызов cleanup.


Исправить это можно несколькими способами. Первый — удаление вызова podman stop из ExecStop. В данном случае помехой является то, что при нормальной работе systemctl такие операции, как systemctl stop clean.service, работать не будут, и нам потребуется отправить контейнеру SIGTERM самостоятельно при отладке.


Давайте лучше зачистим или сбросим (clear/reset) эту ловушку. Обрабатывать сигналы и параллелизм в скриптах оболочки не очень удобно, в Go было бы куда лучше. Но пока оставим это решение.


cleanup() {
  trap - SIGINT SIGTERM
  echo "cleaning..."
  ...
}
...

Перезагрузим и перезапустим clean.service. Обратим внимание на Boot ID в журнале операций, затем попытаемся снова запустить systemctl reboot (с пометкой ????).


Sep 29 17:01:04 ip-10-0-0-27 podman[3688]: Awaiting signals
????
Sep 29 17:02:20 ip-10-0-0-27 podman[3688]: cleaning...
Sep 29 17:02:20 ip-10-0-0-27 podman[3688]: Terminated
Sep 29 17:02:20 ip-10-0-0-27 systemd[1]: Stopping clean.service - Clean on shutdown...
Sep 29 17:02:25 ip-10-0-0-27 podman[3688]: waiting...
Sep 29 17:02:30 ip-10-0-0-27 podman[3688]: waiting...
Sep 29 17:02:35 ip-10-0-0-27 podman[3688]: waiting...
Sep 29 17:02:35 ip-10-0-0-27 podman[3688]: done
Sep 29 17:02:35 ip-10-0-0-27 podman[3688]: 2022-10-21 17:02:35.391304416 +0000 UTC m=+91.032365358 container died 75079c6a180f82d398bb33e1e22a45bf4b281e84912e780842dd167658e6179f (image=docker.io/library/fedora:36, name=clean, health_status=)
Sep 29 17:02:35 ip-10-0-0-27 podman[4229]: 2022-10-21 17:02:35.740391215 +0000 UTC m=+0.325653570 container remove 75079c6a180f82d398bb33e1e22a45bf4b281e84912e780842dd167658e6179f (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 17:02:35 ip-10-0-0-27 podman[4090]: clean
Sep 29 17:02:35 ip-10-0-0-27 systemd[1]: clean.service: Main process exited, code=exited, status=143/n/a
Sep 29 17:02:35 ip-10-0-0-27 systemd[1]: clean.service: Failed with result 'exit-code'.
Sep 29 17:02:35 ip-10-0-0-27 systemd[1]: Stopped clean.service - Clean on shutdown.
-- Boot f8b835d07c7f497c83487bdf3bd3e319 --

Убедимся, что cleanup запущен не более одного раза (3 сообщения waiting...), а также что завершение его работы отсрочено.


Есть ещё одно возможное исправление. Код завершения 143 указывает на выход из контейнера из-за запроса на корректное завершение работы SIGTERM; systemd отмечает это как ошибку, мы же скорее назовём это успехом. Установим статус сервиса SuccessExitStatus.


SuccessExitStatus=143

Sep 29 17:09:56 ip-10-0-0-27 podman[4981]: Awaiting signals
????
Sep 29 17:16:03 ip-10-0-0-27 podman[4981]: cleaning...
Sep 29 17:16:03 ip-10-0-0-27 podman[4981]: Terminated
Sep 29 17:16:03 ip-10-0-0-27 systemd[1]: Stopping clean.service - Clean on shutdown...
Sep 29 17:16:08 ip-10-0-0-27 podman[4981]: waiting...
Sep 29 17:16:13 ip-10-0-0-27 podman[4981]: waiting...
Sep 29 17:16:18 ip-10-0-0-27 podman[4981]: waiting...
Sep 29 17:16:18 ip-10-0-0-27 podman[4981]: done
Sep 29 17:16:18 ip-10-0-0-27 podman[4981]: 2022-10-21 17:16:18.636792684 +0000 UTC m=+382.160266981 container died 91888ef150ea54a9831736a966302d1811028ce3216afb4368bf0a266e61ee52 (image=docker.io/library/fedora:36, name=clean, health_status=)
Sep 29 17:16:18 ip-10-0-0-27 podman[7028]: 2022-10-21 17:16:18.968155309 +0000 UTC m=+15.157225407 container cleanup 91888ef150ea54a9831736a966302d1811028ce3216afb4368bf0a266e61ee52 (image=docker.io/library/fedora:36, name=clean, health_status=, maintainer=Clement Verna <cverna@fedoraproject.org>)
Sep 29 17:16:18 ip-10-0-0-27 podman[7028]: clean
Sep 29 17:16:19 ip-10-0-0-27 systemd[1]: clean.service: Deactivated successfully.
Sep 29 17:16:19 ip-10-0-0-27 systemd[1]: Stopped clean.service - Clean on shutdown.
-- Boot 3517a0a13b3e4885be9901666c0fd173 --

А что дальше?


Как мы убедились, завершение работы при помощи юнита systemd — дело тонкое. А ведь мы уделили внимание только фазе раннего выключения, а не тем сервисам (или операциям), которые должны прекращать работу как можно позднее (например, журналы событий). Тем не менее мы рассмотрели несколько возможных стратегий.


В следующем посте мы применим эти методы к Kubernetes Kubelet. Сервис Kubelet регистрируется в кластере Kubernetes и запускает контейнеры через среду для запуска контейнеров (container runtime) с такими функциями, как хуки preStop, terminationGracePeriod и бюджеты прерывания (disruption budgets). Остановка сервиса Kubelet не оповещает кластер и не останавливает контейнеры, при этом (до недавнего времени) ни один из них не отключался.


Хотите больше такого контента?


  • Следите за новыми твитами в блоге @poseidonlabs
  • Поддержите работы Poseidon с открытым кодом, став одним из наших спонсоров
  • Переписывайтесь с нами по email tech@psdn.io

Исходный код


Рассмотренные в этой статье примеры доступны на blog-bits по лицензии MPL 2.0. Они тестировались на Fedora CoreOS 36.20221001.3.0 (systemd 250.8).


[Unit]
Description=Clean on shutdown
After=multi-user.target

[Service]
Type=simple
ExecStartPre=-/usr/bin/podman rm clean
ExecStart=/usr/bin/podman run \
  --name clean \
  --log-driver=k8s-file \
  --rm \
  -v /usr/local/bin:/scripts \
  --stop-timeout=60 \
  --entrypoint /scripts/await \
  docker.io/fedora:36
ExecStop=/usr/bin/podman stop clean
TimeoutStopSec=180
SuccessExitStatus=143

[Install]
WantedBy=multi-user.target

#!/bin/bash

cleanup() {
  trap - SIGINT SIGTERM
  echo "cleaning..."
  for i in {1..3}; do
    sleep 5s
    echo "waiting..."
  done

  echo "done"
}

trap cleanup SIGINT SIGTERM

echo "Awaiting signals"
sleep infinity & wait $!

Если вы нашли баг, пожалуйста, пришлите свой вариант исправления, и я постараюсь обновить код.



А мы научим работать c Linux, чтобы вы прокачали карьеру или стали востребованным IT-специалистом:



Чтобы посмотреть все курсы, кликните по баннеру:



Комментарии (2)


  1. chemtech
    03.11.2022 08:14

    Спасибо за пост. Искал, искал issue по graceful shutdown в https://github.com/systemd/systemd - ни одного похожего issue не нашел. Есть ли примеры некорректного завершения unit?