Learning Platform
Глоссарий Troubleshooting
Урок 13.04 · 20 мин
Продвинутый
Probesanti-patternsproductiontroubleshootingcascading failures

Probe anti-patterns

Probes — это место, где простой YAML создаёт реальные production-аварии. Большинство restart loops в production — это не баги приложения, а ошибки конфигурации probes. Ниже — шесть типовых анти-паттернов, каждый с реальным сценарием отказа и правильным решением.


vmstat и iostat: что делает CPU и диск под капотом

Anti-pattern 1: Heavy probes

probe делает запрос в БД, или дёргает downstream HTTP service, или вычисляет что-то тяжёлое:

# ПЛОХО
livenessProbe:
  exec:
    command: ["sh", "-c", "psql -c 'SELECT count(*) FROM orders'"]

Что не так:

  • На каждый probe — нагрузка на app и на dependencies. С periodSeconds: 10 и 50 replicas — 5 запросов/секунду на БД только от probes.
  • Если БД деградирует — probes начинают timeout-иться → false-positive failures → рестарты → новые контейнеры идут в БД с холодных connection pools → ещё хуже. Self-DDoS.

Правильно: probe должен быть в-процессе и быстрым:

# ХОРОШО
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080

Endpoint /healthz в коде должен возвращать 200, не делая обращений к БД / downstream. Максимум — проверить in-memory флаг типа isShuttingDown, или просто вернуть 200.


Anti-pattern 2: Один endpoint для liveness и readiness

Самая частая ошибка в legacy кодовых базах:

# ПЛОХО
livenessProbe:
  httpGet:
    path: /health
    port: 8080
readinessProbe:
  httpGet:
    path: /health
    port: 8080

Что не так: /health обычно проверяет «всё ли работает» — БД, кэш, downstream. Когда БД упала:

  1. /health начинает возвращать 503.
  2. readinessProbe FAIL — Pod снят из Endpoints. Это правильно — нет смысла слать трафик в degraded Pod.
  3. livenessProbe FAIL — kubelet kill контейнер. Это неправильно — контейнер был жив, БД восстановится, нужно было подождать.
  4. Новый контейнер стартует с холодных connection pools → опять fail → restart loop.

Цель readiness — оставить контейнер живым в надежде на восстановление. Liveness рестартом этого не достигнет.

Правильный split health endpoints
/healthz (liveness)Проверяет ТОЛЬКО что процесс жив. Возвращает 200 если HTTP server отвечает, deadlock detector не сработал, основной event loop крутится. НЕ проверяет БД / downstream. Должен FAIL только при настоящем зависании.
/ready (readiness)Проверяет, может ли Pod обслуживать запросы прямо сейчас. БД pool инициализирован, кэш прогрет, миграции применены. FAIL — Pod вырезается из трафика, но не убивается.
/startup (startup)Опциональный отдельный endpoint для startupProbe. Часто можно reuse /ready (но строже — например, миграции БД должны быть выполнены, что не обязательно проверять каждый раз).

Anti-pattern 3: Нет startupProbe для slow-starting apps

Java app со Spring Boot стартует 60-90 секунд (classloading, JIT, init beans). Конфигурация:

# ПЛОХО
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 30
  failureThreshold: 3
  periodSeconds: 10

Что не так: первая проба запустится в t=30s. App ещё в warmup → fail. t=40s, t=50s — ещё два fail → failureThreshold: 3 → kubelet kill контейнер на 60-й секунде, именно в тот момент, когда app почти закончил стартовать. Восстановления нет — каждый новый контейнер падает на той же отметке.

Многие пытаются «лечить» это, увеличивая initialDelaySeconds: 120. Это работает, но скрывает диагностику: первые 2 минуты зависший контейнер живёт без присмотра. Если app падает в init — никто не заметит.

Правильно: startupProbe:

# ХОРОШО
startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 18   # 18 × 10s = 3 minutes
  periodSeconds: 10

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 10
  failureThreshold: 3

startupProbe защищает первые 3 минуты, потом активируется агрессивная liveness.


Anti-pattern 4: TCP probe для HTTP service

# ПЛОХО для HTTP сервиса
livenessProbe:
  tcpSocket:
    port: 8080

Что не так: TCP-открытый порт не означает, что HTTP-уровень работает. nginx может accept TCP, но возвращать 500 на все запросы (плохой конфиг). Application server может слушать порт, но deadlock внутри request handler — все запросы висят. tcpSocket видит «соединение установлено» — PASS. Реальные клиенты видят ошибки.

Правильно: для HTTP — httpGet. Для gRPC — grpc. tcpSocket оставьте для legacy TCP-сервисов или как ОЧЕНЬ дешёвый pre-probe (например, в init container).


Anti-pattern 5: HTTP probe на authenticated endpoint

# ПЛОХО
livenessProbe:
  httpGet:
    path: /api/users
    port: 8080

/api/users требует JWT в заголовке. kubelet идёт без токена → 401. 401 не входит в 200-399 → probe FAIL → restart loop. App работает идеально, но рестартуется бесконечно.

