Ожидание готовности сервисов в Docker Compose: wait-for-it vs Healthcheck
При разработке приложений с помощью Docker Compose часто возникает ситуация, когда один сервис должен дождаться готовности другого перед началом своей работы. Например, веб-приложению может понадобиться дождаться запуска базы данных или другого зависимого сервиса. В этой статье мы рассмотрим два популярных способа решения этой задачи:
Для демонстрации мы создадим простое приложение на Python, но для понимания статьи знание Python не требуется — вы сможете просто скопировать код.
Настройка рабочего окружения
Для начала создадим рабочую директорию:
mkdir waitforit cd waitforit
Теперь создадим тестовое веб-приложение на базе aiohttp
, которое будет включать эндпоинт /healthz
для проверки состояния. Также добавим возможность задержки перед запуском через переменную окружения SLEEP_BEFORE_START
. Эта переменная позволит имитировать задержку старта сервиса. Чтобы сымитировать задержку старта сервиса, будем передавать в эту переменную количество секунд на которое надо задержаться перед запуском приложения.
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
для нашего приложения.
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
, который будет запускать два сервиса:
- web — наше веб-приложение с задержкой старта.
- dependent — сервис, который должен ждать, пока web станет доступным.
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-приложениях.