Если используется контейнеризация Docker в боевой среде, настоятельно рекомендуется использовать Healthcheck для сервисов, контейнеров и т.д. Особенно если используется Docker Swarm.
Напомню, что основная цель Docker Swarm это работа приложения без простоя zero-downtime. Если просто запуск сервиса еще можно пережить без Healthcheck, то вот обновление уже сложнее. Дело в том, что при обновлении сервиса Docker Swarm считает контейнер рабочим только если получил от него ответ, по умолчанию ответ - это просто уведомление о том что приложение (nginx, postgresql, custom app) внутри контейнера запустилось и работает.
Согласитесь, тот факт, что приложение внутри контейнера запустилось еще не говорит о том, что это полностью рабочий контейнер. Например, есть контейнер с Nginx, который должен отобразить страницу с тестом Hello. По умолчанию Docker посчитает контейнер рабочим просто потому что внутри стартанул Nginx, при этом он понятия не имеет что получит пользователь при обращении к контейнеру. В идеале я хочу, чтобы пользователь 100% получил страницу с текстом Hello. Вот тут я и могу использовать Healthcheck, настроить его так чтобы видеть действительно ли контейнер отдаёт пользователю то что необходимо.
docker-healthcheck-comprehensive.svg
Как это работает
При создании контейнера Docker выполняет внутри него команду, которую указываю я. И если эта команда возвращает Код выхода (Exit Codes) 0, то контейнер считается рабочим и запускается.
Коды выхода могут быть следующими:
- 0 (Успех): Контейнер исправен
- 1 (Сбой): Контейнер неисправен
- 2 (Резерв): Этот код зарезервирован для Docker и не рекомендуется для использования
В зависимости от ответа контейнер может иметь следующий из статусов:
- healthy — всё ок (0)
- unhealthy — проверка не проходит (1)
- starting — контейнер запущен, но ещё идёт проверка работоспособности (Healthcheck)
При этом важно понимать, что сам Docker не пытается ничего исправить он просто выполняет проверку работоспособности и выставляет статус. Максимум что он может сделать это попытаться перезапустить контейнер, если вы это настроили. А вот Docker Swarm автоматически пересоздаёт unhealthy задачи и исключает их из load balancer.
Основные параметры
Сам Healthcheck можно использовать и в Dockerfile, сервисах, стеках, в docker compose и даже docker run. Вот некоторые самые распространённые параметры для Healthcheck:
- interval: Время между попытками проверки (по умолчанию: 30 с). Т.е. если первая проверка прошла неудачно (нет ответа 0), то следующая попытка проверки через этот интервал времени
- timeout: Максимально допустимое время для выполнения одной проверки (по умолчанию: 30 секунд). Т.е. ждём ответа от контейнера в течении этого интервала времени.
- retries: Количество последовательных сбоев, необходимых для того, чтобы пометить контейнер как неисправный (по умолчанию: 3).
- start-period: Льготный период после запуска, в течение которого сбои не учитываются в лимите повторных попыток (по умолчанию: 0 с). Это как ожидание перед первой проверкой. Обратите внимание что если в течении 30 с (interval) контейнер не ответил 0 на Healthcheck, то он будет считаться неисправным. Отсюда вывод что если приложение в контейнере стартует очень долго (дольше 30 с), то этот параметр нужно выставить больше чем 30 секунд. Т.е. если ваше приложение стартует дольше 30 секунд, то вы получите ложное срабатывание неисправного контейнера.
Для примера если контейнер не вернул значение Hello, точнее не вернул код 0 в течении, времени которое занимает на проверку (interval + retries) то статус такого контейнера становится unhealthy и он отключается.
Пример с docker run
Для начала я создам образ c index.html, который выведет текст Hello при обращении к Nginx (nginx.conf). Саму команду HEALTHCHECK я помещу в Dockerfile, чтобы проверка происходила при каждом создании контейнера:
dockerfileFROM nginx:alpine
# Копируем HTML файл
COPY index.html /usr/share/nginx/html/index.html
# Устанавливаем curl для healthcheck
RUN apk add --no-cache curl
# Настраиваем healthcheck
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/ | grep -q "Hello" || exit 1
EXPOSE 80
Для создания образа я выполню команду docker build -t nginx-health:1.1 .. Заметьте, что тут есть CMD, не путайте это с простым CMD, который выполняет собственно команду внутри контейнера при старте. CMD в файле Dockerfile работает в связке с HEALTHCHECK, т.е. HEALTHCHECK ... CMD ....
Параметры HEALTHCHECK:
--interval=30s- проверка каждые 30 секунд--timeout=3s- таймаут ответа 3 секунды--start-period=5s- задержка перед первой проверкой--retries=3- количество неудачных попыток до пометки как unhealthy
Сама команда CMD для HEALTHCHECK:
1. curl -f http://localhost/
curl- утилита для HTTP запросов-f(–fail) - если сервер вернёт ошибку (404, 500 и т.д.), curl завершится с ненулевым кодомhttp://localhost/- адрес для проверки
2. | (pipe)
- Передаёт вывод curl в следующую команду (grep)
3. grep -q "Hello"
grep- поиск текста-q(–quiet) - тихий режим, ничего не выводит, только возвращает код результата"Hello"- искомый текст- Если найдено → код возврата 0 (успех)
- Если НЕ найдено → код возврата 1 (ошибка)
4. || (логическое ИЛИ)
- Выполняет команду справа, ТОЛЬКО если команда слева провалилась (код ≠ 0)
5. exit 1
- Завершает скрипт с кодом ошибки 1
- Для Docker это означает unhealthy
После сборки образа запускаю контейнер и проверяю его статус:
docker run -d -p 8080:80 --name nginx-healthcheck nginx-health:1.1
docker ps
```json
c1a33dd3ee25 nginx-health:1.1 Up 7 seconds (healthy) nginx-healthcheck
curl http://localhost:8080
<h1>Hello</h1>
Теперь я изменю файл index.html чтобы валидация HEALTHCHECK завершилась с кодом 1.
vim html/index.html
<h1>Hell</h1>
docker build -t nginx-health:1.2 .
docker rm -f nginx-healthcheck
docker run -d -p 8080:80 --name nginx-healthcheck nginx-health:1.2
docker ps
c1a33dd3ee25 nginx-health:1.1 Up 7 seconds (unhealthy) nginx-healthcheck
Пример с docker compose
Тут делается тоже самое, поэтому не буду прописывать всё один в один просто приведу пример файла docker-compose.yml.
docker rm -f nginx-healthcheck
docker compose up -d
docker compose ps
nginx-healthcheck nginx:alpine Up 3 minutes (unhealthy) 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
docker compose down
Пример с docker stack
Для примера буду использовать всё тот же подход с образом nginx и ответом Hello. Но то как ведут себя контейнеры при использовании Healthcheck в docker stack отличается. Как я уже говорил в docker stack самым главным является добиться необходимого статуса REPLICAS, и если статус контейнера не healthy то и контейнер не стартанёт. Особенно полезно использовать этот механизм при обновлении сервисов docker stack.
Для примера представим себе ситуацию:
- Я выкладываю сервис, который работает (отдаёт пользователю Hello)
- Я вношу правки в сервис и делаю ошибку (меняю на Hell)
- Перевыкладываю сервис (снова docker stack deploy)
- Проверяю что же произошло (спойлер обновления не будет)
docker stack deploy -c docker-stack.yml nginx-stack
docker stack services nginx-stack
ID NAME MODE REPLICAS IMAGE PORTS
jo8itz0b9lou nginx-stack_web replicated 2/2 nginx:alpine *:8080->80/tc
docker stack ps nginx-stack
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
djvpuz2ls9f3 nginx-stack_web.1 nginx:alpine c-stream-9-vm4 Running Running 2 minutes ago
ltcr3vfwz5jm nginx-stack_web.2 nginx:alpine c-stream-9-vm9 Running Running 2 minutes ago
Да и тут есть одно, но, сейчас, когда контейнеры работают статус task самого сервиса Running, т.е. статус Healthy или Unhealthy выставляется только контейнерам (docker container ls). Это особенность Swarm - healthcheck статус скрыт в выводе задач, но активно используется внутри для управления репликами и load balancing.
Теперь я поменяю значение Hello на Hell в файле index.html и сделаю деплой используя файл docker-stack-unhealthy.yml, потому что удалять конфиг docker нельзя если его кто-то использует.
vim html/index.html
docker stack deploy -c docker-stack-unhealthy.yml nginx-stack
docker stack ps nginx-stack
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE
860d4wee98by nginx-stack_web.1 nginx:alpine c-stream-9-vm4 Shutdown Complete 15 hours ago
djvpuz2ls9f3 \_ nginx-stack_web.1 nginx:alpine c-stream-9-vm4 Shutdown Shutdown 15 hours ago
ltcr3vfwz5jm nginx-stack_web.2 nginx:alpine c-stream-9-vm9 Running Running 15 hours ago
docker stack services nginx-stack
jo8itz0b9lou nginx-stack_web replicated 1/2 nginx:alpine *:8080->80/tcp
Как видно из вывода выше обновить Docker попытался только один task и его контейнер. Но так как статус контейнера этого task не Healthy он бросил это дело и оставил один task без изменений, т.е. со старой версией. И это хорошо ведь в итоге сервис всё же работает и отдаёт нормальную страницу с текстом Hello. Но можно сделать еще лучше, используя failure_action: rollback в update_config (файл docker-stack-rollback.yml).
Но сейчас есть одна проблема: у меня теперь живой только один task и мне перед обновлением с rollback нужно оживить второй иначе я просто добьюсь того что оба task будут выключены. Поэтому сперва оживляю существующее:
docker stack deploy -c docker-stack.yml nginx-stack
docker stack ps nginx-stack
55mpr1kzdwm4 nginx-stack_web.1 nginx:alpine c-stream-9-vm4 Running Starting 5 seconds ago
wzhycp0w3878 \_ nginx-stack_web.1 nginx:alpine c-stream-9-vm4 Shutdown Shutdown 5 seconds ago
0vmpokkln8xi nginx-stack_web.2 nginx:alpine c-stream-9-vm9 Running Running 20 seconds ago
bss08mcndzwa \_ nginx-stack_web.2 nginx:alpine c-stream-9-vm9 Shutdown Complete 40 seconds ago
vn08vas0y1ic \_ nginx-stack_web.2 nginx:alpine c-stream-9-vm9 Shutdown Complete about a minute ago
n0c84gcrvzbi \_ nginx-stack_web.2 nginx:alpine c-stream-9-vm9 Shutdown Complete 3 minutes ago
1io00g1zp6ms \_ nginx-stack_web.2 nginx:alpine c-stream-9-vm9 Shutdown Shutdown 3 minutes ago
Теперь, когда у меня два task работают я могу использовать файл с rollback.
vim html/index.html
docker stack deploy -c docker-stack-rollback.yml --detach=false nginx-stack
Updating service nginx-stack_web (id: 3h9a3wj2p1iigwud3ahb62wbr)
overall progress: rolling back update: 2 out of 2 tasks
1/2: running [==================================================>]
2/2: running [==================================================>]
verify: Service 3h9a3wj2p1iigwud3ahb62wbr converged
rollback: rollback completed
Различия
docker-healthcheck-comparison.svg
Я рассмотрел три варианта использования Healthcheck, теперь давайте рассмотрим ключевые отличия.
| Функция | docker run | docker-compose | docker stack |
|---|---|---|---|
| Автоматический restart при unhealthy | ❌ Нет | ❌ Нет | ✅ Да |
| Load balancing исключает unhealthy | ❌ Нет LB | ❌ Нет LB | ✅ Да |
| Rolling updates учитывают health | ❌ Нет | ❌ Нет | ✅ Да |
| Видимость статуса (healthy/unhealthy) | docker ps |
docker-compose ps |
docker ps на нодах |
| Production-ready | ❌ Нет | ❌ Нет | ✅ Да |


Комментарии