Под запущен, процесс работает, но приложение не отвечает на запросы. Или отвечает, но с ошибками - потому что база ещё не подключилась. Kubernetes об этом не знает и продолжает слать трафик. Пользователи получают 500-е, дежурный просыпается в три ночи.
Именно для таких ситуаций существуют механизмы проверки состояния подов. Настроенные правильно, они автоматически убирают нерабочий экземпляр из балансировки и перезапускают его, если приложение зависло. Настроенные неправильно - сами становятся источником инцидентов: петли перезапуска, преждевременное исключение из трафика, CrashLoopBackOff без видимой причины.
В этой статье разберём все три типа проверок, конкретные параметры с объяснением последствий неверных значений и сценарии, где стандартная конфигурация ломается.
Коротко:
- Readiness отвечает за маршрутизацию: пока проверка не прошла, под исключён из Service и трафик на него не идёт.
- Liveness отвечает за перезапуск: если проверка провалилась нужное количество раз, kubelet убивает контейнер и поднимает его заново.
- StartupProbe нужен для медленно стартующих приложений - он блокирует liveness до тех пор, пока сервис не поднялся.
- Самая частая ошибка - слишком маленький
initialDelaySecondsв liveness: приложение ещё грузится, а kubelet уже перезапускает его в петле. - Readiness и liveness проверяют разные вещи и не должны указывать на один и тот же эндпоинт.
- Graceful shutdown требует отдельной настройки - одних проб недостаточно.
Как Kubernetes решает, куда слать трафик
Когда под запускается, kubelet начинает выполнять настроенные проверки и передаёт результат двум подсистемам. Первая - эндпоинт-контроллер: он добавляет или убирает IP пода из списка Endpoints у Service. Вторая - сам kubelet: он решает, нужно ли перезапускать контейнер.
Без каких-либо проверок под считается готовым сразу после старта процесса. Это значит, что трафик пойдёт на него ещё до того, как приложение успело инициализироваться. Для большинства нетривиальных сервисов - Java, Python с тяжёлыми зависимостями, Go с прогревом кеша - это гарантированные ошибки в первые секунды после деплоя.
Три типа проверок и чем они отличаются
Путаница между readiness и liveness - одна из самых распространённых проблем при первичной настройке. Они похожи по синтаксису, но делают принципиально разные вещи.
| Тип | Что происходит при провале | Влияет на трафик | Влияет на перезапуск |
|---|---|---|---|
| readinessProbe | Под убирается из Endpoints | Да | Нет |
| livenessProbe | Контейнер перезапускается | Нет напрямую | Да |
| startupProbe | Контейнер перезапускается (если не прошёл за отведённое время) | Нет | Да, только до первого успеха |
Readiness отвечает на вопрос «готов ли под принимать запросы прямо сейчас». Это состояние может меняться в течение жизни пода: например, если сервис временно потерял соединение с базой, он может выйти из ротации и вернуться обратно после восстановления.
Liveness отвечает на вопрос «жив ли процесс вообще». Если приложение зависло намертво и перестало отвечать, kubelet должен его перезапустить. Но делать это нужно только тогда, когда перезапуск действительно поможет - не при каждой временной проблеме.
Методы проверки: HTTP, TCP и exec
Kubernetes поддерживает три способа выполнить проверку. Выбор зависит от того, что умеет ваше приложение.
httpGet - самый распространённый вариант. Kubelet делает GET-запрос на указанный путь и порт. Ответ с кодом 200-399 считается успехом.
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3tcpSocket - проверяет, открыт ли порт. Подходит для сервисов, которые не говорят по HTTP: базы данных, брокеры, gRPC без HTTP-эндпоинта.
exec - запускает команду внутри контейнера. Ненулевой код возврата означает провал. Удобно, когда у сервиса нет сетевого интерфейса для проверки, но есть CLI.
Осторожно с exec: каждый вызов создаёт новый процесс внутри контейнера. При коротком periodSeconds и медленной команде это может заметно нагружать под. Особенно опасно в сочетании с shell-скриптами, которые форкают дочерние процессы.
Параметры: что каждый из них реально делает
Синтаксис у всех трёх типов одинаковый. Разберём каждый параметр с точки зрения того, что сломается при неверном значении.
initialDelaySeconds
Сколько секунд kubelet ждёт после старта контейнера перед первой проверкой. По умолчанию - 0.
Если поставить 0 или слишком маленькое значение для liveness, приложение ещё не успело запуститься, а проверка уже падает. Kubelet фиксирует провал, и после failureThreshold неудач перезапускает контейнер. Приложение снова не успевает запуститься. Это и есть петля перезапуска - CrashLoopBackOff.
Типичная ошибка: взять значение из примера в документации (обычно 3-5 секунд) и применить к Java-сервису, который стартует 40 секунд. Результат предсказуем.
periodSeconds
Как часто выполнять проверку. По умолчанию - 10 секунд.
Слишком маленькое значение создаёт лишнюю нагрузку на приложение и увеличивает количество ложных срабатываний при кратковременных задержках. Слишком большое - увеличивает время обнаружения проблемы. Для большинства HTTP-сервисов 10-15 секунд - разумный баланс.
timeoutSeconds
Сколько секунд ждать ответа. По умолчанию - 1 секунда.
Одна секунда - очень мало для нагруженного сервиса или для проверки, которая ходит в базу. Если приложение отвечает за 1.2 секунды под нагрузкой, проверка будет постоянно таймаутиться и фиксировать провалы. Рекомендуется ставить значение, которое заведомо больше 99-го перцентиля времени ответа вашего health-эндпоинта.
failureThreshold и successThreshold
failureThreshold - сколько последовательных провалов нужно, чтобы проверка считалась упавшей. По умолчанию - 3.
successThreshold - сколько последовательных успехов нужно после провала, чтобы вернуться в рабочее состояние. По умолчанию - 1. Для liveness это значение всегда должно быть 1 (ограничение Kubernetes).
Увеличение failureThreshold даёт приложению больше времени на восстановление перед перезапуском. Это полезно, если сервис иногда медленно отвечает под нагрузкой, но в целом живой. Уменьшение ускоряет реакцию на реальные зависания, но увеличивает риск ложных перезапусков.
Пример расчёта: если periodSeconds: 10 и failureThreshold: 3, то от первого провала до перезапуска пройдёт около 30 секунд. Если нужно быстрее реагировать - уменьшайте period, а не threshold. Если нужно меньше ложных срабатываний - увеличивайте threshold.
StartupProbe: решение для медленного старта
До появления startupProbe разработчики решали проблему медленного старта одним способом: ставили большой initialDelaySeconds в liveness. Это работало, но создавало другую проблему: если приложение зависало уже после старта, kubelet долго не замечал этого из-за большой задержки.
StartupProbe элегантно решает эту задачу. Пока он не вернул успех, liveness и readiness проверки не запускаются вообще. Как только startupProbe прошёл - он отключается, и дальше работают обычные проверки с нормальными интервалами.
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 10В этом примере приложению даётся 300 секунд (30 попыток по 10 секунд) на запуск. Если за это время startupProbe не прошёл - контейнер перезапускается. После успешного старта liveness начинает работать с собственными, более строгими параметрами.
Это особенно важно для сервисов, которые при старте выполняют миграции базы данных, прогревают кеш или ждут внешних зависимостей.
Типичные ошибки и их симптомы
Одинаковый эндпоинт для liveness и readiness
Вакансии для DevOps-инженеров
Если оба типа проверок смотрят на один и тот же /health, который проверяет подключение к базе, то при проблемах с базой под будет не просто убран из ротации - он будет перезапускаться снова и снова. Перезапуск не поможет, если проблема во внешней зависимости.
Правило простое: liveness должен проверять только то, что лечится перезапуском. Readiness может проверять внешние зависимости - это нормально.
Liveness проверяет бизнес-логику
Иногда в health-эндпоинт добавляют проверки типа «есть ли незакрытые заказы» или «работает ли очередь». Если такая проверка упала - это не значит, что перезапуск поможет. Под начнёт циклически перезапускаться, создавая дополнительную нагрузку и мешая диагностике.
Liveness должен быть максимально простым: жив ли процесс, может ли он принять соединение. Всё остальное - в readiness или в отдельные метрики.
Слишком агрессивные параметры при деплое
Во время rolling update новые поды поднимаются параллельно со старыми. Если readiness настроен слишком строго (маленький successThreshold, большой failureThreshold), новый под долго не входит в ротацию, а старый уже убит. Получается кратковременное снижение доступных реплик.
Игнорирование terminationGracePeriodSeconds
Когда под получает сигнал завершения, Kubernetes сначала убирает его из Endpoints (это занимает несколько секунд из-за задержек в распространении изменений), а потом отправляет SIGTERM процессу. Если приложение завершается быстрее, чем балансировщик успевает обновить список - часть запросов попадёт на уже остановленный под.
Graceful shutdown: почему проб недостаточно
Корректное завершение работы пода - отдельная задача, которую проверки состояния не решают полностью. Нужна связка из нескольких механизмов.
Во-первых, приложение должно обрабатывать SIGTERM: завершить текущие запросы, закрыть соединения с базой, дать время на обработку очереди. Многие фреймворки делают это автоматически, но не все.
Во-вторых, terminationGracePeriodSeconds должен быть достаточным для завершения всех текущих запросов плюс небольшой запас. По умолчанию - 30 секунд. Для сервисов с долгими транзакциями этого может не хватать.
В-третьих, полезно добавить preStop хук с небольшой задержкой - это даёт время на распространение изменений в Endpoints до того, как процесс начнёт завершаться.
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
terminationGracePeriodSeconds: 60Пять секунд в preStop - достаточно, чтобы kube-proxy и ingress-контроллер успели убрать под из балансировки до получения SIGTERM.
Реальные сценарии и конфигурации
Сценарий 1: Java-сервис с медленным стартом
Spring Boot приложение стартует 45-60 секунд, потом работает стабильно. Без startupProbe нужно ставить initialDelaySeconds: 70 в liveness - и всё это время зависание после старта не будет замечено.
startupProbe:
httpGet:
path: /actuator/health
port: 8080
failureThreshold: 12
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3Spring Boot Actuator начиная с версии 2.3 предоставляет отдельные эндпоинты /actuator/health/liveness и /actuator/health/readiness, которые разделяют эти два состояния на уровне приложения. Это правильный подход.
Сценарий 2: Сервис с зависимостью от базы данных
Приложение не может работать без PostgreSQL. При старте оно ждёт подключения. Если база недоступна - сервис не должен принимать трафик, но перезапускать его бессмысленно.
startupProbe:
httpGet:
path: /health/startup
port: 3000
failureThreshold: 20
periodSeconds: 5
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 5
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 3000
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 2Здесь /health/live проверяет только процесс, а /health/ready - соединение с базой. successThreshold: 2 в readiness означает, что под вернётся в ротацию только после двух последовательных успехов - это снижает вероятность попасть под нестабильное соединение.
Сценарий 3: Stateful сервис с долгой инициализацией
Сервис при старте загружает несколько гигабайт данных в память. Время старта непредсказуемо - от 2 до 10 минут в зависимости от объёма данных.
Здесь startupProbe с большим failureThreshold - единственный разумный вариант. Liveness с фиксированным initialDelaySeconds либо будет слишком агрессивным, либо слишком медленно реагировать на зависания после старта.
Как проверить, что всё работает
После настройки проверок нужно убедиться, что они работают так, как задумано, а не просто не падают.
Смотреть нужно на несколько вещей. В описании пода (kubectl describe pod) видно текущее состояние каждой проверки и количество провалов. В событиях пода будут записи о перезапусках и их причинах. В логах kubelet - детали выполнения проверок.
Полезно намеренно сломать проверку и посмотреть, как система реагирует: убрать под из Endpoints, перезапустить контейнер, вернуть обратно после восстановления. Это лучше делать в staging, а не обнаруживать поведение в production во время инцидента.
Признаки правильной настройки: при rolling update новые поды входят в ротацию только после прохождения readiness; при зависании процесса kubelet перезапускает контейнер без участия человека; при недоступности базы под выходит из ротации и возвращается после восстановления без перезапуска.
Чеклист перед выкаткой в production
- Настроен startupProbe для сервисов, которые стартуют дольше 20-30 секунд.
- Liveness и readiness смотрят на разные эндпоинты с разной логикой.
- Liveness не проверяет внешние зависимости (базу, кеш, брокер).
timeoutSecondsбольше 99-го перцентиля времени ответа health-эндпоинта под нагрузкой.initialDelaySecondsв liveness больше максимального времени старта приложения (если не используется startupProbe).- Приложение корректно обрабатывает SIGTERM и завершает текущие запросы.
terminationGracePeriodSecondsдостаточен для завершения самых долгих транзакций.- Добавлен preStop хук с задержкой 3-5 секунд для корректного обновления Endpoints.
- Поведение проверок проверено в staging: намеренный провал и восстановление.
- Нет ситуации, когда liveness и readiness используют один и тот же эндпоинт, который зависит от внешних сервисов.
FAQ
Чем readiness отличается от liveness простыми словами?
Readiness говорит балансировщику «не слать сюда трафик». Liveness говорит kubelet «перезапусти этот контейнер». Под может быть живым, но не готовым - например, когда ждёт подключения к базе. В этом случае нужно убрать его из ротации, а не перезапускать.
Почему под уходит в CrashLoopBackOff сразу после деплоя?
Чаще всего причина - слишком маленький initialDelaySeconds в livenessProbe. Приложение не успевает запуститься, проверка падает несколько раз подряд, kubelet перезапускает контейнер. Цикл повторяется. Решение: увеличить задержку или перейти на startupProbe.
Нужен ли startupProbe, если я уже поставил большой initialDelaySeconds?
Технически нет, но startupProbe лучше. Большой initialDelaySeconds означает, что liveness не работает всё это время - даже если приложение зависло через 5 секунд после старта. StartupProbe даёт гибкость: много времени на старт, но быстрая реакция после него.
Что должен возвращать health-эндпоинт?
Для liveness - минимальную проверку: жив ли процесс, может ли принять соединение. Никаких обращений к базе или внешним сервисам. Для readiness - проверку всех зависимостей, без которых сервис не может корректно обработать запрос. HTTP 200 при успехе, 500 или 503 при провале.
Трафик всё равно идёт на завершающийся под. Почему?
Это гонка между обновлением Endpoints и получением SIGTERM. Kubernetes убирает под из балансировки и отправляет сигнал завершения почти одновременно, но kube-proxy и ingress-контроллер обновляют правила с задержкой. Решение - preStop хук с задержкой 3-5 секунд: приложение получает SIGTERM только после того, как балансировщик успел обновиться.
Можно ли использовать одну проверку вместо трёх?
Можно, но это компромисс. Если настроить только readiness, под не будет перезапускаться при зависании - нужно будет вмешиваться вручную. Если только liveness - трафик пойдёт на под ещё до готовности. Для production-сервисов с нетривиальным стартом все три типа оправданы.
Как понять, что проверки работают правильно?
Запустите kubectl describe pod и посмотрите на секцию Conditions и Events. Там видно, проходят ли проверки и сколько раз они упали. Дополнительно можно смотреть метрику kube_pod_container_status_ready в Prometheus - она показывает историю изменений состояния.
Итог
Проверки состояния подов - не формальность и не галочка в чеклисте деплоя. Это механизм, который определяет, получат ли пользователи ошибку во время rolling update, насколько быстро система восстановится после зависания и будет ли трафик идти только на готовые экземпляры.
Главное, что нужно запомнить: readiness и liveness решают разные задачи и должны проверять разные вещи. Liveness - минимальная проверка жизнеспособности процесса. Readiness - проверка готовности обслуживать запросы, включая внешние зависимости. StartupProbe - буфер для медленного старта, который не жертвует скоростью обнаружения проблем после него. А graceful shutdown требует отдельного внимания независимо от того, насколько хорошо настроены сами проверки.