Как управлять зависимостями между контейнерами, зависящими от времени, в стеке приложения

Как добропорядочные граждане Docker, мы все создаем контейнеры, разделяя проблемные области: контейнер для нашей серверной части, другой для нашего внешнего интерфейса и третий для постоянной службы.

Но что происходит, когда внутренний контейнер запускается и работает только для того, чтобы вы узнали, что контейнер базы данных все еще пытается стать доступным? Ваше приложение не запустится или оно будет повторять попытки до тех пор, пока служба базы данных не станет доступной?

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

Определение зависимостей в Docker Compose

Docker Compose, начиная с версии 2, позволяет вам явно определять взаимозависимости ваших контейнеров с помощью ключа depends_on. Добавляя ключ depends_on в спецификацию службы в вашем YAML-файле Docker Compose, вы фактически сообщаете Docker Compose, что «Контейнер FOO зависит от контейнера BAR». Вот как это просто:

Приведенный выше YAML позволит Docker Compose знать не только, как запускать ваши контейнеры в определенном порядке, но и как их выключать в точном обратном порядке:

Итак, это было проще простого! Наши контейнеры запускаются в точном порядке, в котором мы хотим, и когда наша серверная часть попытается получить доступ к базе данных, она уже будет там, готова и будет прослушивать запросы… верно? Ну не так быстро.

Запущенный контейнер - это еще не готовый контейнер

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

Docker Compose не знает, что и как проверять внутри вашего контейнера, чтобы узнать фактический статус приложения, запущенного внутри контейнера. Все, что его волнует, это то, что CMD или ENTRYPOINT были успешно выполнены. Давайте проведем быстрый тест, чтобы увидеть это на практике, с помощью следующего примера файла Docker Compose:

Давайте запустим его с docker-compose up:

Как вы можете видеть выше, хотя Docker Compose уважал наше определение зависимостей и запускал контейнер базы данных до внутреннего контейнера, последнему потребовалось намного больше времени, чтобы быть готовым принять запрос. Если бы внутренний контейнер попытался получить доступ к базе данных за 26 секунд до того, как первый был готов, это бы не удалось.

Очевидно, необходим более совершенный механизм проверки статуса службы - механизм, который учитывает природу базовой встроенной службы, которая знает, как проверять ее статус. Давайте посмотрим, как это реализовать дальше.

Внедрение проверок статуса обслуживания

Есть несколько разных способов проверить, готова ли базовая служба в контейнере принимать запросы, независимо от того, что сам Docker сообщает о своем хост-контейнере. Давайте рассмотрим наши варианты.

Использование проверок работоспособности контейнеров

Хотя проверки работоспособности Docker - очень полезная концепция, вы не можете использовать ее, чтобы один контейнер ожидал другого контейнера. Я просто упоминаю об этом здесь, так как вы можете найти в Интернете ссылки на него, которые, к сожалению, больше не подходят для современных версий Docker Compose. В репозитории Docker на GitHub идет длинное обсуждение именно этой темы и того, почему эта функция была удалена (и никогда не вернется). С другой стороны, если вы хотите внедрить проверки работоспособности для мониторинга своих контейнеров, я писал об этом в предыдущей статье.

Использование сторонних инструментов и интеграций

С первых дней Docker Compose люди пытались решить проблему взаимозависимости контейнеров с помощью собственных решений. Вот три наиболее широко используемых:

  1. wait-for-it
    wait-for-it.sh - это чистый сценарий bash, который ожидает доступности хоста и порта TCP. Это полезно для синхронизации развертывания взаимозависимых служб, таких как связанные контейнеры Docker. Поскольку это чистый сценарий bash, он не имеет внешних зависимостей.
  2. dockerize
    dockerize - это утилита для упрощения запуска приложений в контейнерах Docker. Он позволяет создавать файлы конфигурации приложения во время запуска контейнера из шаблонов и переменных среды контейнера, сводить несколько файлов журнала к stdout и / или stderr или ждать, пока другие службы будут доступны с использованием TCP, HTTP (S).
  3. await.sh
    await.sh - это самодостаточный сценарий оболочки POSIX, ожидающий доступности ресурсов и служб. Как и wait-for-it, он имеет минимальные системные требования (оболочка POSIX, тайм-аут, nc, wget) и работает с BusyBox, а также с образами Alpine Linux.

С технической точки зрения, ни один из вышеперечисленных инструментов не требует для работы модифицированного образа Docker вашего контейнера. Однако они должны быть каким-то образом доступны внутри контейнера для выполнения. Тогда давайте попробуем обернуть наш контейнер hello-world с помощью dockerize и перестроить его с помощью docker build . -t hello-world-dockerize:

FROM hello-world
FROM jwilder/dockerize
COPY --from=0 hello /
ENTRYPOINT dockerize -wait tcp://database:3306 -timeout 60s /hello

Затем обновите файл Docker Compose, чтобы использовать обернутый образ:

Пришло время еще раз запустить наш демонстрационный стек приложения withdocker-compose up:

На этот раз внутренний контейнер терпеливо ждал, пока служба базы данных, работающая внутри контейнера базы данных, не станет доступной.

Введение в отказоустойчивость

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

Кроме того, если вы введете проверки работоспособности в свои контейнеры - как и следовало бы - вы легко можете столкнуться с ситуациями динамической блокировки, когда контейнер, ожидающий, когда станет доступен другой контейнер, сам сбрасывается из-за тайм-аута проверки работоспособности.

Парадигма проектирования в таких случаях заключается в обеспечении отказоустойчивости в вашей системе. Например, вместо того, чтобы ваш внутренний контейнер не загружался из-за того, что контейнер базы данных был недоступен, он может продолжить загрузку и просто вернуть код ошибки, когда он пытается обслужить запрос до того, как будет установлен доступ к базовой базе данных. Фоновый процесс может проверить доступность службы базы данных и прозрачно подключиться к базе данных, когда будет готов.

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

Заключение

Когда у вас есть стек приложений, состоящий из контейнеров Docker, вы можете оказаться в ситуации, когда требуется определенный порядок загрузки. Для этого вы можете интегрировать сторонние решения, чтобы контейнер ожидал доступности службы в другом контейнере. Однако, когда это возможно, вы должны ввести отказоустойчивость в свой дизайн, позволяя вашим контейнерам постепенно деградировать, когда ресурсы становятся недоступными.

Спасибо, что прочитали эту статью. Надеюсь увидеть вас в следующем.