Docker Healthchecks зроблені правильно — припиніть розгортати «зелені» відмови

Було корисно?

Найдорожча відмова — та, що виглядає здоровою. Панелі моніторингу зелені, деплої виглядають «успішними»,
а клієнти тим часом стикаються з таймаутами, бо контейнер запущений, але сервіс не працює.
Docker охоче показує running. Ваш оркестратор знизав плечима. Вас усе одно викликають по PagerDuty.

Healthchecks мають бути детектором брехні. Занадто часто це просто наклейка на лобі з написом «OK»,
бо хтось один раз виконав curl localhost і вважав справу зробленою. Давайте виправимо це — перевірками, які відповідають
реальним режимам відмов, не створюють нових проблем і справді допомагають приймати рішення під тиском.

Що таке Docker healthchecks (і що ні)

Docker healthcheck — це команда, яку Docker запускає всередині контейнера за розкладом.
Команда повертає код виходу 0 для здорового стану, ненульовий — для нездорового. Docker відстежує стан:
starting, healthy або unhealthy.

Ось і все. Ніякої магії. Ніякого розподіленого консенсусу. Ніякої гарантії того, що додаток «добрий для користувачів».
Це локальна перевірка. Корисна, але лише якщо ви цілите її в правильну ціль.

Що Docker healthcheck робить добре

  • Виявляє дедлоки та завислі процеси, які все ще мають PID.
  • Виявляє локальні відмови залежностей (сокет бази даних не приймає підключення, збій автентифікації кешу).
  • Регулює послідовність старту у Docker Compose, щоб уникнути штурму бази даних.
  • Надає сигнал оркестраторам і людям: «цей контейнер вам брешe».

Чого він не може зробити (перестаньте просити)

  • Довести успіх кінцевого користувача. Локальна перевірка не може валідувати DNS, маршрутизацію, зовнішні ACL чи поведінку клієнта.
  • Замінити метрики. «Healthy» — це булева ознака. Латентність, насичення і бюджет помилок — ні.
  • Виправити погані роллаути. Якщо ваша перевірка помилкова, вона з упевненістю сертифікує неправильне.

Операційна істина: healthcheck — це контракт. Ви визначаєте «достатньо здоровий, щоб приймати трафік»
або «достатньо здоровий, щоб продовжувати виконання». Пишіть цей контракт так, ніби від нього залежить ваш пейджер — бо так воно і є.

Цікаві факти та контекст (те, що пояснює сучасний безлад)

  • Docker додав HEALTHCHECK у 2015 році (ера Docker 1.12), переважно для підтримки оркестраційних патернів до домінування Swarm/Kubernetes.
  • Healthchecks запускаються в просторі імен контейнера, тому вони бачать DNS контейнера, локальні сокети і localhost відрізняються від хоста.
  • Docker зберігає стан здоров’я в метаданих контейнера і показує його через docker inspect; це не рядок логу, якщо ви його явно не шукаєте.
  • Початковий depends_on в Compose не чекав на готовність; пізніші версії додали умовні залежності на основі здоров’я, але багато стеків досі працюють за старою ментальною моделлю.
  • Kubernetes популяризував окремі “liveness” і “readiness”, що змусило людей усвідомити: один endpoint часто змішує дві несумісні цілі.
  • Ранні “health endpoints” часто просто повертали “200 OK”, бо балансувальники потребували лише heartbeat; сучасні системи потребують readiness з урахуванням залежностей і швидкого фейлу.
  • cgroups і CPU-throttling можуть зробити здоровий додаток схожим на мертвий; таймаут 1s під CPU-навантаженням — це фактично підкидання монети.
  • Політики перезапуску передували healthchecks у багатьох установках; оператори зв’язали перевірки з перезапусками і випадково створили самозаподіяну DoS-петлю.

Режими відмов, які приховують «зелені» деплої

1) Процес живий; сервіс мертвий

Класика: ваш головний процес все ще працює, але завис. Дедлок, нескінченна GC, очікування на зламану залежність,
блокування диску або приставання на м’ютексі. Docker показує «Up». Ваш реверс-проксі таймаутить.

Хороший healthcheck тестує поведінку сервісу, а не наявність PID.

2) Порт відкритий, додаток не готовий

TCP-сокети на прослуховуванні піднімаються рано. Фреймворки так роблять. Ваш додаток ще розігріває кеші, виконує міграції,
завантажує моделі або чекає на базу даних.

Перевірка порту — це сигнал щось на кшталт liveness. Readiness вимагає підтвердження на рівні застосунку.

3) Часткова відмова залежності

Ваш сервіс може відповісти на /health, але не може спілкуватися з Redis через невідповідність автентифікації,
не може записати в Postgres через зміну прав або не може дістатися зовнішнього API через зміну правил egress.

Якщо ви не включите перевірки залежностей, ви задеплоїте прекрасну «зелену» відмову.

4) Повільна відмова: працює, але не вчасно

Під навантаженням або через шумних сусідів латентність прориває таймаути клієнтів. Ваш health endpoint все ще повертає 200 — але з запізненням.
Тим часом користувачі бачать помилки.

Ваш healthcheck повинен мати бюджет латентності і примусово дотримуватися його таймаутами. «Здоровий через 30 секунд» — це просто оптимізм.

5) Перевірка сама викликає відмову

