Readiness и Liveness Probes в Kubernetes: почему их путают
На примере FastAPI и Kubernetes разбираем, как общая проверка готовности вызывает лишние перезапуски контейнера и как это исправить.
Когда неготовность вызывает перезапуск
Приложение запущено и отвечает на HTTP-запросы, но пока не может обрабатывать полезную нагрузку: например, ещё загружает необходимые данные. В такой ситуации его нужно временно исключить из маршрутизации Service. Для этого в Kubernetes используют readinessProbe.
Проблема возникает, когда маршрут /ready одновременно указывают в livenessProbe. Временная неготовность тогда становится поводом для перезапуска контейнера. Если причина неготовности сохраняется, новый процесс столкнётся с той же проблемой.
Разберём ошибку на одном приложении: сравним две конфигурации Deployment, проверим состояние Pod и EndpointSlice, а затем исправим настройки. Результаты получены в локальном кластере kind с одной репликой. Важны не сами интервалы между проверками, а разница в реакции Kubernetes на один и тот же отказ.
Что делает каждая проверка
readinessProbe определяет, можно ли направлять запросы через Service в этот Pod. Когда проверка не проходит, контейнер считается неготовым; сам отказ readiness не требует перезапуска контейнера. Состояние отражается в условии Ready у Pod и в данных EndpointSlice.
livenessProbe отвечает на другой вопрос: следует ли перезапустить контейнер. Если число последовательных неуспехов достигает failureThreshold, kubelet запускает процедуру перезапуска с учётом политики Pod. Она не дожидается успеха readiness. Проверка нужна при неисправности, которую может устранить перезапуск, например при зависании обработчика. Более высокий порог лишь откладывает перезапуск: если причина не зависит от состояния процесса, проблема останется.
Для HTTP-проверки httpGet ответ со статусом от 200 до 399 означает успех, а 503 — неуспех. Kubelet оценивает статус согласно типу проверки, не зная, почему приложение вернуло ошибку. Подробности приведены в описании проверок Kubernetes и руководстве по их настройке.
Сигнал: readinessProbe
При неуспехе: Pod исключается из обычной маршрутизации
Что наблюдать: Ready у Pod, EndpointSlice.conditions.ready
Сигнал: livenessProbe
При неуспехе: После порога kubelet перезапускает контейнер
Что наблюдать: restartCount, события Kubernetes
Сигнал: startupProbe
При неуспехе: До её успеха остальные две проверки не выполняются
Что наблюдать: Состояние запуска, события Kubernetes
Теперь посмотрим, к чему эти правила привели в локальном кластере.
Одно приложение, два состояния
Для эксперимента достаточно небольшого приложения на FastAPI. Маршрут /ready возвращает 503, если существует файл /state/unready. Независимый маршрут /live возвращает 503 при появлении /state/unlive; он понадобится позднее, чтобы проверить действие liveness. Пока файлов нет, оба маршрута отвечают 200.
Эти файлы имитируют два разных состояния приложения, а не настоящий сбой базы данных или зависание процесса. В Kubernetes они размещены на томе emptyDir. Как указано в документации томов, emptyDir переживает сбой контейнера в рамках прежнего Pod, но удаляется вместе с Pod. Поэтому перезапуск контейнера не сбросит искусственную неготовность.
Файл probe_app.py:
После установки зависимостей (шаги приведены ниже) приложение можно запустить командой PORT=8080 UNREADY_FILE=/tmp/unready LIVE_FAIL_FILE=/tmp/unlive .venv/bin/python probe_app.py. Создавайте и удаляйте файлы /tmp/unready и /tmp/unlive с помощью touch и rm, а ответы проверяйте через curl -i localhost:8080/ready и curl -i localhost:8080/live.
Проверка HTTP-ответов. Локальный тест FastAPI TestClient показал следующую последовательность статусов (/live, /ready): (200, 200) → (200, 503) → (200, 200). Отдельный отказ живости дал (503, 200), затем оба маршрута снова вернули 200. Неизвестный путь вернул 404. Проверка прошла также в контейнере с Python 3.12.12 и закреплёнными зависимостями. TestClient обращается к приложению без сетевого порта: о поведении Kubernetes эти результаты ничего не говорят. Подробнее о методе — в документации FastAPI.
Ошибка в Deployment: обе проверки обращаются к /ready
Исходники приложения, Dockerfile и манифесты находятся в проверенной версии учебного проекта. Понадобятся Docker, kind и kubectl, совместимый с кластером. Руководство kind объясняет создание локального кластера и загрузку образа. Команды ниже выполняются из корня проекта. Базовый образ Python 3.12.12 закреплён по SHA-256, контейнер запускается без прав root.
Убедитесь, что текущий контекст — kind-probes-lab. Проверка --dry-run=server подтверждает, что API кластера принимает манифесты, но ещё не показывает поведение запущенного приложения.
Файл bad.yaml создаёт Deployment с одной репликой:
service.yaml:
В этой конфигурации обе проверки обращаются к /ready. При создании файла /state/unready обе начнут получать 503, хотя /live продолжит возвращать 200. Файл сохранится после перезапуска контейнера в том же Pod.
Прежде чем включать отказ, зафиксируем состояние Pod и EndpointSlice. Это позволит связать будущие перезапуски с конкретным контейнером.
Что показали Pod, события и EndpointSlice
Перед созданием файла /state/unready запишите исходное состояние Pod и EndpointSlice. Сохраните время наблюдения, UID Pod и restartCount: без исходных значений нельзя доказать, что перезапускался именно контейнер в прежнем Pod. Имя Pod возьмите из вывода предыдущей команды.
В JSON Pod проверьте условие Ready в status.conditions, счётчик status.containerStatuses[].restartCount и lastState.terminated. Сообщения о неудачных проверках и действиях kubelet ищите в событиях Kubernetes. Само по себе Ready=false не означает, что контейнер перезапускался; рост restartCount нужно сопоставить с событиями и неизменным UID Pod.
Для Service смотрите EndpointSlice.endpoints[].conditions.ready. Адрес Pod может оставаться в EndpointSlice даже при ready=false: важно именно условие готовности. В документации EndpointSlice описаны исключения. При publishNotReadyAddresses=true значение ready принудительно устанавливается в true; для завершающихся Pod дополнительно важны serving и terminating.
Результат эксперимента. 24 октября 2026 года в kind v0.33.0 с Kubernetes v1.37.0 проверялся Pod с одной репликой. До отказа у него были Ready=true, restartCount=0 и EndpointSlice.ready=true. Через 75 секунд после создания /state/unready UID остался тем же, Ready=false, restartCount=4, а EndpointSlice показал ready=false, serving=false, terminating=false. События содержали сообщения Liveness probe failed: HTTP probe failed with statuscode: 503 и Killing Container web failed liveness probe. Файл unready сохранился в emptyDir после перезапусков: это дополнительно проверено на узле kind.
К концу наблюдения контейнер находился в состоянии CrashLoopBackOff. В таком состоянии kubectl exec может не сработать. Для восстановления примените исправленный Deployment из следующего раздела: Kubernetes создаст новый Pod, а сравнение начнётся с его исходных показателей.
Исправление: liveness проверяет /live
В good.yaml параметры и условия готовности остаются прежними. Изменяется только адрес livenessProbe: вместо /ready она обращается к /live.
Это фрагмент конфигурации контейнера; полный good.yaml уже включён в проект. readinessProbe по-прежнему проверяет готовность, а livenessProbe больше не зависит от файла unready.
Изменение livenessProbe обновляет шаблон Deployment, поэтому Kubernetes создаёт новый Pod. У него свой UID, новый том emptyDir и счётчик перезапусков. Сравнивайте изменения счётчика в пределах одного Pod, а не абсолютные значения до и после обновления Deployment.
Результат после исправления. Новый Pod начал с Ready=true и restartCount=0. Через 75 секунд с файлом unready тот же Pod имел Ready=false, restartCount=0, а EndpointSlice — ready=false, serving=false, terminating=false. После удаления файла готовность Pod и endpoint восстановилась, перезапусков не было. Таким образом, отказ readiness больше не приводит к перезапуску работающего контейнера.
Осталось убедиться, что livenessProbe действительно вызывает перезапуск при отказе своего маршрута. Создайте /state/unlive: только /live начнёт возвращать 503. Сравните UID и restartCount через те же 75 секунд. После проверки удалите файл; если kubectl exec недоступен во время перезапуска, повторите команду после запуска контейнера. Этот контроль проверяет реакцию kubelet на отказ HTTP-маршрута, а не реальное зависание процесса.
Результат проверки liveness. В отдельном контроле с /state/unlive через 75 секунд тот же Pod UID a34c5da6-4222-49b9-b2b4-8a5f7819c5e7 имел restartCount=4 вместо 0. В событиях появилось Container web failed liveness probe, will be restarted. Во время снимка контейнер находился в CrashLoopBackOff, поэтому Pod и endpoint стали неготовыми. Хотя /ready не зависит от unlive, остановленный контейнер не может ответить на проверку готовности. После удаления файла Pod и EndpointSlice восстановились.
Если после удаления флага готовность не восстанавливается, проверьте расположение файла UNREADY_FILE, подключение emptyDir, логи контейнера и события Kubernetes. После завершения опыта локальный кластер можно удалить командой kind delete cluster --name probes-lab.
Долгий запуск и пределы проверки
При длительной инициализации livenessProbe может сработать до того, как приложение успеет запуститься. Для этого существует startupProbe: пока она не завершилась успешно, Kubernetes не выполняет readiness- и liveness-проверки. Если же startupProbe сама превышает порог неудач, контейнер перезапускается. Такое поведение описано в документации Kubernetes.
Общий маршрут для readiness и liveness сам по себе не запрещён: он подходит, если продолжительная неуспешная проверка действительно означает необходимость перезапуска. Здесь /ready зависит от файла в emptyDir, который остаётся на месте после перезапуска контейнера. Поэтому liveness на этом маршруте приводит к повторению той же ошибки.
В опыте использовался один Pod и искусственные файлы-флаги вместо реальной внешней зависимости. Мы сравнили состояния через 75 секунд, но не измеряли задержки выполнения проверок, обновления EndpointSlice или доставки трафика. Также не проверялись поведение многорепличного Service, существующие клиентские соединения и потери запросов. Полученные числа показывают реакцию конкретного кластера на два заданных условия; они не задают универсальных значений periodSeconds или failureThreshold.
Итог: готовность и перезапуск — разные решения
В локальном kind-кластере одна и та же искусственная неготовность привела к разным результатам в зависимости от конфигурации:
Проверяемое состояние: /ready = 503; обе проверки смотрят на /ready
Ready через 75 секунд: false
restartCount одного Pod: 0 → 4
Проверяемое состояние: /ready = 503; liveness смотрит на /live
Ready через 75 секунд: false
restartCount одного Pod: 0 → 0
Проверяемое состояние: /live = 503; liveness смотрит на /live
Ready через 75 секунд: false
restartCount одного Pod: 0 → 4
В каждом случае сравнивались значения внутри одного Pod UID. При замене Deployment Pod создавался заново. Результаты подтверждают, что причиной лишних перезапусков была привязка liveness к неготовности, которую перезапуск контейнера не устранял.
При настройке собственных проверок сначала определите нужное действие: временно исключить Pod из маршрутизации или перезапустить контейнер. Затем выберите условие, соответствующее этому действию, и проверьте его на реальном отказе. Отдельный маршрут /live полезен только тогда, когда его неуспех действительно указывает на проблему, которую перезапуск может исправить.
Источники
- Контракт readiness/liveness/startup и успешность HTTP probes
- Конфигурация и параметры проверок kubelet
- Условия ready/serving/terminating и publishNotReadyAddresses
- emptyDir сохраняет состояние при рестарте контейнера внутри Pod
- Официальная процедура kind create cluster и kind load docker-image
- Локальная HTTP-проверка через FastAPI TestClient
- Запуск FastAPI через ASGI-сервер Uvicorn
- Публичный учебный проект с исполняемым примером и манифестами
- Точный проверенный commit учебного проекта