🐳 Раздел 14 · Вопрос #19

Зачем нужны health checks

В Kubernetes существует три вида проб (Health Probes):


🟢 Junior Level

30-секундный ответ

Health Checks (Проверки работоспособности / Пробы) — это встроенный диагностический комплекс Kubernetes, с помощью которого оркестратор непрерывно отслеживает физическое и логическое состояние контейнеров для реализации автономного самоисцеления (Self-Healing) и развёртывания без простоя (Zero Downtime).

В Kubernetes существует три вида проб (Health Probes):

  1. startupProbe (Проба запуска): отвечает на вопрос: «Приложение уже успело загрузиться?» Даёт медленным сервисам (например, тяжелым Java-монолитам) время на спокойный старт, временно отключая остальные проверки.
  2. livenessProbe (Проба живучести): отвечает на вопрос: «Процесс всё ещё жив или он завис?» Если проба падает, Kubernetes принудительно убивает и перезапускает контейнер.
  3. readinessProbe (Проба готовности): отвечает на вопрос: «Готов ли под принимать пользовательские запросы?» Если проба падает, контейнер НЕ перезапускается, но его IP временно удаляется из балансировщика нагрузки.

Наглядная аналогия из жизни

Представьте открытие новой кофейни:

  • startupProbe: вывеска «Идёт ремонт и завоз оборудования». Инспекторы ждут и не штрафуют кофейню за то, что кофе ещё не наливают.
  • livenessProbe: проверка «Бариста в сознании или упал в обморок?» Если бариста упал без чувств (Deadlock), менеджер вызывает замену (перезапуск).
  • readinessProbe: табличка на кассе «Перерыв 5 минут на помол зёрен». Бариста жив, но прямо сейчас покупателей не обслуживает (трафик перенаправляется на соседнюю кассу).

Сводный манифест со всеми тремя пробами (Java 21 / Spring Boot 3)

apiVersion: v1
kind: Pod
metadata:
  name: order-service
spec:
  containers:
  - name: order-app
    image: order-service:1.0
    # 1. Защита на время старта JVM (до 30 * 5с = 150 секунд)
    startupProbe:
      httpGet:
        path: /actuator/health/liveness
        port: 8080
      periodSeconds: 5
      failureThreshold: 30

    # 2. Ловля дедлоков и зависаний процесса
    livenessProbe:
      httpGet:
        path: /actuator/health/liveness
        port: 8080
      periodSeconds: 10
      failureThreshold: 3

    # 3. Управление поступлением клиентского трафика
    readinessProbe:
      httpGet:
        path: /actuator/health/readiness
        port: 8080
      periodSeconds: 5
      failureThreshold: 2

🟡 Middle Level

Жизненный цикл и конечный автомат проверок (State Machine)

Kubelet координирует выполнение трёх проб по строгому алгоритму:

               [ Контейнер запущен в Linux ]
                             │
                             ▼
              [ 1. Активна startupProbe ]
              (liveness и readiness заморожены)
                             │
             ┌───────────────┴───────────────┐
             │ Успех                         │ Провал (исчерпан failureThreshold)
             ▼                               ▼
 [ startupProbe отключена навсегда ]    [ 🔴 Убийство и перезапуск контейнера ]
             │
             ├───────────────────────────────┐
             ▼                               ▼
  [ 2. Активна livenessProbe ]    [ 3. Активна readinessProbe ]
  (Периодический опрос)           (Периодический опрос)
             │                               │
       Провал:                         Провал:
       🔴 Перезапуск контейнера        🟡 Исключение IP из Endpoints

Сравнительная таблица триады проверок

| Критерий | startupProbe | livenessProbe | readinessProbe | | :— | :— | :— | :— | | Ключевой вопрос | Завершился ли старт? | Жив ли рабочий поток? | Готов ли к обработке RPC? | | Период работы | Только при старте пода | После успеха startup | После успеха startup | | Реакция на отказ | 🔴 Убийство контейнера | 🔴 Перезапуск контейнера | 🟡 Отключение от трафика | | Влияние на другие | Блокирует Liveness/Readiness| Независимо | Независимо | | Что проверять | Загрузку контекста Spring | Deadlock, Thread Starvation | Доступность кэшей, пулов БД |


