Интересную проблему удалось решить вчера.

Вот уже несколько месяцев мы в Naumen Phone используем Docker для создания окружения для сборки и тестирования приложений. Технология для этих целей подходит отлично, экономит кучу времени и нервов по сравнению с тем, что было раньше. Образ контейнера создаётся на билд-сервере по рецепту из репозитория (с код-ревью и историей изменений). Он получается по-настоящему иммутабельным - а значит, гарантируется воспроизводимость поведения при локальных запусках и на билд-сервере. Он запускается практически мгновенно, а значит, контейнер не жалко убить и пересоздать заново. На него можно довольно легко писать автотесты (например, на Testinfra), которые тоже выполняются быстро.

Но, как и везде, возникают свои нюансы.

Так как мы перешли на Docker с виртуализованного окружения (Virtualbox/KVM/Vagrant), то многие вещи делаем по старинке, как привыкли на виртуалках. В частности, мы используем для интеграционных автотестов большой “сборный” образ, в котором установлена и СУБД, и пачка наших собственных сервисов, и даже целый systemd (через который запускается всё это хозяйство). Конечно, это далеко от тех же принципов 12 factor app, но в целом в подход each container should have only one concern вписывается. Да и User Experience для разработчиков остаётся почти такой же, как был на VBox+Vagrant, что немаловажно.

Но вот была одна странная проблема, которая долго пила нам кровушку. Почему-то при первом запуске контейнера далеко не сразу становился доступен сервис PostgreSQL. Он, вроде бы, и запускался быстро, и соединения принимал - но затем клиенты отбивались на этапе аутентификации. И сообщение об ошибке мало что говорило нам:

    ВАЖНО:  система баз данных запускается

Это безобразие продолжалось порой по несколько минут, после чего сервер наконец-то становился паинькой и соглашался работать. Вот только далеко не все клиенты были к этому времени готовы работать с ним. То один, то другой сервис не дожидался старта СУБД и отваливался по таймауту. Приходилось ставить костыль в виде shell-скрипта, который дожидается полного запуска PG и после этого перезапускает наши сервисы. При запуске в Vagrant это выглядело сурово:

img

Однако, как ни странно, если контейнер после работы не удалялся, а просто останавливался (через vagrant halt), то при повторных его запусках проблема уже не повторялась. Старт проходил быстро, и сервисы подключались к базе без проблем.

Это наводило на мысль, что можно попытаться заставить СУБД сделать при создании что-то такое, что она делает во время работы контейнера, чтобы при следующем запуске быстро подняться. Только было непонятно, что же именно делать. Гуглёж по сообщению об ошибке не находил ничего, что наводило бы на суть проблемы.

Пришлось расчехлять strace и смотреть, чем заняты процессы СУБД. Оказалось, что они открывают и закрывают большое количество файлов, в которых СУБД, собственно, и хранит все свои данные. Я включил фантазию и начал гуглить всякие конструкции типа “postgresql slow startup”, “postgresql startup base files” и т.п. По ссылкам встречалось упоминание recovery mode, так что я начал шерстить мануалы на эту тему.

И внезапно нашёл в документации pg_ctl это

pg_ctl stop [-W] [-t seconds] [-s] [-D datadir] [-m s[mart] | f[ast] | i[mmediate]]

In stop mode, the server that is running in the specified data directory is
shut down. Three different shutdown methods can be selected with the -m
option.  “Smart” mode waits for all active clients to disconnect and any
online backup to finish. If the server is in hot standby, recovery and
streaming replication will be terminated once all clients have
disconnected.  “Fast” mode (the default) does not wait for clients to
disconnect and will terminate an online backup in progress. All active
transactions are rolled back and clients are forcibly disconnected, then
the server is shut down.  “Immediate” mode will abort all server processes
immediately, without a clean shutdown. This will lead to a crash-recovery
run on the next restart.

Если перевести на русский, то у PostgreSQL есть три режима завершения работы:

  • smart: дожидаться завершения работы всех клиентов, а затем делать бэкап
  • fast: отбивать всех клиентов, а затем делать бэкап (режим по-умолчанию)
  • immediate: отбивать всех клиентов и завершаться без бэкапа

Ключевая фраза оказалась в самом конце абзаца: “this will lead to crash-recovery run on the next restart”.

Если запустить PG на “железной” машине или внутри обычной виртуалки, то при корректном завершении работы машины СУБД получит от init соответствующий сигнал и остановится в режиме fast. Но в docker работа завершается через брутальное убийство всего дерева процессов, начиная с контейнерного init. Разумеется, в этой ситуации PG не имеет возможности сделать бэкап. Поэтому мы и получаем “crash-recovery run” при следующем запуске. И пока СУБД не проверит полностью всё своё добро на диске, пока не убедится в корректности данных (или не починит некорректные данные), ни одному клиенту не позволительно подключаться к серверу. Но после прогона этого восстановления база оказывается в консистентном состоянии, поэтому следующие запуски осуществляются быстро.

Значит, чтобы ускорить самый первый запуск контейнера, мы должны дать PostgreSQL возможность сделать online backup при сборке образа.

Проверить эту гипотезу оказалось совсем несложно, так как контейнер создаётся из исходного кода. Я просто добавил одну команду в самом конце скрипта:

su - postgres -c '/usr/pgsql-9.6/bin/pg_ctl stop'

Собрал образ локально, запустил его через Vagrant, и - о чудо! - теперь всё запускается моментально!

Полученные уроки

Очевидный вывод из этой истории может звучать так: know your tools. Не думаю, что наша проблема показалась бы сложной для эксперта по PG. Но… мы не волшебники, мы только учимся. И в том числе на таких вот ошибках - довольно эффективный способ, между прочим :)

Ещё один показательный пример: в проблеме помог разобраться strace. Хотя он и не указал на источник проблемы явным образом, но задал правильное направление для дальнейших поисков. Это безусловное преимущество открытой инфраструктуры GNU/Linux: ни один процесс, запущенный на машине, не является “чёрным ящиком”. Используя правильные инструменты, можно увидеть, чем именно он занят, даже не используя исходники. Хотя, если надо, качай исходники и их тоже используй - красота же!

Ну и не удержусь, порадуюсь ещё раз, что мы наводим порядок в своей инфраструктуре, используя подход infrastructure as code. Я уже наелся за прошлые годы проблем с мутабельными, настраиваемыми вручную виртуалками, проблемы с которыми вопроизводятся ненадёжно, так что нынешнее положение дел радует меня очень и очень.

Чего и вам желаю, мои уважаемые читатели!

P.S.: а мы пойдём вычищать из кода костыли…