Docker
October 12

Ожидание готовности сервисов в Docker Compose: wait-for-it vs Healthcheck

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

  1. Скрипт wait-for-it.sh
  2. Использование depends_on и healthchecks в Docker Compose

Для демонстрации мы создадим простое приложение на Python, но для понимания статьи знание Python не требуется — вы сможете просто скопировать код.

Настройка рабочего окружения

Для начала создадим рабочую директорию:

mkdir waitforit
cd waitforit

Теперь создадим тестовое веб-приложение на базе aiohttp, которое будет включать эндпоинт /healthz для проверки состояния. Также добавим возможность задержки перед запуском через переменную окружения SLEEP_BEFORE_START. Эта переменная позволит имитировать задержку старта сервиса. Чтобы сымитировать задержку старта сервиса, будем передавать в эту переменную количество секунд на которое надо задержаться перед запуском приложения.

Файл: app.py

import os
import time
from aiohttp import web

async def healthz(request):
    return web.Response(text="OK")

if __name__ == "__main__":
    sleep_time = int(os.getenv("SLEEP_BEFORE_START", 0))
    print(f"Sleep {sleep_time} sec before start...")
    time.sleep(sleep_time)

    app = web.Application()
    app.add_routes([web.get("/healthz", healthz)])
    print("Starting...")
    web.run_app(app, host="0.0.0.0", port=8080)

Теперь создадим Dockerfile для нашего приложения.

Файл: Dockerfile

FROM python:3.9-slim

WORKDIR /app
RUN pip install aiohttp \
    && apt-get update \
    && apt-get install -y curl
COPY app.py .
CMD ["python", "-u", "app.py"]

Теперь создадим docker-compose.yml, который будет запускать два сервиса:

  1. web — наше веб-приложение с задержкой старта.
  2. dependent — сервис, который должен ждать, пока web станет доступным.

Файл: docker-compose.yml

version: "3.8"

services:
  web:
    image: sleep-web-app
    build: .
    environment:
      SLEEP_BEFORE_START: 10

  dependent:
    image: sleep-web-app
    depends_on:
      - web

Запустим контейнеры и посмотрим на результат:

docker compose up --build

Результат покажет, что сервис dependent начнёт запуск до того, как сервис web будет готов:

web-1        | Sleep 10 sec before start...
dependent-1  | Sleep 0 sec before start...
dependent-1  | Starting...
web-1        | Starting...

Наша задача — настроить Docker Compose так, чтобы сервис dependent начинал работу только после того, как сервис web будет полностью готов. В результате хотим получить такой лог:

web-1        | Sleep 10 sec before start...
web-1        | Starting...
dependent-1  | Sleep 0 sec before start...
dependent-1  | Starting...

Использование wait-for-it.sh

wait-for-it.sh — это простой Bash-скрипт, который блокирует выполнение сервиса до тех пор, пока другой сервис не станет доступен, проверяя доступность конкретного порта. Это удобно для проверки готовности баз данных, веб-сервисов или других TCP-сервисов.

Скрипт надо запустить до основной команды запуска приложения. Это можно сделать в entrypoint или непосредственно подставить перед командой запуска. В нашем примере воспользуемся вторым способом для простоты.

В качестве аргументов достаточно передать host:port, доступность которого ожидаем. Тогда скрипт подождёт 15 секунд (значение по умолчанию). Чтобы задать своё время ожидания, можно добавить аргумент --timeout <sec>. Если за это время порт станет доступным, то скрипт остановится и сервис продолжит запуск, но если host:port окажется недоступным по истечении таймаута сервис всё равно начнёт запуск. Чтобы этого избежать и остановить запуск с ошибочным статусом, необходимо передать флаг --strict.

Давайте скачаем скрипт с GitHub и попробуем им воспользоваться:

curl -o wait-for-it.sh https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh
chmod +x wait-for-it.sh

Добавим к сервису dependent волюм, через который прокинем скрипт. И добавим команду запуска приложения с использованием wait-for-it.sh. Скрипт будет ожидать готовность web сервиса на порту 8080. Основная команда запуска сервиса выполняется после --. Вы можете поиграть с параметрами и различными сценариями запуска. Мы рассмотрим пример, когда мы примерно понимаем сколько надо ждать web сервис (10 секунд) и оставим таймаут по умолчанию (15 секунд).

  dependent:
    image: sleep-web-app
    command:
      - ./wait-for-it.sh
      - web:8080
      - --
      - python
      - -u
      - app.py
    volumes:
      - ./wait-for-it.sh:/app/wait-for-it.sh

Теперь запустим наши сервисы:

docker-compose up

Из лога видно, что сервис dependent начинает работу с запуска wait-for-it.sh и ожидания web:8080. После того как web сервис запускается, dependent останавливает wait-for-it.sh, пишет сколько он прождал и выполняет команду, указанную после -- слешей. Мы добились необходимого поведения: зависимый сервис запускается после того, как другой сервис готов принимает соединения на своём порту.