Катастрофический антипаттерн: «Единый эндпоинт /health»

Многие разработчики по незнанию прописывают один и тот же эндпоинт /health во все три секции:

# ❌ ГРУБЕЙШАЯ ОШИБКА АРХИТЕКТУРЫ
livenessProbe:
  httpGet: { path: /health, port: 8080 } # Проверяет БД и Redis!
readinessProbe:
  httpGet: { path: /health, port: 8080 }

Почему это убивает систему:

  1. Если сервер Redis кратковременно перезагрузится, эндпоинт /health вернет HTTP 503.
  2. Из-за общего эндпоинта сработает Liveness-проба.
  3. Kubernetes решит, что Java-приложение умерло, и перезапустит абсолютно все поды бэкенда в кластере!
  4. Сервис уйдёт в бесконечный CrashLoop, вызвав полный отказ бизнеса.

🔴 Senior Level

Архитектура Kubelet: Внутренности Prober Manager

Внутри демона kubelet (написанного на Go) за выполнение проверок отвечает модуль pkg/kubelet/prober:

  • Асинхронные воркеры: на каждую объявленную пробу в поде Kubelet запускает отдельную изолированную горутину-воркер (worker.go).
  • Собственные очереди (WorkQueue): выполнение проб не блокирует основной цикл управления контейнерами Kubelet’а.
  • Обработка сетевых тайм-аутов: если сокет приложения не отвечает за время timeoutSeconds, горутина принудительно обрывает контекст net.Dialer и регистрирует отказ, предотвращая утечку файловых дескрипторов на хосте.

Тонкости настройки под виртуальную машину Java (HotSpot JVM)

При работе с Java в контейнерах необходимо учитывать специфику работы JVM:

1. Учет пауз Stop-The-World (GC STW Pauses)

Если вы используете сборщик мусора G1 или Parallel GC на больших кучах (Heap 8–32 GB):

  • Пауза Full GC может заморозить JVM на 1–4 секунды.
  • Если в манифесте задан таймаут timeoutSeconds: 1, Kubelet зафиксирует ложный сбой.
  • Правило: timeoutSeconds должен быть всегда больше максимальной ожидаемой паузы сборщика мусора (рекомендуется 3–5 секунд).

2. Прогрев JIT-компилятора и Spring Boot AOT

В Java 21 со Spring Boot 3 первые запросы к приложению вызывают интенсивную компиляцию C2 JIT:

  • startupProbe должна давать достаточный запас времени (failureThreshold: 30, periodSeconds: 5 = 150 сек).
  • Для моментального прохождения startupProbe (менее чем за 100 мс) применяют компиляцию в нативный бинарный код (GraalVM Native Image) или технологию CRaC (Coordinated Restore at Checkpoint).

Взаимодействие со стадией Graceful Shutdown

Когда под выводится из эксплуатации, важно предотвратить гонку между health-пробами и остановкой:

1. kubectl delete pod -> Статус Terminating
2. Kubelet запускает preStop hook (sleep 10)
3. EndpointSlice Controller исключает IP пода из сетевых таблиц
4. Kubelet посылает SIGTERM процессу Java
5. Spring Boot дожидается завершения активных HTTP-запросов (spring.lifecycle.timeout-per-shutdown-phase=30s)
6. Контейнер завершает работу штатно с кодом 0

Во время фазы Terminating агент Kubelet прекращает перезапускать контейнер по liveness probe, даже если проба начинает падать.


4 Tricky Questions

1. Что произойдёт с контейнером, если он исчерпает лимит попыток failureThreshold в startupProbe? Чем отличается поведение в современных версиях K8s от ранних версий?

