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. Когда БД упала:
/healthначинает возвращать 503.- readinessProbe FAIL — Pod снят из Endpoints. Это правильно — нет смысла слать трафик в degraded Pod.
- livenessProbe FAIL — kubelet kill контейнер. Это неправильно — контейнер был жив, БД восстановится, нужно было подождать.
- Новый контейнер стартует с холодных connection pools → опять fail → restart loop.
Цель readiness — оставить контейнер живым в надежде на восстановление. Liveness рестартом этого не достигнет.
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 работает идеально, но рестартуется бесконечно.
Варианты решения:
- Завести отдельный unauthenticated
/healthzendpoint. Самый правильный вариант. - Передать токен через
httpHeadersв probe — но тогда токен в манифесте Pod, что хуже секрит-менеджмент. - Сделать
/api/usersnon-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.
В сомнительных случаях — не настраивайте livenessProbe вообще. Контейнер всё равно рестартуется при exit / crash. livenessProbe нужен только для процессов, которые «висят живыми, но не работают» — а это меньшинство сценариев. Pod без livenessProbe и с правильным readinessProbe — нормальная production-конфигурация.