Привет, Хабр!
Если вы хоть раз дебажили под, который вроде работает, но Kubernetes его всё равно убивает — добро пожаловать. Сегодня разложим по косточкам, как livenessProbe может угробить ваш сервис в самый беззащитный момент — и как не дать этому случиться.
Сценарий проблемы
Рассмотрим типичный кейс. Есть микросервис, например на Spring Boot или.NET, который при запуске выполняет стандартный набор операций:
-
применяет миграции схемы базы данных (Flyway, Liquibase);
-
загружает конфигурации из внешнего Vault или Consul;
-
устанавливает соединения с Redis, Kafka, S3;
-
прогревает кеш, создаёт фоновые воркеры;
-
и только после этого начинает слушать HTTP‑порт и отдавать /healthz.
Инициализация может занимать от 30 до 60 секунд — особенно на загруженном CI/CD‑кластере, с прогретыми томами и сетевой задержкой к БД.
Теперь посмотрим на фрагмент манифеста:
livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 2
Что делает kubelet:
-
Через 10 секунд после старта контейнера запускается первая проверка на /healthz.
-
Приложение ещё не завершило старт: порт 8080 может не слушаться, эндпоинт /healthz ещё не отвечает.
-
Первая проверка возвращает connection refused или timeout — это считается ошибкой.
-
Через 5 секунд — вторая попытка. Ситуация та же: приложение не готово.
-
Второй фейл подряд. Достигнут failureThreshold = 2 → kubelet считает контейнер «неживым».
-
Контейнер убивается и перезапускается с нуля.
-
Цикл повторяется бесконечно.
Суть проблемы: livenessProbe
срабатывает до того, как приложение технически готово пройти проверку, и Kubernetes ошибочно считает, что контейнер «завис». Но пофакту контейнер не завис, он просто ещё не успел инициализироваться.
livenessProbe не ждёт, пока приложение будет готово. Она начинает проверки строго по initialDelaySeconds
. И если приложение в этот момент ещё не подняло HTTP‑сервер — оно будет считаться «мертвым».
Что ещё усугубляет ситуацию:
-
Эндпоинт /healthz зависит от внешней инфраструктуры: если при старте сервис не может достучаться до БД или Redis, он будет возвращать 500 или 503, даже если сам процесс жив и находится в стабильной инициализации.
-
Параметры пробы подобраны без учёта real‑world‑таймингов: значения вроде
initialDelaySeconds
: 10 иfailureThreshold
: 2 подходят для лёгких сервисов, но не для сложных backend‑приложений с цепочкой инициализаций. -
В CI/CD pipeline нет возможности протестировать пробу под высокой нагрузкой — на слабых узлах с подогретым volume startup может быть заметно медленнее.
Как правильно настраивать пробы
readinessProbe — контроль трафика, а не здоровья
Это первая проба, которую стоит включить в продакшен. Её задача — сообщить Kubernetes, что контейнер ещё не готов обрабатывать запросы, даже если он запущен.
readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 15 periodSeconds: 10 failureThreshold: 5
Что происходит при отказе: контейнер не будет включён в список endpoints, и сервис не станет направлять на него трафик. Но контейнер останется жив — он не будет перезапущен. Это нужно, если:
-
приложение стартует, но ещё инициализирует зависимости (DB, кеш, внешние API);
-
сервис может пережить временные ошибки, не прерывая работу (например, перегрузка, пауза GC);
-
вы хотите, чтобы временно недоступные поды просто «выпали из балансировки».
use‑case: сервис стартует за 20–30 секунд, но обрабатывать запросы может начать только после 40-й. readinessProbe
не даст нагружать его раньше времени.
startupProbe — защита от преждевременного рестарта
Если приложение имеет тяжёлую инициализацию — обязательно добавляйте startupProbe
.
startupProbe: httpGet: path: /healthz port: 8080 periodSeconds: 10 failureThreshold: 30
Эта конфигурация позволяет приложению стартовать в течение до 5 минут (30 попыток по 10 секунд), прежде чем Kubernetes начнёт применять livenessProbe
и readinessProbe
.
Пока startupProbe не пройдена, Kubernetes не выполняет другие пробы.
livenessProbe — только после стабилизации поведения
livenessProbe
— самая опасная из всех трёх. Её задача — обнаруживать зависшие процессы. Но если она настроена неправильно, она же может и убить живой под.
livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 60 periodSeconds: 20 failureThreshold: 3
Параметры initialDelaySeconds: 60
, periodSeconds: 20
и failureThreshold: 3
означают, что первая проверка начнётся через минуту после запуска, затем будут попытки каждые 20 секунд, и только три подряд неудачи приведут к перезапуску контейнера.
Если под уходит в рестарты, необходимо понять — какая именно проба срабатывает. Делается это с помощью kubectl describe
.
kubectl describe pod <pod-name>
В выводе ищите секции вроде:
Liveness probe failed: HTTP probe failed with statuscode: 500
или
Readiness probe failed: Get http://10.0.1.5:8080/ready: connection refused
Также полезна команда:
kubectl get events --sort-by=.lastTimestamp
Она покажет хронологию всех событий по поду: когда сработала проба, когда был рестарт, какие статусы возвращались.
Пример комбинированной конфигурации
startupProbe: httpGet: path: /healthz port: 8080 periodSeconds: 10 failureThreshold: 30 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 60 periodSeconds: 20 failureThreshold: 3
Такой шаблон подходит для большинства веб‑приложений, которые:
-
стартуют от 30 до 90 секунд;
-
имеют временные зависимости;
-
должны переживать кратковременные сбои без рестарта.
Ошибки, которых стоит избегать
Неиспользование startupProbe при медленном старте
Если приложение запускается дольше 10–15 секунд — обязательно нужно использовать startupProbe
. Без неё Kubernetes может начать выполнять livenessProbe
слишком рано, когда сервис ещё инициализируется. В результате — ложные фейлы и перезапуски.
Что происходит:
-
livenessProbe
срабатывает до завершения инициализации. -
Приложение не отвечает вовремя на
/healthz
. -
kubelet считает его умершим и перезапускает.
Для этого нужно добавить startupProbe
, которая даст приложению больше времени на запуск. Например:
startupProbe: httpGet: path: /healthz port: 8080 failureThreshold: 30 periodSeconds: 10
Это даёт до 5 минут на старт (30 × 10 секунд), без риска быть «убитым» раньше времени.
Зависимость /healthz от внешних сервисов
Бывает, что в /healthz вы проверяете всё: базу данных, Redis, очередь сообщений, и даже внешний API. Кажется логичным: пусть он скажет, что «всё работает».
Проблема в другом: эти сервисы могут временно недоступны, а это не означает, что сам ваш сервис «умер». Например, если Redis отвалился на 5 секунд — зачем перезапускать весь контейнер?
Поэтому в livenessProbe проверяйте, работает ли сам процесс: есть ли доступ к памяти, нет ли deadlock»ов, не «завис» ли event loop. А так же проверку внешних зависимостей лучше делать в readinessProbe — чтобы временно выключать под из балансировки, не убивая его.
Пример безопасного liveness‑эндпоинта:
func Healthz(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }
Возврат 200 OK в /healthz в любом случае
Самая распространённая ошибка — всегда возвращать 200, независимо от состояния сервиса. Да, это удобно на старте, чтобы pod не падал. Но по факту такое поведение лишает смысла все проверки.
Что происходит:
-
/healthz всегда отвечает 200, даже если сервис завис.
-
Kubernetes считает, что всё в порядке.
-
Балансировщик продолжает направлять трафик на нерабочий под.
/healthz
должен возвращать 200 только в случае, если сервис действительно работоспособен. При любых сбоях — например, потере соединения с базой, блокировке потоков или внутренней ошибке — следует возвращать 503, чтобы Kubernetes мог адекватно отреагировать.
Если вы работаете с Kubernetes в проде, хорошо знаете его поведение в теории и на практике — возможно, вас заинтересуют открытые уроки, где разберём чуть глубже архитектурные принципы и современные инструменты экосистемы.
Ближайшие темы:
ссылка на оригинал статьи https://habr.com/ru/articles/897550/
Добавить комментарий