CrashLoop по выходным: агрессивный livenessProbe
Почему сервис падает без нагрузки и как подобрать параметры probe под холодный старт.
Алерт приходит в субботу ночью: сервис в CrashLoopBackOff, счётчик рестартов растёт. К понедельнику всё само чинится, и тикет закрывают как «непонятно, воспроизвести не удалось». Через неделю повторяется.
Привязка ко времени суток это не мистика, а подсказка. Симптом «работает под нагрузкой, падает в покое» почти всегда означает liveness плюс холодный старт.
Почему «по выходным» это диагноз
В будни на сервис идёт трафик. JIT прогрет, кэши заполнены, коннекты к базе установлены, health-check отвечает за 50 мс. В выходные нагрузки нет: Pod пересоздаётся (rolling update, eviction, обслуживание ноды) и новый контейнер стартует с холодного. Первый запрос после старта может ехать секунд десять.
Если livenessProbe бьёт каждые periodSeconds: 10 и терпит
failureThreshold: 3 промаха, kubelet убьёт контейнер секунд через 30
после старта. Новый контейнер стартует так же медленно и умирает так
же. Круг замкнулся.
Реже за тем же симптомом стоит утечка памяти, которую в будни маскирует GC. Ещё реже таймер, который тикает только без трафика.
Какие параметры probe ломают всё
initialDelaySeconds
Сколько ждать до первого опроса. Дефолт 0 и это плохой выбор: Pod ещё не открыл порт, probe ловит refused и считает это провалом. На холодном старте ставят с запасом: 30 для типичного http-сервиса, 60-120 для JVM.
periodSeconds
Интервал между опросами. Дефолт 10. Уменьшать ниже 5 редко имеет смысл: получаешь нагрузку на приложение без полезного сигнала.
timeoutSeconds
Сколько ждать ответа от probe. Дефолт 1. Маленькое значение для тяжёлой Java или нагруженного Python превращает медленный, но живой ответ в провал. Нормальный диапазон 3-5.
failureThreshold
Сколько неудач подряд считается падением probe. Дефолт 3. При значении 1 контейнер убьёт любая мелкая ошибка. Здравая середина 3-5.
Формула «когда kubelet убьёт контейнер»
totalTimeToKill ≈ initialDelaySeconds + (periodSeconds × failureThreshold)
Агрессивная probe:
livenessProbe:
httpGet: { path: /, port: 80 }
initialDelaySeconds: 0
periodSeconds: 5
timeoutSeconds: 1
failureThreshold: 2
Время до kill: 0 + 5*2 = 10 секунд. Любое приложение со стартом
дольше 10 секунд гарантированно уходит в CrashLoop.
Путь probe важен не меньше таймингов. / на веб-сервере отвечает 200
раньше, чем приложение готово, а health-эндпоинт с походом в базу
превращает деградацию базы в одновременный перезапуск всех Pod.
Безопаснее:
livenessProbe:
httpGet: { path: /healthz, port: 80 }
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
Время до kill: 30 + 10*3 = 60 секунд. Старт до минуты выживет.
холодный старт первый ответ ~10 сек
агрессивная probe 0 + 5×2 = 10 сек
безопасная probe 30 + 10×3 = 60 сек
Как это выглядит в диагностике
# Сколько раз контейнер уже перезапускался
kubectl get pod -l app=web
# Свежий exit-код и причина
kubectl describe pod -l app=web | grep -A5 -E 'Last State|Reason|Exit Code'
# События про probe
kubectl describe pod -l app=web | grep -A2 Probe
# Логи предыдущего контейнера, уже убитого
kubectl logs -l app=web --previous
В kubectl describe pod появится строка вроде:
Warning Unhealthy kubelet Liveness probe failed: Get "http://10.42.0.17:80/": context deadline exceeded
Это значит, что probe не дождалась ответа. Если рядом стоит
Last State: Terminated с Reason: Error и Exit Code: 137, контейнер
получил SIGKILL. Приложение не падало, его убил kubelet.
Логи предыдущего контейнера дают вторую половину картины. Если там нормальный старт без ошибок и обрыв на середине инициализации, версия с probe подтверждается.
startupProbe как отдельный инструмент
Появилась в 1.16 как alpha, стабильна с 1.20. Пока startupProbe не вернула success, liveness и readiness даже не запускаются. Приложение прогревается, агрессивный liveness в это время не работает. Подходит для очень долгого старта: Java, ML-модели, прогрев индексов.
startupProbe:
httpGet: { path: /healthz, port: 80 }
periodSeconds: 5
failureThreshold: 30 # 30 * 5 = 150 секунд на прогрев
livenessProbe:
httpGet: { path: /healthz, port: 80 }
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3 # обычная жёсткая проверка после прогрева
Приложение получает до 150 секунд на холодный старт, а зависший в рантайме контейнер всё равно перезапустится за 30 секунд.
Как подбирать параметры
- Замерь холодный старт:
time curl localhost:port/healthzсразу после запуска. Удвой результат и поставь вinitialDelaySeconds. - Время ответа здорового приложения умножь на 3 и поставь в
timeoutSeconds. failureThreshold: 3это здравый дефолт для большинства сервисов.periodSeconds: 10без причины не уменьшай.- Старт дольше 30 секунд выноси в startupProbe, а liveness оставляй жёстким.
Домашка
Теорию закрепляет задание на своём кластере: 1 шаг с проверкой. Кластер поднимается по треку Kubernetes локально, задания лежат в репозитории k8s-practice.
./run.sh start k8s-weekend-crashloop # развернуть стартовое состояние
./run.sh verify k8s-weekend-crashloop # проверить текущий шаг
Коротко
- Падения без нагрузки указывают на связку liveness и холодного старта.
Exit Code: 137плюсLiveness probe failedозначает, что контейнер убил kubelet, а не приложение.kubectl logs --previousпоказывает, что происходило до рестарта.- Время до kill считается как
initialDelaySeconds + periodSeconds × failureThreshold. - Долгий старт лечится startupProbe, а не бесконечным
initialDelaySeconds.
Прошли материал до конца? Отметьте урок пройденным.
Прогресс сохранён в этом браузере
Почистишь историю или откроешь с телефона, и отметки пропадут. Оставь почту, они переедут в аккаунт. Пароль придумывать не нужно.
Готово. теперь в аккаунте: прогресс виден с любого устройства. Мой прогресс