- BrainTools - https://www.braintools.ru -

Контейнер притворяется сервером: четыре места, где выдаёт себя

В моём тренажёре Prod is Down [1] каждый игрок получает свой сломанный «сервер». На самом деле это контейнер, но игрок об этом думать не должен: он набирает те же команды, что и на настоящем дежурстве. systemctl restart nginx, journalctl -u api, free -m, top.

Из коробки половина этих команд в Docker либо не работает, либо врёт. Ниже четыре места, где контейнер выдаёт себя.

1. systemctl: PID 1 — не systemd

В обычном контейнере первым процессом запускается ваш entrypoint, а не systemd. Даже если пакет systemd стоит в образе, systemctl не с кем разговаривать:

$ systemctl restart nginx
System has not been booted with systemd as init system (PID 1). Can't operate.
Failed to connect to bus: Host is down

Вариантов три, и у каждого своя цена:

Вариант

Что даёт

Чем платите

Настоящий systemd как PID 1

Всё работает как на сервере: юниты, Restart=, journald

Нужны доступ к cgroup, tmpfs на /run, часто --privileged. Для контейнера с чужим root это плохая идея

Замена systemctl

Скрипт читает юнит-файлы и сам запускает ExecStart. Готовый пример — docker-systemctl-replacement

Подмножество systemd: что-то из юнитов будет проигнорировано

Свой скрипт запуска

Ровно то, что нужно вашим сервисам

Всё, чего в нём нет, придётся дописывать самому

В Prod is Down сервисы кейса поднимает отдельный шаг entrypoint — pd-systemd-boot, а терминал запускается последним. Первый вариант для меня отпадал сразу: в контейнере сидит незнакомый человек с sudo, и давать ему привилегированный контейнер ради настоящего systemd нельзя.

2. Сигналы и зомби: чем особенен PID 1

У первого процесса две обязанности, о которых на сервере заботится systemd, а в контейнере – никто.

Контейнер притворяется сервером: четыре места, где выдаёт себя - 1

сСигналы. Ядро не применяет к PID 1 действие по умолчанию. Если скрипт не установил обработчик SIGTERM, сигнал просто игнорируется. docker stop ждёт 10 секунд и добивает контейнер SIGKILL, а сервисы внутри не успевают завершиться чисто.

Зомби. Когда родитель умирает, его дети переходят к PID 1, и он должен забирать их статус выхода. Обычное приложение этого не делает, и в ps копятся процессы <defunct>. В тренажёре это вдвойне неприятно: игрок ищет в ps причину инцидента и находит мусор, который к кейсу отношения не имеет.

Контейнер притворяется сервером: четыре места, где выдаёт себя - 2

Лечится одним флагом: docker run -init (в compose – init: true). Первым процессом станет маленький init из комплекта Docker. Он передаст сигналы вашему entrypoint и будет собирать зомби. А в самом entrypoint последний процесс стоит запускать через exec, чтобы не оставлять лишний слой bash.

3. journalctl и логи

Нет systemd – нет и journald. Игрок набирает первую команду любого разбора и получает:

$ journalctl -u nginx
No journal files were found.

При этом у Docker свои логи, и они тоже не помогают: docker logs собирает только stdout и stderr первого процесса. Всё, что nginx или PostgreSQL пишут в свои файлы, туда не попадает. Да и игроку изнутри контейнера docker logs недоступен.

Потому решите заранее, где человек будет искать причину:

  • либо сервисы пишут в привычные файлы в /var/log, и вы проверяете, что они там правда появляются: многие образы направляют логи в stdout симлинком;

  • либо journalctl должен работать, и тогда нужен journald или замена systemctl, которая умеет отдавать логи сервиса.

Худший вариант – когда логи не пишутся никуда. На настоящем сервере такого не бывает, и человек долго ищет ошибку [2] там, где её нет.

4. free и top показывают хост, а не лимиты

Контейнеру выдали 512 МБ и четверть ядра. Игрок набирает free -m и видит всю память [3] сервера, а top и nprocпоказывают все его ядра. Эти утилиты читают /proc/meminfo и /proc/stat, а в обычном Docker это файлы хоста. nproc учитывает --cpuset-cpus, но не квоту.

Для тренажёра это ловушка. В кейсе про утечку памяти free покажет гигабайты свободного места, а OOM killer всё равно придёт. У контейнера свой потолок, и виден он только в cgroup:

cat /sys/fs/cgroup/memory.max       # 536870912 — настоящий лимит
cat /sys/fs/cgroup/memory.current   # сколько занято сейчас
cat /sys/fs/cgroup/cpu.max          # 25000 100000 — четверть ядра

Решений два. LXCFS [4] (ссылка на GitHub) подменяет /proc/meminfo и компанию так, чтобы они показывали лимиты контейнера, и привычные утилиты начинают говорить правду. Или можно показывать цифры самому. Дашборд Prod is Down, например, считает CPU от лимита контейнера: процесс, съевший свою четверть ядра, выглядит как привычные 100%.

И одно предупреждение. В песочницах вроде gVisor /proc эмулируется, и часть цифр ведёт себя по-своему. Всё, что увидит человек, проверяйте в том рантайме, в котором он будет работать.

Итог

Если контейнер должен выглядеть для человека как сервер, проверьте четыре вещи:

  1. Кто у вас PID 1 и что отвечает на systemctl.

  2. Кто передаёт сигналы и собирает зомби (--init).

  3. Где человек найдёт логи сервиса и работает ли journalctl.

  4. Что показывают free, top и nproc: лимиты контейнера или хост.

Автор: verin_is

Источник [5]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/38007

URLs in this post:

[1] Prod is Down: https://prodisdown.ru/?utm_source=habr

[2] ошибку: http://www.braintools.ru/article/4192

[3] память: http://www.braintools.ru/article/4140

[4] LXCFS: https://github.com/lxc/lxcfs

[5] Источник: https://habr.com/ru/articles/1103466/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1103466

www.BrainTools.ru

Rambler's Top100