dependent-1  | wait-for-it.sh: waiting 15 seconds for web:8080
web-1        | Sleep 10 sec before start...
web-1        | Starting...
dependent-1  | wait-for-it.sh: web:8080 is available after 11 seconds
dependent-1  | Sleep 0 sec before start...
dependent-1  | Starting...

Преимущества wait-for-it.sh:

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

Недостатки:

  • wait-for-it.sh проверяет только доступность порта, но не проверяет полную готовность сервиса (например, не проверяет завершение инициализации схемы базы данных). Часто бывает, что порт уже открыт, а сервис не готов обрабатывать входящие запросы. Эту проблему поможет решить следующий способ.

Использование depends_on и healthcheck

Еще один способ заставить один сервис ожидать готовности другого — это использование условной опции depends_on и механизма healthcheck в Docker Compose.

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

Добавим healthcheck для сервиса web, чтобы проверять его готовность через эндпоинт /healthz. Тем самым, мы будем знать, что у сервиса не просто открыт порт, но и то, что сервис готов обрабатывать запросы. Подробнее про healthcheck можно прочитать здесь. В test задаётся команда, которая проверяет здоровье сервиса. Команда могла бы проверять просто открытость порта, тогда это был бы эквивалент wait-for-it.sh. Но суть healthcheck не просто проверить порт, а готовность сервиса обработать запросы.

  web:
    image: sleep-web-app
    build: .
    environment:
      SLEEP_BEFORE_START: 10
    healthcheck:
      test: ["CMD", "curl", "http://web:8080/healthz"]
      interval: 10s # Как часто будет проверяться статус
      retries: 5 # Сколько раз проверить, прежде чем считать недоступным
      start_period: 10s # Через сколько после запуска начинать проверки
      timeout: 10s # Таймаут на каждый запуск test

К зависимому сервису добавим depends_on с условием. Сервис dependent зависит от web, но будет запускаться только после того, как сервис web успешно пройдет проверку состояния. Больше про условный depends_on можно прочитать здесь. Надо упомянуть, что не все версии композа поддерживают условный depends_on.

Финальный композ:

version: "3.8"

services:
  web:
    image: sleep-web-app
    build: .
    environment:
      SLEEP_BEFORE_START: 10
    healthcheck:
      test: ["CMD", "curl", "http://web:8080/healthz"]
      interval: 10s
      retries: 5
      start_period: 10s
      timeout: 10s

  dependent:
    image: sleep-web-app
    depends_on:
      web:
        condition: service_healthy

Теперь снова запускаем наши сервисы:

docker-compose up

На этот раз сервис dependent начнет выполнение только после того, как проверка состояния сервиса web через /healthz пройдет успешно.

web-1        | Sleep 10 sec before start...
web-1        | Starting...
dependent-1  | Sleep 0 sec before start...
dependent-1  | Starting...

Популярные сервисы имеют готовые решения для healthcheck:

postgresql: ["CMD", "pg_isready", "-U", "postgres"]
kafka: ["CMD-SHELL", "kafka-broker-api-versions.sh --bootstrap-server localhost:9092"]
redis: ["CMD", "redis-cli", "ping"]
mysql: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
mongodb: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"]
elasticsearch: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]

Преимущества depends_on + healthcheck:

  • Проверки состояния дают более полное представление о готовности сервиса по сравнению с простой проверкой порта.
  • Это встроенное решение Docker Compose, что упрощает его использование без дополнительных скриптов.
  • Легче настраивать цепочки зависимостей.

Недостатки:

  • Более объемная конфигурация, которая может быть менее гибкой для сервисов, не основанных на TCP.
  • Не все образы поддерживают проверки состояния по умолчанию, возможно, потребуется писать свои скрипты.
  • Сложность с проверкой готовности внешних сервисов, вне композа.

Какой метод выбрать?

Выбор между wait-for-it.sh и depends_on с healthcheck зависит от вашего конкретного случая:

  • Используйте wait-for-it.sh, если вам нужно быстрое и простое решение для ожидания доступности порта, особенно в тех средах, где вы имеете больший контроль над зависимостями.
  • Используйте depends_on + healthcheck, если вам нужно более надежное, встроенное решение Docker Compose, которое гарантирует полную готовность сервиса, а не только его доступность.

Заключение

Управление готовностью сервисов в Docker Compose — важный аспект создания надежных многосервисных приложений. Независимо от того, выберете ли вы простоту wait-for-it.sh или встроенные возможности depends_on с проверками состояния, оба метода помогают обеспечить, что ваши сервисы ожидают полной готовности зависимостей перед запуском. Правильный выбор стратегии позволит избежать потенциальных проблем и сделать запуск сервисов более плавным в ваших Docker-приложениях.