Probes и лимиты ресурсов
Readiness гейтит трафик, liveness рестартит, startup щитит медленный старт; requests планируют, limits ограничивают — превышение CPU даёт throttling, памяти — OOMKilled. Настрой неверно — здоровое приложение уйдёт в CrashLoop или будет вытеснено.
Сервис отгрузился нормально, а в 3 ночи поды начинают мигать. На дашборде CrashLoopBackOff растёт по всем репликам сразу. Никто не пушил код. Причина: livenessProbe с initialDelaySeconds: 5 на приложении, которому нужно 25 секунд, чтобы прогреть JIT и подключиться к базе. Каждый запуск проба падала три раза до того, как приложение было готово, Kubernetes считал контейнер мёртвым и рестартил его — бесконечно. Совершенно здоровое приложение казнила его собственная проверка здоровья. Фикс — четыре строки YAML. Урок занял ночь.
Три probe, отвечающие на три разных вопроса
Прежде чем настраивать любую probe, зафиксируй, на какой именно вопрос ты отвечаешь — потому что три probe выглядят похоже, но имеют противоположные последствия при срабатывании.
Kubernetes не считает запущенный контейнер работающим. Он спрашивает, раз за разом, через probes — и тип probe решает, что произойдёт, когда ответ «нет». Путаница между ними — главный источник самонанесённых аварий в этом уроке.
Readiness спрашивает может ли этот под обслуживать трафик прямо сейчас? Когда она падает, Kubernetes убирает под из endpoints’ов Service — трафик к нему прекращается, — но контейнер не рестартит. Под остаётся живым, просто вне ротации, и возвращается в момент, когда readiness снова проходит. Это гейт для роллаутов (новый под не получает трафик, пока реально не готов) и для временной перегрузки (под под давлением GC может сбросить трафик, не умирая). Сделай readiness отражающей реальную готовность: приложение забиндило порт, прогнало миграции и подтвердило, что критичные зависимости достижимы.
Liveness спрашивает жив ли ещё этот контейнер, или он завис? Когда она падает (за порогом), Kubernetes рестартит контейнер. Она существует ради одной задачи: восстановление из неустранимых состояний — дедлок, event loop, который перестал крутиться, утечка памяти, подвесившая процесс. Liveness — это кувалда. Если медленное-но-здоровое приложение или временный сбой зависимости её триггерит, ты получаешь тот самый restart loop из хука.
Startup спрашивает закончил ли этот контейнер загрузку? Она бежит первой; пока она падает, liveness и readiness probes полностью придержаны. Как только startup прошла один раз, она больше не запускается, и две другие берут управление. Это правильный щит для медленно стартующих приложений — старых JVM, всего с долгим прогревом, — чтобы держать liveness агрессивной для рантайм-сбоев, не давая ей убивать приложение во время старта.
containers:
- name: api
image: registry.example.com/api:1.4.0
startupProbe: # щит старта: до 30 × 10s = 5 мин на оживление
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
failureThreshold: 30
readinessProbe: # гейт трафика; падение убирает из Service, без рестарта
httpGet: { path: /ready, port: 8080 }
periodSeconds: 5
failureThreshold: 3
livenessProbe: # рестарт, если завис; падение убивает + рестартит контейнер
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
failureThreshold: 3▸Почему это работает
Две ловушки probe бьют сеньоров. Первая — liveness, указывающая на зависимость: если твой /healthz проверяет базу, а у базы 30-секундная икота, liveness каждого пода падает разом, и Kubernetes рестартит весь твой флот — превращая короткий сбой зависимости в самонанесённую каскадную аварию. Liveness должна тестировать только сам процесс; пусть readiness отражает зависимости. Вторая — переиспользование одного эндпоинта для обоих: если /healthz делает тяжёлые проверки зависимостей и ты вешаешь на него liveness, ты тихо поставил liveness в зависимость от внешнего мира. Держи дешёвый локальный liveness-эндпоинт и более богатый readiness.
requests и limits: планирование против жёсткого потолка
У ресурсов два числа на контейнер, и они делают совершенно разную работу.
requests — это то, что поду гарантировано, и то, что планировщик использует для размещения: Kubernetes поставит под только на ноду, у которой свободно как минимум requests.cpu и requests.memory (суммарно по ноде). Это резервирование, а не потолок. Ставь requests на типичное, устойчивое потребление контейнера.
limits — это жёсткий потолок, который контейнер не может превысить в рантайме, — и то, что происходит при попытке, люди понимают неправильно, потому что CPU и память ведут себя противоположно:
- CPU сверх лимита → throttling, не убийство. CPU сжимаемый. Превысишь
limits.cpu— и CFS-планировщик ядра (Completely Fair Scheduler — планировщик процессорного времени Linux) просто даёт контейнеру меньше тайм-слотов; приложение продолжает работать, но становится медленным. Заниженный CPU-лимит проявляется как загадочная латентность, а не как падения. - Память сверх лимита → OOMKilled. Память несжимаема — нельзя дать процессу «меньше» RAM, чем он уже выделил. Превысишь
limits.memory— и OOM-киллер ядра завершает контейнер с кодом выхода 137; ты видишьOOMKilledи рестарт. Заниженный лимит памяти проявляется как падения, а не как медлительность.
resources:
requests: # гарантия + планирование — ставь ≈ типичное потребление
cpu: "250m"
memory: "256Mi"
limits: # жёсткий потолок — защищает ноду от убежавшего контейнера
cpu: "1" # сверху → throttling CFS (медленно, не убит)
memory: "512Mi" # сверху → OOMKilled (выход 137, контейнер рестартит)Правило сеньора: ставь requests ≈ типичное, чтобы планировщик честно паковал ноду, и ставь limits достаточно высоко, чтобы поглотить всплески, но достаточно низко, чтобы защитить ноду. Дальше следи за двумя сигналами сбоя — повторяющийся OOMKilled означает, что лимит памяти слишком низок (или есть утечка); устойчивый CPU throttling означает, что CPU-лимит слишком низок. Настраивай по реальным метрикам, а не по догадкам.
QoS-классы решают, кто умрёт первым
Соотношение requests и limits тихо назначает каждому поду класс качества обслуживания (QoS), и этот класс задаёт порядок вытеснения, когда у ноды кончается память:
- Guaranteed — у каждого контейнера
requests == limitsи по CPU, и по памяти. Вытесняется последним. Используй для критичных по латентности, stateful-нагрузок. - Burstable — задан хотя бы один request, но не равный limits (частый случай). Вытесняется после BestEffort, в порядке того, насколько он вышел за свои requests.
- BestEffort — никаких requests и limits вообще. Первый на вытеснение, первый на OOM-kill под давлением ноды. Годится только для одноразовой batch-работы.
Все три класса вместе означают, что судьбу pod-а решает не только планировщик — OOM-киллер ядра тоже, и он использует класс, чтобы выбрать, кто умрёт первым. Без явных requests и limits ты случайно получаешь BestEffort, что делает твой pod первой жертвой при всплеске «шумного соседа».
| Параметр | requests | limits |
|---|---|---|
| Роль | Гарантированное резервирование; драйвит планирование | Жёсткий рантайм-потолок на контейнер |
| Использует планировщик? | Да — под попадёт только на ноду со столькими свободными | Нет — применяется в рантайме ядром |
| Сверх CPU | н/д (request — это пол) | throttling CFS — приложение тормозит, остаётся живым |
| Сверх памяти | н/д | OOMKilled (выход 137) — контейнер рестартит |
| Ставь в | ≈ типичное устойчивое потребление | Высоко для всплесков, низко для защиты ноды |
Медленно стартующему JVM-сервису (прогрев 25с) нужно восстанавливаться из редких дедлоков, не мигая на каждом рестарте. Как настроить его probes?
Контейнер под нагрузкой раз за разом завершается с кодом выхода 137, затем рестартит. О чём это говорит?
- 01Здоровое приложение застряло в CrashLoopBackOff сразу после деплоя. Пройдись по misconfiguration probe, который это вызывает, и по точному фиксу.
- 02В чём разница между OOMKilled и throttling контейнера и что каждое говорит изменить?
Kubernetes держит поды здоровыми и правильно размеренными двумя независимыми инструментами, и сбои идут от путаницы случаев. Теперь, когда встретишь CrashLoopBackOff на только что задеплоенном сервисе, твой первый вопрос: есть ли у liveness-пробы запас на реальное время загрузки — и не направлена ли она на зависимость? Есть три probes, отвечающие на три вопроса: readiness («может ли обслуживать?» — падение убирает под из endpoints’ов Service, но не рестартит, гейт для роллаутов и сброса временной нагрузки), liveness («завис ли он?» — падение рестартит контейнер, только для восстановления из дедлоков) и startup («загрузился ли он?» — бежит первой и придерживает liveness и readiness до конца старта, щит для медленно стартующих приложений). Две ловушки, кладущие здоровые флоты, — это слишком агрессивная liveness-проба, гоняющая в CrashLoop медленное-но-здоровое приложение, и liveness, направленная на зависимость, которая рестартит все поды разом при кратком сбое зависимости — держи liveness дешёвой и локальной, пусть readiness отражает зависимости. По ресурсам: requests — это гарантированное резервирование, которое планировщик использует для размещения (ставь их около типичного потребления), а limits — жёсткий рантайм-потолок, но два ресурса применяются противоположно: превысишь CPU-лимит — тебя троттлит CFS (медленно, не убит), превысишь лимит памяти — тебя OOMKilled с кодом выхода 137 (убит и рестартнут). Соотношение requests к limits назначает QoS-класс — Guaranteed (requests == limits, вытесняется последним), Burstable или BestEffort (без limits, вытесняется первым), — который задаёт порядок гибели подов под давлением ноды. Следи за повторяющимися OOMKill (лимит памяти слишком низок) и устойчивым throttling (CPU-лимит слишком низок) и настраивай по метрикам, а не по догадкам.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.