Разработка системы с элегантным завершением работы может оказаться той ещё пляской с бубном. В идеальном мире каждый сервис управлялся бы юнитом 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-специалистом:
Чтобы посмотреть все курсы, кликните по баннеру:
Data Science и Machine Learning
- Профессия Data Scientist
- Профессия Data Analyst
- Курс «Математика для Data Science»
- Курс «Математика и Machine Learning для Data Science»
- Курс по Data Engineering
- Курс «Machine Learning и Deep Learning»
- Курс по Machine Learning
Python, веб-разработка
- Профессия Fullstack-разработчик на Python
- Курс «Python для веб-разработки»
- Профессия Frontend-разработчик
- Профессия Веб-разработчик
Мобильная разработка
Java и C#
- Профессия Java-разработчик
- Профессия QA-инженер на JAVA
- Профессия C#-разработчик
- Профессия Разработчик игр на Unity
От основ — в глубину
А также
chemtech
Спасибо за пост. Искал, искал issue по graceful shutdown в https://github.com/systemd/systemd - ни одного похожего issue не нашел. Есть ли примеры некорректного завершения unit?