Перевірки, які звертаються до дорогих endpoint-ів, запускають міграції або відкривають нові DB-з’єднання щосекунди, — чудовий спосіб
знищити систему і одночасно потішитися з «доданої надійності».

Жарт #1: Healthcheck, що DDoS-ить вашу власну базу даних, технічно все ще «тестує продакшен». Просто це не той тип тестування, який вам потрібен.

Принципи дизайну: перевірки, що говорять правду

Визначте, що означає «здоровий» в операційному сенсі

Виберіть одне з наведеного і будьте конкретні:

  • Готовий приймати трафік: може обслуговувати реальні запити в межах SLO-подібної латентності і має потрібні залежності.
  • Безпечний для продовження роботи: процес не завислий; він може просуватися вперед; може коректно завершитися.

Docker дає один стан здоров’я. Це незручно. Ви все ще можете змоделювати обидва сенси, вибравши те, що вам важливіше для конкретного контейнера.
Для edge-проксі важлива «готовність до трафіку». Для фонового воркера важливіше «може продовжувати роботу».

Fail fast, але не дурно

Хороша перевірка швидко фейлиться, коли контейнер справді зламався, але не флапає під час нормального старту
або тимчасових проблем залежностей.

  • Використовуйте start_period, щоб не карати повільний розігрів.
  • Використовуйте таймаути, щоб завислий виклик не блокував перевірку назавжди.
  • Використовуйте повторні спроби, щоб одна втрата пакета не перетворилася на бурю перезапусків.

Віддавайте перевагу локальним, дешевим, детермінованим пробам

Healthchecks запускаються часто. Зробіть їх такими:

  • Локальні: звертайтесь до localhost, UNIX-сокета або внутрішнього стану процесу.
  • Дешеві: уникайте важких запитів, уникайте створення нових пулів з’єднань.
  • Детерміновані: однаковий ввід — однаковий вивід; без випадковості; без «іноді повільно».

Включайте залежності, але вибирайте глибину правильно

Якщо сервіс не може працювати без Postgres, ваш чек має підтверджувати, що він може пройти аутентифікацію і виконати тривіальний запит.
Якщо сервіс може деградуватися Gracefully (подавати кешований контент), не позначайте контейнер як нездоровий тільки через падіння Redis.

Операційно: ваш healthcheck має відображати очікувану поведінку при відмові.

Використовуйте коди виходу свідомо

Docker дивиться лише на нуль проти ненульового коду, але людям важливо знати чому. Нехай ваша перевірка виводить коротку причину
в stderr/stdout перед ненульовим виходом. Ця причина з’явиться в docker inspect.

Змушуйте перевірки тестувати той самий шлях, яким йде ваш трафік

Якщо реальний трафік йде через Nginx до вашого додатка, перевірка додатка напряму може пропустити клас відмов:
зламана конфігурація Nginx, вичерпання worker-з’єднань, поганий upstream DNS, проблеми з TLS. Інколи ви хочете, щоб проксі перевіряв свій upstream.
Інколи зовнішній LB має перевіряти проксі. Накладайте перевірки так само, як накладаєте шари відмов.

Цитата (парафраз) — Jim Gray: «Ставтеся до системних відмов як до нормального явища; проектуйте так, ніби компоненти можуть впасти у будь-який момент».

План швидкої діагностики: знайдіть вузьке місце швидко

Коли контейнер «здоровий», але користувачі отримують помилки, вам не потрібна філософія. Потрібна послідовність.
Ось порядок, який найчастіше найшвидше виявляє реальне обмеження.

Перш за все: сигнал здоров’я бреше, чи система змінилася?

  • Перевірте статус здоров’я Docker і останні логи health для цього контейнера.
  • Підтвердіть, що ваша перевірка тестує правильну річ (залежність, шлях, латентність).
  • Порівняйте конфіг до/після деплою на предмет змін у healthcheck, таймаутах і start_period.

По-друге: сервіс дійсно обслуговує на очікуваному інтерфейсі?

  • Всередині контейнера: перевірте прослуховувані порти, DNS і локальну з’єднаність.
  • З хоста: перевірте порт-мапінг і правила брандмауера.
  • З сусіднього контейнера: перевірте service discovery і еквіваленти політик мережі.

По-третє: чи заблоковані ми на CPU, пам’яті, диску або залежності?

  • CPU-throttling і стрибки load average можуть перетворити обробник 200ms у таймаут 5s.
  • OOM-кілли можуть створити цикли «працює… поки не впаде».
  • Насичення диску може заморозити I/O-інтенсивні сервіси, тоді як процес лишається живим.
  • Насичення залежності (максимум підключень DB) часто виглядає як випадкові таймаути в додатку.

По-четверте: чи викликає шкоду сама перевірка?

  • Перевірте частоту і вартість: чи лупить вона DB або пул потоків додатка?
  • Перевірте конкурентність: перевірки не повинні накопичуватися.
  • Перевірте побічні ефекти: health endpoints мають бути тільки для читання.

Фішка: ставтеся до healthchecks як до будь-якого іншого генератора навантаження в продакшені. Тому що вони ним і є.

Практичні задачі (команди, очікуваний вивід та що вирішуєте)

Це реальні операційні кроки. Кожна задача включає: команду, що означає вивід, і рішення, яке ви приймаєте.
Використовуйте їх під час інцидентів і в спокійний час інженерії, коли ви хочете попередити наступний.