Варианты решения:

  1. Завести отдельный unauthenticated /healthz endpoint. Самый правильный вариант.
  2. Передать токен через httpHeaders в probe — но тогда токен в манифесте Pod, что хуже секрит-менеджмент.
  3. Сделать /api/users non-auth — никогда не делайте этого.
# ХОРОШО
livenessProbe:
  httpGet:
    path: /healthz   # otherwise: dedicated /healthz без auth
    port: 8080

Anti-pattern 6: Probe смотрит на cascading dependencies

# ПЛОХО — /health проверяет DB + Redis + Kafka + external API
livenessProbe:
  httpGet:
    path: /health
    port: 8080

Endpoint /health в коде:

def health():
    if not db.is_connected(): return 503
    if not redis.is_connected(): return 503
    if not kafka.is_connected(): return 503
    if not external_api.ping(): return 503
    return 200

Что не так: зависимость падает — все Pods рестартуются. Один Kafka outage → весь fleet микросервиса рестартуется → плюс к проблеме Kafka получаем cold start всех app. Cascading failure из одного компонента в зависимый.

Правильно: liveness — только про процесс жив, не про dependencies. readiness — может проверять критичные dependencies (только те, без которых Pod вообще не может обслуживать запросы), но не транзитивные (downstream of downstream).

def healthz():
    # liveness — только in-process check
    return 200 if not is_shutting_down else 503

def ready():
    # readiness — только критичные direct deps
    return 200 if db_pool.is_healthy() else 503
    # NO redis, NO kafka, NO external API

Killer момент: liveness — последняя инстанция

Главная ментальная модель про liveness — это не первая линия диагностики. Это аварийный exit. Pod restart — дорогая операция:

  • Новый контейнер с холодных pools / кэшей.
  • Lost connections (для stateful soketов).
  • Provisional capacity drop в Service до того, как новый контейнер пройдёт startup + readiness.
  • Cascading nuisance: рестарт одного → нагрузка переезжает на остальных → больше шансов рестарта других.

Используйте liveness только для случаев, когда точно знаете: процесс залип и рестарт его починит. Deadlock detection. In-memory state corruption. Detected memory leak above threshold. Всё остальное — readiness.

DANGER

В сомнительных случаях — не настраивайте livenessProbe вообще. Контейнер всё равно рестартуется при exit / crash. livenessProbe нужен только для процессов, которые «висят живыми, но не работают» — а это меньшинство сценариев. Pod без livenessProbe и с правильным readinessProbe — нормальная production-конфигурация.


Проверка знанийKnowledge check
Production-сервис теряет соединение с PostgreSQL. У него один и тот же endpoint /health на liveness и readiness. Что произойдёт?
ОтветAnswer
readinessProbe FAIL — Pod снят из Endpoints (правильно, не шлём трафик в degraded Pod). livenessProbe FAIL — kubelet kill контейнер (неправильно, БД восстановится сама). Новый контейнер стартует, БД может быть всё ещё down → restart loop. Цель readiness — дать app время восстановиться без рестарта. Когда один endpoint обслуживает оба probes, это свойство теряется. Решение: split endpoints — /healthz (только процесс жив, без deps) для liveness, /ready (с deps) для readiness.
Проверка знанийKnowledge check
Java Spring Boot app стартует 60 секунд. Liveness probe initialDelaySeconds: 30, periodSeconds: 10, failureThreshold: 3. Почему restart loop?
ОтветAnswer
Первая проба в t=30s — app в warmup, fail. t=40s — fail. t=50s — fail. На t=50-60s достигается failureThreshold: 3, kubelet kill контейнер именно тогда, когда app почти стартовал. Новый контейнер повторяет. Лечение: startupProbe с failureThreshold большим (например, 18 × 10s = 3 минуты), liveness — без initialDelaySeconds (startup защищает первую фазу).
Проверка знанийKnowledge check
Probe endpoint /health проверяет статус Kafka, Redis и downstream service. Kafka недоступен 5 минут. Что произойдёт со всем fleet микросервиса (100 replicas)?
ОтветAnswer
Все 100 Pods начнут fail-ить health → если это liveness, все 100 одновременно рестартуются. Это cascading failure: одна dependency (Kafka) положила весь fleet, при том что app мог бы продолжать работать (или fail gracefully) на остальных endpoints. Правильно: liveness — только in-process check, без external deps. readiness может проверять только direct critical deps (например, БД), но не транзитивные.
Проверка знанийKnowledge check
Когда правильно НЕ настраивать livenessProbe?
ОтветAnswer
Когда нет конкретного сценария зависания, который рестарт лечит. Контейнер при crash / exit рестартуется автоматически (по restartPolicy). livenessProbe нужен только для случая 'процесс жив, но завис' (deadlock, infinite loop, memory leak threshold). Если приложение не имеет таких сценариев — livenessProbe только добавляет риск false-positive restart-ов. readinessProbe нужен почти всегда (для смены состояния трафика без рестарта).

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Probe endpoint /health делает SELECT 1 в БД. Что произойдёт при деградации БД (slow queries)?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5