Ответ:

  • В современном Kubernetes (1.20+): если startupProbe возвращает отказ failureThreshold раз подряд, Kubelet считает, что приложение не смогло инициализироваться, принудительно убивает контейнер сигналом SIGKILL и перезапускает его в соответствии с политикой restartPolicy (счетчик перезапусков увеличивается).
  • В ранних версиях (до 1.20): из-за бага в подсистеме prober отказ startupProbe приводил к вечному зависанию пода в неготовности без инициализации процедуры перезапуска контейнера (исправлено в PR #95190).

2. Почему в высоконагруженных системах категорически не рекомендуется использовать проверки типа exec для частых Health Checks?

Ответ: При выполнении пробы exec Kubelet каждые periodSeconds обращается к CRI (Container Runtime Interface, например containerd) и выполняет системные вызовы ядра Linux clone() / fork() и execve():

  • Внутри пространства имён контейнера порождается новый полноценный процесс (например, /bin/sh или curl).
  • Это вызывает резкий рост расхода CPU хостовой системы, переключение контекстов ядра Linux, блокировки в cgroups и фрагментацию таблицы процессов PID.
  • Если рантайм containerd испытывает дефицит ресурсов, fork может зависнуть по таймауту, вызвав ложное падение пробы. Использование httpGet или tcpSocket свободно от этих проблем, так как Kubelet проверяет открытый сетевой сокет снаружи без порождения дочерних процессов.

3. Как ведут себя пробы Liveness и Readiness, если в манифесте объявлен startupProbe, но он ещё не вернул первый успешный ответ?

Ответ: Пока startupProbe не вернёт успешный статус (Success), обе пробы — и livenessProbe, и readinessProbe — полностью ОТКЛЮЧЕНЫ. Kubelet даже не начинает отправлять проверочные запросы на их адреса. Это сделано для того, чтобы тяжелое приложение могло спокойно выполнять миграции Liquibase/Flyway, прогревать кэш и компилировать классы в течение нескольких минут, не рискуя быть преждевременно убитым агрессивным livenessProbe.


4. Может ли упавшая readinessProbe защитить приложение от нехватки памяти (OOMKilled) при резком наплыве входящих HTTP-запросов?

Ответ: Да, может. Если в приложении реализован механизм саморегуляции (Backpressure / Shedding Load):

  • При заполнении пула соединений Tomcat или превышении очереди задач сервис программно переводит ReadinessState в REFUSING_TRAFFIC (возвращая HTTP 503 на пробу готовности).
  • Kubelet исключает под из EndpointSlice, и новые пользователи перестают направляться на этот экземпляр.
  • Под спокойно дообрабатывает уже принятые запросы в памяти, мусор утилизируется сборщиком Garbage Collector, память освобождается, и процесс избегает фатального вызова ядра Linux OOM Killer (Exit Code 137). После нормализации состояния readiness-проба снова включается.

🎯 Шпаргалка для интервью

Главное о Health Checks

  • Триада проб:
    • startupProbe — защита медленного старта (JVM, Spring Boot); блокирует остальные пробы.
    • livenessProbe — выявление дедлоков/зависаний; при отказе $\to$ рестарт контейнера.
    • readinessProbe — готовность обслуживать клиентов; при отказе $\to$ снятие с балансировки.
  • Категорический антипаттерн: единый эндпоинт /health, проверяющий внешнюю БД во всех пробах (приведет к падению всех подов при сбое БД).
  • Таймауты в Java: timeoutSeconds должен превышать максимальную паузу Full GC (не менее 3–5 секунд).
  • Механизмы: предпочитать httpGet и grpc вместо тяжелого exec.
  • Архитектура Kubelet: пробы выполняются асинхронно отдельными горутинами через pkg/kubelet/prober.

Сводка действий при сбое

| Проба | Действие K8s | Влияние на трафик | Применение | | :— | :— | :— | :— | | startup | Рестарт контейнера | Трафик не подаётся | Старт Spring Boot / миграции БД | | liveness| Рестарт контейнера | Трафик сбрасывается | Ликвидация Deadlock и зависаний | | readiness| Снятие с балансировки| Трафик уходит на другие поды | Защита от перегрузки / прогрев кэша |

Красные флаги на собеседовании (Чего говорить нельзя)

  • ❌ «Мы используем один и тот же health-эндпоинт для Liveness и Readiness проб» — грубейшая ошибка, при проблемах в зависимостях кластер упадет целиком.
  • ❌ «Readiness-проба убивает под, если он долго не отвечает» — readiness никогда не убивает процесс, только изолирует от сети.
  • ❌ «Для Java-сервисов startupProbe не нужна, достаточно поставить livenessProbe с initialDelaySeconds: 5» — сервис гарантированно попадет в CrashLoopBackOff.

Связанные вопросы