Задача 1: Побачити статус здоров’я з першого погляду

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
NAMES           STATUS                          IMAGE
api-1           Up 12 minutes (healthy)         myorg/api:1.9.3
db-1            Up 12 minutes (healthy)         postgres:16
worker-1        Up 12 minutes (unhealthy)       myorg/worker:1.9.3

Що означає: Docker запускає healthchecks і повідомляє стан. (unhealthy) — це не натяк.

Рішення: Якщо критичний компонент нездоровий, перестаньте вважати «Up» за успіх. Розбирайтесь, перш ніж масштабувати або направляти трафік.

Задача 2: Інспект лог здоров’я і причину помилки

cr0x@server:~$ docker inspect --format '{{json .State.Health}}' worker-1 | jq
{
  "Status": "unhealthy",
  "FailingStreak": 5,
  "Log": [
    {
      "Start": "2026-01-02T08:11:12.123456789Z",
      "End": "2026-01-02T08:11:12.223456789Z",
      "ExitCode": 1,
      "Output": "redis ping failed: NOAUTH Authentication required\n"
    }
  ]
}

Що означає: Перевірка послідовно фейлиться і друкує корисну причину. Хвала тому, хто написав цей вивід.

Рішення: Виправляйте креденшали/конфіг замість сліпого перезапуску. Також розгляньте, чи повинен збій автентифікації Redis позначати воркер як нездоровий або як деградований.

Задача 3: Запустіть команду healthcheck вручну всередині контейнера

cr0x@server:~$ docker exec -it api-1 sh -lc 'echo $0; /usr/local/bin/healthcheck.sh; echo exit=$?'
sh
ok: http=200 db=ok redis=ok latency_ms=27
exit=0

Що означає: Ви виконуєте ту ж пробу, яку виконує Docker. Вона пройшла і повернулася швидко.

Рішення: Якщо користувачі все ще терплять невдачі, проблема, ймовірно, поза межами локального вигляду контейнера (мережа, проксі, LB, DNS) або є невідповідність між критеріями здоров’я і шляхом користувача.

Задача 4: Підтвердіть, яку саме перевірку Docker запускає

cr0x@server:~$ docker inspect --format '{{json .Config.Healthcheck}}' api-1 | jq
{
  "Test": [
    "CMD-SHELL",
    "/usr/local/bin/healthcheck.sh"
  ],
  "Interval": 30000000000,
  "Timeout": 2000000000,
  "StartPeriod": 15000000000,
  "Retries": 3
}

Що означає: Interval 30s, timeout 2s, start period 15s, retries 3. Ці числа — ваша поведінка.

Рішення: Якщо ви бачите timeout=1s для JVM-сервісу під CPU-лімітами, ви знайшли майбутній інцидент. Налаштуйте зараз.

Задача 5: Спостерігайте переходи здоров’я в реальному часі

cr0x@server:~$ docker events --filter container=api-1 --filter event=health_status --since 10m
2026-01-02T08:03:12.000000000Z container health_status: healthy api-1
2026-01-02T08:08:42.000000000Z container health_status: unhealthy api-1
2026-01-02T08:09:12.000000000Z container health_status: healthy api-1

Що означає: Флапінг: він стає нездоровим, потім відновлюється. Це або справжня періодична проблема, або дуже чутлива перевірка.

Рішення: Якщо флапінг корелює з навантаженням, швидше за все у вас насичення ресурсів. Якщо випадковий — збільшіть timeout/retries і розберіться з мережею/DNS.

Задача 6: Перевірте DNS і маршрутизацію контейнер→контейнер

cr0x@server:~$ docker exec -it api-1 sh -lc 'getent hosts db && nc -zvw2 db 5432'
172.20.0.3 db
db (172.20.0.3:5432) open

Що означає: DNS резольвиться і TCP-підключення працює з контейнера додатка до контейнера бази даних.

Рішення: Якщо це фейлиться, не витрачайте час в додатку. Виправляйте мережу, ім’я сервісу або конфіг Compose мережі.

Задача 7: Доведіть, що додаток прослуховує там, де ви думаєте

cr0x@server:~$ docker exec -it api-1 sh -lc 'ss -lntp | head'
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN 0      4096   0.0.0.0:8080       0.0.0.0:*     users:(("java",pid=1,fd=123))

Що означає: Сервіс слухає на 0.0.0.0:8080. Якби він слушав лише на 127.0.0.1, ваш порт-мапінг міг бути «up», але недоступним зовні.

Рішення: Якщо бінд неправильно налаштований, виправте адресу прив’язки в додатку. Не маскуйте це host networking, якщо вам не подобається потім шкодувати.

Задача 8: Протестуйте реальний шлях запиту з хоста (порт-мапінг)

cr0x@server:~$ curl -fsS -m 2 -D- http://127.0.0.1:18080/healthz
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 2

ok

Що означає: Через опублікований порт health endpoint відповідає в межах 2 секунд.

Рішення: Якщо host curl фейлиться, а in-container curl працює, проблема в мапінгу, брандмауері, проксі або біндінгу додатка до неправильного інтерфейсу.

Задача 9: Виявити CPU-throttling, що робить перевірки таймаутними

