Пробы (probes) - это способ, которым kubelet понимает, здоров контейнер или нет и можно ли пускать в него трафик. Идея простая, но именно на пробах чаще всего спотыкаются: неправильно настроенная проба не защищает сервис, а роняет его сама - каскадными рестартами или тем, что трафик уходит в заведомо сломанный pod. Разбираемся, что делает каждая из трёх проб, какие ошибки встречаются чаще всего и как настроить их так, чтобы они работали на вас, а не против.
Три пробы и что они реально делают
Kubernetes различает три пробы, и путать их - главный источник проблем. Ключевое отличие не в том, как они проверяют (HTTP-запрос, TCP-соединение, команда в контейнере - механизм у всех одинаковый), а в том, что происходит при провале.
- liveness отвечает на вопрос «контейнер ещё жив или завис?». Провал = kubelet перезапускает контейнер. Это тяжёлое действие: pod теряет состояние в памяти, рвутся соединения.
- readiness отвечает «готов ли pod обслуживать запросы прямо сейчас?». Провал = pod убирается из endpoints Service, трафик на него не идёт, но контейнер не перезапускается. Как только проба снова проходит - pod возвращается в ротацию.
- startup нужна приложениям с долгим стартом (прогрев кэша, миграции, JVM). Пока startup не прошла успешно, liveness и readiness не выполняются вовсе - значит, pod не убьют за «неответ» во время загрузки.
Пять ошибок, которые роняют прод
- Одна и та же логика в liveness и readiness. Контейнер под нагрузкой начинает медленно отвечать - readiness выкидывает pod из трафика (правильно), но та же проверка в liveness одновременно перезапускает контейнер (неправильно). Вместо разгрузки получаете перезапуск в самый неподходящий момент.
- liveness проверяет внешние зависимости. Классика: liveness дёргает эндпоинт, который ходит в базу. База на секунду затормозила - liveness падает во всех подах разом - Kubernetes перезапускает весь сервис целиком. Одна медленная зависимость превращается в лавину рестартов.
- Слишком агрессивные пороги.
timeoutSeconds: 1иfailureThreshold: 1означают, что один медленный ответ = перезапуск. Любой GC-паузы или всплеска латентности достаточно, чтобы получитьCrashLoopBackOffна ровном месте. - Нет startup-пробы у медленного приложения. Без неё liveness стартует сразу, и приходится угадывать
initialDelaySeconds. Поставили мало - pod убивают на старте; поставили много - реальные зависания ловятся с задержкой. startup-проба снимает этот компромисс. - readiness, который никогда не падает. Проба возвращает 200, даже когда приложение не в состоянии обслуживать запросы (потеряло коннект к зависимости, забит пул). Трафик исправно летит в сломанный pod, пользователи получают ошибки.
Как настроить правильно
Правило, которое закрывает большинство проблем: liveness должна быть дешёвой и локальной, а readiness может смотреть на зависимости.
- liveness - только «процесс не в дедлоке». Отдельный лёгкий эндпоинт, который отвечает 200, если event loop жив. Никаких запросов в БД, кэш или соседние сервисы.
- readiness - «могу ли я обслужить запрос». Здесь уместно проверить критичные зависимости: есть ли коннект к базе, прогрет ли пул. Если зависимость недоступна - pod выходит из трафика, но остаётся жив и вернётся сам.
- startup - рассчитайте бюджет времени:
failureThreshold × periodSecondsдолжно быть больше самого долгого честного старта. Например,failureThreshold: 30иperiodSeconds: 5дают 150 секунд на загрузку, после чего pod считается зависшим.
Короткая шпаргалка по параметрам: initialDelaySeconds - пауза перед первой проверкой (для liveness почти не нужна, если есть startup); periodSeconds - как часто проверять; timeoutSeconds - сколько ждать ответа; failureThreshold - сколько провалов подряд считать за отказ; successThreshold - сколько успехов для восстановления (для liveness всегда 1).
Как диагностировать
Если pod бесконечно перезапускается, первым делом смотрите события: kubectl describe pod <имя> - в разделе Events будет строка вроде Liveness probe failed: ... с конкретной причиной (таймаут, код ответа, отказ соединения). Растущий RESTARTS в kubectl get pods и статус CrashLoopBackOff почти всегда указывают на liveness. А если pod долго висит в состоянии READY 0/1, но не перезапускается - дело в readiness: контейнер жив, но не пускает трафик. Разделение «перезапуск против трафика» - самый быстрый способ понять, какую пробу чинить.
Отдельный нюанс: сообщение Last State: Terminated - Reason: OOMKilled к пробам отношения не имеет - это нехватка памяти, и пробы тут ни при чём. Не путайте одно с другим, иначе будете крутить timeoutSeconds там, где нужно поднять лимит памяти.
Когда пробы можно не писать руками
Всё описанное выше - это ручная работа, которую в каждом новом сервисе повторяют заново, и ошибаются в одних и тех же местах. Внутренние платформы (Internal Developer Platform) решают это иначе: пробы генерируются по healthcheck-пути или из шаблона по стандарту команды, а не подбираются на глаз в каждом деплое. Если интересно, как выглядит такой подход на практике - мы описали его в обзоре платформы Opsy. Но даже без платформы правило «liveness дешёвая и локальная, readiness смотрит на зависимости, startup - для медленного старта» закрывает большую часть боли.