Ожидание готовности сервисов в 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.shdocker-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-приложениях.