cr0x@server:~$ docker stats --no-stream api-1
CONTAINER ID   NAME    CPU %     MEM USAGE / LIMIT     MEM %     NET I/O       BLOCK I/O     PIDS
a1b2c3d4e5f6   api-1   198.32%   1.2GiB / 1.5GiB       80.12%    1.3GB / 1.1GB  25MB / 2MB   93

Що означає: Високе використання CPU і пам’яті. Якщо ви також встановили жорсткі таймаути для healthchecks, ви отримаєте хибні негативи під навантаженням.

Рішення: Або виділіть більше CPU/пам’яті, зменшіть роботу, або розширте таймаут health, щоб «здоровий при очікуваному навантаженні» залишався істинним.

Задача 10: Перевірте OOM-кілли або рестарти, що маскують «нестабільність»

cr0x@server:~$ docker inspect --format 'RestartCount={{.RestartCount}} OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}' api-1
RestartCount=2 OOMKilled=true ExitCode=137

Що означає: Exit code 137 і OOMKilled=true: ядро вбило процес. Ваш healthcheck не фейлив; контейнер помер.

Рішення: Виправте межі пам’яті, витоки або раптові сплески. Також переконайтесь, що ваш стартовий start_period не надто короткий, інакше ви накопичите збої під час відновлення.

Задача 11: Виявити блокування дискових I/O, що заморожують «здорові» процеси

cr0x@server:~$ docker exec -it db-1 sh -lc 'ps -o stat,comm,pid | head'
STAT COMMAND PID
Ss   postgres 1
Ds   postgres 72
Ds   postgres 73

Що означає: Процеси в стані D застрягли в неперервному очікуванні I/O. Вони не відповідатимуть на ваші чемні запити.

Рішення: Припиніть звинувачувати додаток. Дослідіть латентність сховища, насичення диска, «шумних сусідів» або драйвери томів.

Задача 12: Підтвердити готовність залежності спеціалізованою пробою (Postgres)

cr0x@server:~$ docker exec -it db-1 sh -lc 'pg_isready -U postgres -h 127.0.0.1 -p 5432; echo exit=$?'
127.0.0.1:5432 - accepting connections
exit=0

Що означає: Postgres приймає підключення. Це краща перевірка, ніж просто відкритий порт, бо вона говорить мовою протоколу.

Рішення: Якщо це переривчасто фейлиться, дивіться на max connections, checkpoints, диск або процес відновлення. Не просто збільшуйте retries health і не чекайте чуда.

Задача 13: Переконайтесь, що ваш health endpoint достатньо швидкий (бюджет латентності)

cr0x@server:~$ docker exec -it api-1 sh -lc 'time -p curl -fsS -m 1 http://127.0.0.1:8080/healthz >/dev/null'
real 0.04
user 0.00
sys 0.00

Що означає: 40ms локально. Чудово. Якщо ви бачите 0.9–1.0s з таймаутом 1s, ви живете на краю.

Рішення: Встановіть таймаут з запасом. Healthchecks мають фейлитися при реальній повільності, а не через нормальні коливання під навантаженням.

Задача 14: Виловити хибні припущення про «localhost» (проксі vs додаток)

cr0x@server:~$ docker exec -it nginx-1 sh -lc 'curl -fsS -m 1 http://127.0.0.1:8080/healthz || echo "upstream unreachable"'
upstream unreachable

Що означає: Всередині контейнера Nginx localhost — це Nginx, а не ваш додаток. Це одна з топ-10 причин марних healthchecks.

Рішення: Напрямляйте перевірки на правильне upstream-ім’я/сервіс, або запускайте перевірку в правильному контейнері. «Працює на моєму контейнері» — це не стратегія мережі.

Шаблони healthcheck для поширених сервісів

Шаблон A: HTTP-сервіс з readiness, що враховує залежності

Для API добра перевірка зазвичай підтверджує:
що HTTP-потік відповідає швидко, і основні залежності досяжні та автентифіковані.
Тримайте її мінімальною: один крихітний запит, один ping кешу, неглибока внутрішня перевірка стану.

Приклад: Dockerfile healthcheck, що викликає скрипт

cr0x@server:~$ cat Dockerfile
FROM alpine:3.20
RUN apk add --no-cache curl ca-certificates
COPY healthcheck.sh /usr/local/bin/healthcheck.sh
RUN chmod +x /usr/local/bin/healthcheck.sh
HEALTHCHECK --interval=30s --timeout=2s --start-period=20s --retries=3 CMD ["/usr/local/bin/healthcheck.sh"]
cr0x@server:~$ cat healthcheck.sh
#!/bin/sh
set -eu

t0=$(date +%s%3N)

# Fast local HTTP check (service path)
code=$(curl -fsS -m 1 -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/healthz || true)
if [ "$code" != "200" ]; then
  echo "http failed: code=$code"
  exit 1
fi

# Optional dependency: DB shallow check via app endpoint (preferred) or direct driver probe
# Here we assume /readyz includes db connectivity check inside the app.
code2=$(curl -fsS -m 1 -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/readyz || true)
if [ "$code2" != "200" ]; then
  echo "ready failed: code=$code2"
  exit 1
fi

t1=$(date +%s%3N)
lat=$((t1 - t0))
echo "ok: http=200 ready=200 latency_ms=$lat"
exit 0

