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

—

от автора

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

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

1. systemctl: PID 1 — не systemd

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

$ systemctl restart nginxSystem 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, а в контейнере — никто.

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

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

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

3. journalctl и логи

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

$ journalctl -u nginxNo journal files were found.

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

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

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

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

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

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

Контейнеру выдали 512 МБ и четверть ядра. Игрок набирает free -m и видит всю память сервера, а 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 (ссылка на GitHub) подменяет /proc/meminfo и компанию так, чтобы они показывали лимиты контейнера, и привычные утилиты начинают говорить правду. Или можно показывать цифры самому. Дашборд Prod is Down, например, считает CPU от лимита контейнера: процесс, съевший свою четверть ядра, выглядит как привычные 100%.

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

Итог

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

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

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

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

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

ссылка на оригинал статьи https://habr.com/ru/articles/1103466/