Чому це працює: Перевірка нав’язує бюджет часу і валідує реальний інтерфейс сервісу.
Вона уникає глибокої логіки залежностей у shell, коли додаток може зробити це краще (і повторно використати існуючі пули).

Шаблон B: Контейнери баз даних — використовуйте нативні інструменти, не «порт відкритий»

Якщо ви перевіряєте Postgres — використовуйте pg_isready. Для MySQL — mysqladmin ping.
Для Redis — redis-cli ping. Ці проби «розмовляють» протоколом достатньо, щоб бути значущими.

Приклад: Postgres у Compose

cr0x@server:~$ cat compose.yaml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -h 127.0.0.1 -p 5432"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s

Чому це працює: Це виявляє «процес запущений, але не приймає з’єднання», включаючи проблеми відновлення або міграцій.
Це недорого, бо не вимагає реального запиту.

Шаблон C: Фонові воркери — перевірки на просування

У воркерів часто немає HTTP. Ваша перевірка має підтверджувати:
процес може спілкуватися з чергою і не застряг.
Якщо можна записати heartbeat-файл або легкий таймштамп «остання оброблена задача», зробіть це.

Воркер, що може підключитись до Redis, але не обробляв жодної задачі 10 хвилин — це не здоровий, а просто онлайн.

Шаблон D: Реверс-проксі — перевіряйте upstream, а не себе

Nginx «здоровий», коли upstream впав, — марно, якщо проксі — це шлюз трафіку.
Або перевіряйте досяжність upstream, або налаштуйте балансувальник, щоб він перевіряв endpoint, який відображає стан upstream.

Жарт #2: Реверс-проксі, що відповідає 200, поки upstream горить, — як рецепціоніст, що каже «всі на нараді» під час евакуації.

Compose-оркестрація, залежності та реальність старту

Docker Compose — це місце, де healthchecks або рятують вас, або роблять вам надмірну впевненість.
Пастка: люди вважають, що depends_on означає «чекати до готовності». Історично воно означало «стартап в порядку».
Це не те саме, і ви можете здогадатися, що це означає для вашої бази даних.

Використовуйте залежності на основі здоров’я там, де це справді має значення

Якщо API буде падати в цикл перезапусків, поки БД не піднялась, ви можете заґейтити старт API на здоров’я DB у Compose.
Це зменшить шум і уникне штурму при завантаженні.

Приклад: Залежність у Compose на основі здоров’я

cr0x@server:~$ cat compose.yaml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -h 127.0.0.1 -p 5432"]
      interval: 5s
      timeout: 3s
      retries: 12
      start_period: 10s

  api:
    image: myorg/api:1.9.3
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS -m 1 http://127.0.0.1:8080/readyz || exit 1"]
      interval: 10s
      timeout: 2s
      retries: 3
      start_period: 30s

Перевірка реальності: Це покращує порядок старту, але не довготривалу надійність. Якщо DB впаде пізніше, Compose не почне магічний грамотний failover.
Але це зупиняє «все стартує одночасно — все падає одночасно» під час завантаження.

Не дозволяйте healthchecks перетворюватися на шлюзи деплою, які ви не можете зрозуміти

Якщо ви зробите readiness залежним від кожної зовнішньої залежності, один непевний upstream може блокувати деплої та ролбеки.
Це не стійкість; це зв’язування. Визначте, що потрібно саме вашому сервісу, щоб обслуговувати трафік.

Відокремте розігрів старту від здоров’я в стаціонарному стані

Старт — особливий. Міграції виконуються. JIT-компілятори прокидаються. Сертифікати завантажуються. DNS-кеші холодні.
Саме тому існує start_period: щоб уникнути перезапусків і «нездорових» станів під час очікуваного розігріву.

Але не зловживайте ним. Start period у 10 хвилин ховатиме справжні відмови протягом 10 хвилин. Якщо вам це потрібно — ймовірно, потрібен кращий дизайн ініціалізації.

Перезапуски, healthchecks і «спіраль смерті»

Docker healthchecks самі по собі не перезапускають контейнер. Інше робить це: ваш оркестратор, скрипти, супервізори
або «розумна» автоматизація, що інтерпретує unhealthy як «перезапустити зараз».

Саме тут добрі наміри йдуть у нікуди.

Класична спіраль смерті

  • Залежність сповільнюється (стрибок латентності DB).
  • Healthcheck таймаутиться (занадто агресивний).
  • Автоматизація перезапускає контейнери.
  • Хвиля перезапусків підвищує навантаження (холодні кеші, перепідключення, міграції, реплеї).
  • Залежність сповільнюється ще більше.

Як уникнути цього

  • Healthchecks мають бути спочатку діагностичними. Перезапуск — останній засіб, а не рефлекс.
  • Використовуйте backoff в системі, що приймає рішення про перезапуск.
  • Зробіть перевірку дешевою і обмеженою по часу, та налаштуйте таймаути під очікувані флуктуації.
  • Проектуйте деградований режим: якщо Redis недоступний, чи можна обслуговувати readonly? Тоді не позначайте readiness за його відсутності.

Healthcheck, що викликає перезапуски, — це заряджена зброя. Зберігайте її відповідально.

Healthchecks vs моніторинг vs метрики: перестаньте плутати

Healthcheck — це бінарна локальна проба. Моніторинг — це система, що каже вам, коли ви порушуєте цілі.
Метрики — дані, що пояснюють чому.

Коли використовувати healthchecks

  • Щоб не направляти трафік до контейнерів, які не можуть обробити його.
  • Щоб уникнути старту залежних сервісів надто рано.
  • Щоб швидко і послідовно виявляти очевидні зламані стани.

Коли не слід використовувати healthchecks

  • Як заміну перцентилям латентності і показникам помилок.
  • Як єдиний сигнал для перезапуску. Так ви створите елегантні ампліфікатори відмов.
  • Щоб «перевірити весь світ» з одного контейнера. Це для інтеграційних тестів і синтетичного моніторингу.

Дворівневий патерн: внутрішній health + зовнішні синтетичні перевірки

Надійний патерн багаторівневий:

  • Внутрішній healthcheck: дешевий, швидкий, достатньо aware про залежності для локальної коректності.
  • Зовнішня синтетична перевірка: зачіпає реальний публічний шлях, тестує DNS/TLS/маршрутизацію і вимірює латентність.

Це ловить два великі класи брехні: «контейнер у порядку, але шлях зламався» і «шлях у порядку, але контейнер помер».

Три корпоративні міні-історії з передової

Міні-історія 1: Інцидент через неправильне припущення

Компанія запускала невелику внутрішню платформу: контейнер реверс-проксі перед кількома API-контейнерами.
Вони додали Docker healthcheck до проксі, який робив curl http://127.0.0.1/ і повертав 0 при 200.
Проксі завжди відповідав 200, навіть коли upstream був down, бо корінний шлях віддавав статичну «welcome» сторінку.

Під час рутинного деплою один upstream не стартував через відсутню змінну середовища.
Проксі продовжував повертати 200 балансувальнику. Трафік ішов. Користувачі отримували дружній 502 і таймаут.
Моніторинг показував «proxy healthy». Пайплайн деплою показував «всі контейнери запущені». Всі дивилися на графіки у невірі,
ніби невіра може повернути сервіс.

Неправильне припущення було тонким і людським: «Якщо проксі up, то сервіс up».
Але для клієнтів проксі — це лише вхідні двері. Якщо кімната за ним у вогні, відкриті двері — це не успіх.

Вони виправили це, визначивши два endpoint-и:
/healthz для «процес проксі живий», і /readyz для «критичні upstream досяжні і повертають очікуваний статус».
Балансувальник перевіряв /readyz. Docker healthcheck проксі теж перевіряв /readyz,
з розумними таймаутами, щоб уникнути каскадування відмов, коли upstream повільний.

Негайний результат — менше драм: неконфігуровані upstream-и більше не ставали чорними провалами трафіку.
Довготерміновий результат — культурний: люди перестали вживати «контейнер запущений» як синонім «сервіс працює».

Міні-історія 2: Оптимізація, що відпалилася боком

Інша організація хотіла «швидшого виявлення» поломок контейнерів. Вони загострили healthchecks:
interval 2 секунди, timeout 200ms, retries 1. Вони вітали себе за серйозний підхід до надійності.
В тихий день все виглядало добре.

Потім настала нормальна пікова хвиля. CPU-throttling увімкнувся, бо контейнери були обмежені ресурсами, щоб зменшити витрати.
Пауза GC виросла. Латентність диска трохи підскочила через фонові знімки на хості.
Запити все ще проходили, але інколи за 400–700ms замість 80ms. Healthchecks таймаутились.

Їхня автоматизація трактувала «unhealthy» як «перезапустити негайно». Перезапуск викликав cache-miss, що підключав більше DB,
що збільшувало латентність, що викликало нові фейли healthcheck. Скоро в них була синхронізована парадна перезавантажень.
Системи спочатку не були «впали», але політика здоров’я зробила їх впалими.

Вони відкотили агресивність healthcheck і додали backoff до перезапусків.
Також вони розділили перевірки: швидка liveness-перевірка для «процес відповідає» і більш поблажлива readiness-перевірка
з довшим таймаутом і кількома спробами.

Урок не в тому, щоб «ніколи не оптимізувати». Він у тому, щоб «оптимізувати правильну річ».
Швидше виявлення марне, якщо ваш детектор більш крихкий, ніж сервіс, який він має захищати.

Міні-історія 3: Нудна, але правильна практика, що врятувала день

Третя команда запускала stateful-сервіси з персистентними томами. Вони мали звичку, яку ніхто не святкував:
кожен сервіс мав задокументований контракт здоров’я, а команда healthcheck друкувала однорядковий статус
з часовою міткою і ключовими станами залежностей.

Одного ранку частина контейнерів стала нездоровою після вікна обслуговування хоста.
Додатки були up, але healthchecks показували db=ok і disk=slow, з латентністю в мілісекундах.
Цей «disk=slow» не був геніальним. Це був простий поріг: записати тимчасовий файл і виміряти, скільки часу займе.

On-call не довелося гадати. Вони перевірили статі хоста диска, знайшли, що один бекенд томів працює некоректно,
і відключили проблемний хост. Трафік перескочив. Помилки впали. Ніяких героїчних дебагів всередині додатку о 3:00 ранку,
що завжди є справжньою перемогою.

Пізніше вони налаштували healthcheck так, щоб повільність диска не одразу позначала контейнери як нездорові, якщо це не тривало кілька інтервалів.
Нудна практика — консистентний вивід здоров’я і задокументований контракт — перетворила невизначений інцидент у чітке дерево рішень.

Ось як виглядає «операційна досконалість»: здебільшого неефектна, але іноді рятівна.

Поширені помилки (симптом → причина → виправлення)

1) Симптом: Контейнер «здоровий», але користувачі отримують 502/504

Причина: Healthcheck перевіряє лише локальний процес або статичну сторінку, а не upstream-залежності чи реальний шлях запиту.

Виправлення: Перевіряйте той самий шлях, що і реальний трафік (proxy→upstream), або відкривайте /readyz, який валідовує критичні залежності з бюджетом часу.

2) Симптом: Контейнери флапають між healthy/unhealthy під навантаженням

Причина: Таймаут занадто малий, healthcheck конкурує з реальним трафіком за CPU/потоки, або стрибки латентності залежності.

Виправлення: Збільште таймаут, додайте retries, використайте start_period і зробіть перевірку дешевшою. Перевірте CPU-throttling та насичення пулу потоків.

3) Симптом: Деплойти зависають, бо сервіси ніколи не стають здоровими

Причина: Healthcheck вимагає зовнішніх залежностей, які опціональні, або залежить від довгих міграцій, що тривають довше за start_period.

Виправлення: Звузьте readiness до «може безпечно обслуговувати трафік» (не «весь світ ідеальний»), і перенесіть довгі міграції в одноразову задачу.

4) Симптом: DB заганяють кожні кілька секунд, навіть коли проста стагнація

Причина: Healthcheck виконує важкі запити або відкриває нові DB-з’єднання кожного інтервалу; в множині реплік це стає реальним навантаженням.

Виправлення: Використовуйте нативні readiness-інструменти (pg_isready), неглибокі запити або перевірки на рівні додатка, що переиспользують пули з’єднань. Збільшіть інтервал.

5) Симптом: Healthcheck проходить локально, але фейлиться тільки в продакшені

Причина: Припущення про localhost, DNS, сертифікати, заголовки проксі або специфічна для середовища автентифікація.

Виправлення: Запустіть перевірку в тому самому мережевому просторі і контейнері, де вона виконуватиметься. Перевірте discovery сервісів з пірами, а не лише з вашого ноутбука.

6) Симптом: Unhealthy викликає перезапуски, перезапуски роблять все гірше

Причина: Автоматизація перезапуску без backoff + надто строгий чек + підсилення холодного старту.

Виправлення: Додайте backoff, розширте пороги, розділіть семантику liveness і readiness, і уникайте перезапусків через транзитивні затримки залежностей.

7) Симптом: Healthcheck сам викликає стрибки латентності

Причина: Чек звертається до дорогих endpoint-ів (весь граф залежностей, відновлення кешу, handshake автентифікації) занадто часто.

Виправлення: Зробіть спеціальний дешевий endpoint, кешуйте результати здоров’я у додатку коротко, і переконайтесь, що перевірка не має побічних ефектів.

8) Симптом: Healthcheck завжди повертає healthy, навіть коли додаток завис

Причина: Перевірка лише «порт відкритий» або «процес існує», або викликає обробник, що не зачіпає завислу підсистему.

Виправлення: Додайте тривіальну операцію, що вимагає прогресу (наприклад, enqueue/dequeue noop, виконати маленький DB-запит або перевірити тик event-loop).

Контрольні списки / покроковий план

Крок за кроком: напишіть healthcheck, що не поставить вас в дурне становище

  1. Опишіть контракт: «Здоровий означає X. Нездоровий означає Y.» Зберігайте це в репозиторії поруч із Dockerfile.
  2. Виберіть ціль: liveness-ish (прогрес) або readiness-ish (безпечний для трафіку). Не вдавайтеся, що одна булева змінна робить обидва ідеально.
  3. Обирайте бюджет латентності: виберіть таймаут, що відображає реальні умови плюс запас (не ваш laptop на Wi‑Fi).
  4. Start period: встановіть start_period на основі виміряного часу старту, а не надії.
  5. Retries: налаштуйте retries так, щоб терпіти транзитивні коливання, але не ховати постійну відмову.
  6. Зробіть її дешевою: уникайте важких endpoint-ів і штормів підключень. Віддавайте перевагу pooled перевіркам у додатку або нативним інструментам.
  7. Зробіть її спостережною: друкуйте однорядкову причину при помилці і короткий підсумок при успіху.
  8. Тестуйте під стресом: запустіть healthchecks під обмеженням CPU і I/O; подивіться, чи флапають вони.
  9. Вирішіть політику перезапуску: хто перезапускає при unhealthy, з яким backoff? Задокументуйте. Реалізуйте свідомо.
  10. Регулярно переглядайте: healthchecks зношуються із змінами систем. Додавайте їх у список рев’ю «production correctness».

Контрольний список: переддеплойна санітизація для Compose-стеків

  • Чи використовують залежності condition: service_healthy там, де це доречно?
  • Чи healthchecks використовують правильні імена хостів (service names), а не 127.0.0.1 між контейнерами?
  • Чи інтервали розумні (не 1–2s для десятків реплік, якщо перевірка не справді тривіальна)?
  • Чи таймаути і retries налаштовані під очікуване навантаження і коливання?
  • Чи перевірки уникають побічних ефектів і важких запитів?
  • Чи перевірки фейляться для тих режимів відмов, які вам справді важливі?

Контрольний список: реакція на інцидент, коли healthchecks задіяні

  • Чи healthcheck фейлиться через те, що сервіс зламався, чи через те, що чек надто строгий?
  • Чи автоматизація перезапуску підсилює проблему?
  • Чи логи здоров’я показують специфічну помилку залежності (автентифікація, DNS, таймаут)?
  • Чи вузьке місце — CPU, пам’ять, диск чи насичення upstream?
  • Чи можна деградуватися замість жорсткого фейлу?

FAQ

1) Чи має кожен контейнер мати healthcheck?

Ні. Додавайте healthchecks там, де вони запускають рішення: маршрутизація, залежності або швидка діагностика.
Для одноразових задач або тривіальних stateless sidecar-ів healthcheck може бути шумом. Не робіть це за шаблоном.

2) У чому різниця між «liveness» і «readiness» в Docker?

Docker експонує лише один стан здоров’я, але ви можете обирати семантику.
«Liveness» — це «процес може робити прогрес». «Readiness» — це «безпечно приймати трафік».
Для фронтендів і API пріоритет — readiness. Для воркерів — пріоритет прогресу.

3) Чому не просто перевіряти, що порт відкритий?

Бо порт може бути відкритий, коли додаток зламаний: закликані хендлери зависли, пул потоків вичерпано,
залежності падають, або проксі віддає статичну сторінку. Перевірки «порт відкритий» ловлять лише найледачіші відмови.

4) Як часто я маю запускати healthcheck?

Почніть з 10–30 секунд для більшості сервісів. Частіші перевірки підвищують навантаження і ризик флапу.
Якщо вам потрібне виявлення менше секунди — зазвичай ви вирішуєте не ту проблему або використовуєте неправильний інструмент.

5) Які таймаути і retries варто використовувати?

Таймаути мають бути меншими за таймаути клієнтів і відображати очікувану латентність під навантаженням плюс запас.
Retries повинні терпіти транзитивні проблеми (1–3) без маскування постійної відмови. Вимірюйте старт і steady-state окремо.

6) Чи має healthcheck тестувати залежності, як бази даних і кеші?

Якщо сервіс не може функціонувати без них — так, неглибоко.
Якщо сервіс може деградуватися Gracefully, не фейльте readiness через опціональні залежності. Інакше часткова відмова перетвориться на тотальну.

7) Чому мій healthcheck проходить в одному контейнері, але фейлиться в іншому?

Прості речі: простори імен. 127.0.0.1 всередині контейнера — це той контейнер. DNS сервісів відрізняється між мережами.
Також сертифікати і автентифікація можуть бути специфічні для середовища. Завжди тестуйте з того ж мережевого шляху, де перевірка виконуватиметься.

8) Чи Docker перезапускає нездорові контейнери автоматично?

За замовчуванням — ні. Docker повідомляє стан здоров’я; перезапуски керуються політиками перезапуску (on exit) або зовнішньою автоматизацією.
Будьте дуже обережні, коли підключаєте «unhealthy» до перезапусків. Додайте backoff і уникайте хору перезапусків.

9) Чи має мій health endpoint повертати детальні діагностики?

Внутрішньо — так: коротка рядкова причина — золото під час інцидентів. Зовні — будьте обережні: не витікайте секрети чи топологію.
Багато команд виставляють мінімальний зовнішній /healthz і захищений внутрішній diagnostics endpoint.

10) Як не допустити, щоб healthchecks перевантажували базу даних?

Використовуйте нативні проби (pg_isready) або перевірки на рівні додатка, які повторно використовують connection pools.
Збільшуйте інтервал. Уникайте запитів, що сканують таблиці. Ваш healthcheck має бути дешевшим, ніж реальний запит.

Наступні кроки, які можна доставити цього тижня

Перестаньте дозволяти «зелений» означати «добре». Healthchecks — це контролі продакшену, а не декоративний YAML.
Якщо ваша перевірка не відповідає реальним режимам відмов, вона з впевненістю підтверджуватиме відмови.

  1. Аудит ваших топ-5 сервісів: що саме тестують їхні healthchecks і чи це той самий шлях, що й користувачі?
  2. Додайте один dependency-aware readiness сигнал там, де це має значення (API, proxy, gateway). Тримайте його неглибоким і обмеженим по часу.
  3. Налаштуйте таймаути і start_period на основі вимірюваного старту і поведінки під навантаженням, а не здогадок.
  4. Робіть вивід дієвим: однорядкові причини помилок, що з’являються в docker inspect.
  5. Перегляньте поведінку перезапуску: якщо «unhealthy» викликає перезапуски, додайте backoff і перевірте, що ви не будуєте спіраль смерті.

Мета не зробити healthchecks суворими. Мета — зробити їх чесними. Чесні перевірки не запобігають кожному інциденту.
Вони запобігають найгіршому: тому, про який ваші системи наполегливо стверджують, що його немає.

← Попередня
VPN-логи, що мають значення: знайти причини «не підключається» в логах MikroTik/Linux
Наступна →
Скидання SATA-з’єднання в ZFS: шаблон несправності контролера/кабеля

Залишити коментар