Приложение стартует пять минут и падает в CrashLoopBackOff: настраиваем startupProbe, не ломая быструю liveness
Релиз выкатили, под поднялся, две минуты крутил миграции — и его убили. Потом ещё раз. Потом ещё, уже с растущей паузой между попытками. В логах ничего криминального, приложение просто не успевает проснуться. Это не баг приложения и не баг кластера — это арифметика проверок состояния, которую почти никто не считает. Ниже разбираю, как я развожу медленный старт и быстрое обнаружение зависаний по разным пробам, как считаю окно запуска и что при этом ломается в реальных стендах.
Что на самом деле происходит: арифметика, а не мистика
Типовая картина, которую я вижу у клиентов раз в квартал. Java-приложение на старте прогревает пул соединений, накатывает миграции Liquibase, поднимает кэш из базы — суммарно две-четыре минуты. В манифесте стоит livenessProbe с initialDelaySeconds: 30, periodSeconds: 10, failureThreshold: 3. Считаем: kubelet ждёт 30 секунд, потом стучится в /healthz каждые 10 секунд, три подряд неуспеха — и контейнер убит. То есть приговор выносится на шестидесятой секунде жизни. Приложению до готовности ещё две минуты, а его уже нет.
Дальше включается механика рестартов. Контейнер убит, применяется restartPolicy пода, kubelet поднимает его заново — и всё повторяется с нуля: миграции, прогрев, шестьдесят секунд, смерть. После нескольких таких циклов kubelet начинает наращивать паузу между перезапусками экспоненциально (по умолчанию 10, 20, 40 секунд и так далее, потолок — 300 секунд), и в kubectl get pods появляется тот самый статус CrashLoopBackOff. Приложение при этом абсолютно исправно. Его просто не пускают дожить до готовности.
Самое неприятное — как это выглядит в диагностике. kubectl logs показывает обрывок нормального старта без единой ошибки. kubectl logs --previous — то же самое. Люди начинают искать проблему в базе, в сети, в образе, поднимают уровень логирования. А ответ лежит в kubectl describe pod в разделе Events, двумя строчками:
kubectl describe pod app-7c9f8d5b4-x2ktq | sed -n '/Events:/,$p'
# Warning Unhealthy 62s kubelet Liveness probe failed: Get "http://10.244.3.17:8080/healthz": context deadline exceeded
# Normal Killing 62s kubelet Container app failed liveness probe, will be restartedВот эта пара «Unhealthy → Killing» — единственный надёжный признак того, что контейнер убила проба, а не OOM-киллер и не сам процесс. Если в Events её нет, а под всё равно рестартует — ищите Reason: OOMKilled в kubectl get pod -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}', это совсем другая история.
Отдельно про сам статус. CrashLoopBackOff — это не причина, а состояние ожидания: kubelet выдерживает паузу перед следующим запуском. По документации pod lifecycle пауза начинается с 10 секунд, удваивается после каждого рестарта и упирается в потолок 300 секунд, то есть пять минут. Счётчик сбрасывается, если контейнер после запуска проработал без проблем 10 минут. Поэтому под, который убивают на шестидесятой секунде, из этой спирали сам не выйдет никогда — он просто не доживает до сброса.
В новых версиях эту кривую начали тюнить (KEP-4603), и тут важно не спутать, что включено по умолчанию, а что нет. Feature gate KubeletCrashLoopBackOffMax появился в 1.32 как alpha, а с 1.35 он beta и включён по умолчанию: он позволяет на уровне ноды опустить потолок через crashLoopBackOff.maxContainerRestartPeriod в конфигурации kubelet, допустимо от 1 до 300 секунд, при этом старт остаётся 10 секунд. Feature gate ReduceDefaultCrashLoopBackOffDecay — alpha с 1.33, выключен по умолчанию: он меняет кривую для всего кластера на 1 секунду на старте и 60 секунд максимум. Ни то, ни другое не лечит неправильную пробу — вы просто быстрее и чаще убиваете здоровое приложение.
# фрагмент KubeletConfiguration на ноде (при включённом KubeletCrashLoopBackOffMax)
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
crashLoopBackOff:
maxContainerRestartPeriod: "60s"- «Unhealthy» + «Killing ... failed liveness probe» — виновата проба;
- «OOMKilled» в lastState — не хватило памяти, пробы ни при чём;
- «Back-off restarting failed container» без Unhealthy — процесс упал сам, смотрите logs --previous;
- Exit code 143 (SIGTERM) или 137 (SIGKILL после grace period) — контейнер завершили снаружи; вместе с «Killing» в Events это почти всегда проба.
Почему я не лечу это через initialDelaySeconds
Первое, что делает разработчик, — увеличивает initialDelaySeconds со тридцати до трёхсот. Работает. Под перестаёт падать. И вместе с этим вы получаете пять минут, в течение которых kubelet вообще не проверяет контейнер на зависание — каждый раз, всю жизнь пода, а не только на первом старте. Приложение повисло на сорок восьмой секунде после запуска? Кластер узнает об этом на триста тридцатой. Умножьте на количество реплик и на частоту релизов.
Проблема глубже, чем «пять минут вслепую». initialDelaySeconds — это константа, которую вы подбираете по худшему наблюдаемому случаю. Худший случай меняется: база под нагрузкой отвечает медленнее, миграция стала тяжелее, нода перегружена, образ раскатывается на холодный кэш. Вы либо ставите запас с потолка и теряете чувствительность к зависаниям, либо не ставите — и ловите CrashLoop в пиковый день. Компромисса тут нет, потому что одним числом вы пытаетесь описать два разных режима работы: старт и установившийся режим.
Именно для этого в Kubernetes есть третья проба — startupProbe. Её смысл ровно один: пока startupProbe не отдала первый успешный результат, kubelet не выполняет ни livenessProbe, ни readinessProbe. В документации 1.35 это сформулировано прямо: «Kubernetes does not execute liveness or readiness probes until the startup probe succeeds». Как только startupProbe прошла — она больше не запускается никогда, и дальше за зависания отвечает livenessProbe со своими, уже жёсткими порогами.
То есть вы разводите два вопроса по двум механизмам. «Приложение ещё поднимается или уже сдохло на старте?» — это startupProbe с широким окном. «Живой процесс завис после трёх недель аптайма?» — это livenessProbe с окном в тридцать секунд. Одно другому не мешает, потому что они физически не работают одновременно.
- initialDelaySeconds откладывает первую проверку при каждом старте контейнера — и всё это время зависание не детектируется;
- значение подбирается по худшему случаю, который со временем растёт вместе с данными и миграциями;
- startupProbe срабатывает только до первого успеха, после неё сразу включается строгая liveness;
- до успеха startupProbe под не попадает в endpoints Service, трафик на недогретое приложение не идёт.
Как считать окно запуска и какие поля реально важны
Окно запуска считается как failureThreshold × periodSeconds (плюс initialDelaySeconds, если он у startupProbe задан — обычно я его не задаю). В официальном примере документации стоят failureThreshold: 30 и periodSeconds: 10, что даёт ровно 300 секунд — пять минут на инициализацию. Формулировка из докуменации: задайте startupProbe с теми же проверками, что и liveness, «with a failureThreshold × periodSeconds long enough to cover the worse case startup time».
Я беру не средний старт, а худший наблюдаемый, и умножаю на два. Не потому, что люблю запас, а потому что цена ошибки асимметрична. Слишком большое окно стоит вам задержки обнаружения битого релиза — под будет пять минут висеть в Not Ready, и вы это увидите в мониторинге деплоя. Слишком маленькое окно стоит вам CrashLoopBackOff, то есть релиза, который не выкатится вообще. Первое — неудобство, второе — инцидент.
Значения по умолчанию, которые надо держать в голове (они одинаковы для всех трёх проб): initialDelaySeconds — 0, periodSeconds — 10 (минимум 1), timeoutSeconds — 1 (минимум 1), successThreshold — 1 (минимум 1), failureThreshold — 3 (минимум 1). Для livenessProbe и startupProbe successThreshold обязан быть равен 1 — API-сервер отклонит манифест с другим значением, это единственное место, где можно нарваться на ошибку валидации.
Дефолт timeoutSeconds: 1 — самая частая мина. На старте приложение занято миграциями и прогревом, JIT ещё не разогрелся, CPU упирается в лимит — HTTP-эндпоинт отвечает не за 20 миллисекунд, а за полторы секунды. Проба валится по таймауту, хотя приложение живое. Для startupProbe я всегда ставлю timeoutSeconds в 3-5 секунд, для liveness — 2-3. Это не «на всякий случай», это компенсация того, что на старте контейнер работает в самых тяжёлых для себя условиях.
- startupProbe: широкое окно, failureThreshold 30-60, periodSeconds 5-10, timeoutSeconds 3-5;
- livenessProbe: быстрая, periodSeconds 10, failureThreshold 3, timeoutSeconds 2-3 — детект за ~30 секунд;
- readinessProbe: periodSeconds 5, failureThreshold 3, successThreshold можно и 2, если хотите защиты от дребезга;
- successThreshold для liveness и startup — только 1, иначе манифест не пройдёт валидацию.
Разбор из практики: школа вокала, 19 рабочих мест, сайт с онлайн-записью у подрядчика
Клиент — школа вокала «ВокалГрад», 19 рабочих мест: администраторы, педагоги, бухгалтерия. Офисную инфраструктуру ведём мы, а сайт с онлайн-записью на занятия школе делал и поддерживает сторонний подрядчик: Spring Boot 3, PostgreSQL 16, три реплики в управляемом Kubernetes 1.35 у облачного провайдера. Доступа к кластеру у нас изначально не было — школа пришла с жалобой «после обновления сайта запись то работает, то нет, подрядчик откатывает релизы вручную». Подрядчик выдал нам read-only доступ к namespace, и дальше разбирались вместе. Картина в кластере: из трёх реплик нового релиза поднималась одна, остальные уходили в CrashLoopBackOff, rollout не завершался и отваливался по progressDeadlineSeconds.
Замерили холодный старт — подрядчик запустил контейнер с отключёнными пробами в тестовом namespace, засекли время до первого 200 на /actuator/health. Получилось 2 минуты 10 секунд днём и 2 минуты 50 секунд ночью, когда на той же базе шла выгрузка расписания педагогов и резервное копирование. В манифесте при этом стояло: liveness с initialDelaySeconds: 45, periodSeconds: 10, failureThreshold: 3, timeout по умолчанию. Окно до убийства — 75 секунд, разрыв больше чем вдвое. Причина, почему «раньше работало»: в релизе добавили миграцию Liquibase с перестройкой индекса по таблице истории записей на занятия (несколько миллионов строк за годы работы школы) и прогрев кэша расписания — старт подрос примерно на минуту и перевалил через порог.
Развели пробы. Подрядчик добавил в приложение три отдельных эндпоинта: /livez (только процесс, без похода в базу), /readyz (пул к БД + очередь) и /startupz (флаг «миграции применены, кэш прогрет»). Манифест получился такой:
containers:
- name: booking
image: registry.example.com/booking/web:2026.9.3
ports:
- name: http
containerPort: 8080
startupProbe:
httpGet:
path: /startupz
port: http
periodSeconds: 10
failureThreshold: 60 # 60 x 10 = 600 c на инициализацию
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /livez
port: http
periodSeconds: 10
failureThreshold: 3 # детект зависания ~30 c
timeoutSeconds: 3
readinessProbe:
httpGet:
path: /readyz
port: http
periodSeconds: 5
failureThreshold: 3
timeoutSeconds: 3Итог через неделю наблюдений: rollout трёх реплик стал стабильно завершаться за 4-5 минут вместо «двадцать минут и ручной откат», CrashLoopBackOff по этой причине не повторялся. Побочный эффект, которого не ждали: раньше поды попадали в Service до прогрева пула соединений, и ученики, записывавшиеся на урок сразу после деплоя, 15-20 секунд ловили ошибку 502 на форме записи — администраторы школы принимали это за «сайт снова упал». С честным /readyz это ушло. Окно 600 секунд — это примерно двукратный запас к ночному худшему случаю; снижать его не стали, запас ничего не стоит.
Честно про нашу роль: манифесты правил подрядчик, мы как ИТ-аутсорсер школы помогли локализовать проблему по Events и замерам и сформулировали требования к пробам. Для небольшой организации это типовая ситуация — кластер не свой, но ответственность перед пользователями на школе, и без разбора по фактам спор «виноват хостинг» против «виноват код» может длиться неделями.
- Events пода: пары «Liveness probe failed» → «Killing» на 75-й секунде жизни каждого контейнера;
- замер холодного старта без проб: 2:10 днём, 2:50 ночью при резервном копировании базы;
- фикс: отдельные /startupz, /livez, /readyz и startupProbe с окном 60 × 10 = 600 секунд;
- результат: rollout трёх реплик за 4-5 минут, ошибки 502 после деплоя исчезли.
Три эндпоинта, а не один: почему liveness не должна ходить в базу
Самая дорогая ошибка в пробах — не короткое окно старта, а зависимости внутри livenessProbe. Логика «пусть healthcheck проверит, что мы видим базу» кажется разумной ровно до первого замедления базы. Дальше происходит вот что: база отвечает медленнее таймаута пробы, liveness падает одновременно у всех реплик, kubelet убивает их все, они одновременно перезапускаются и одновременно ломятся в уже перегруженную базу открывать пул соединений. Из деградации вы получаете полный отказ плюс лавину переподключений.
Правило, которое я формулирую клиентам одной фразой: livenessProbe должна проверять только то, что чинится перезапуском. Зависла ли очередь событий, жив ли HTTP-поток, не залип ли пул потоков — да. Доступна ли база, отвечает ли соседний сервис, есть ли связь с брокером — нет, потому что перезапуск контейнера это никак не лечит. Ровно эта мысль — центральная в разборе «The liveness probe that makes your uptime worse»: «A liveness probe must only test conditions that a restart can fix».
Зависимости живут в readinessProbe. Провал readiness не убивает контейнер — kubelet просто помечает его как not ready, и Service перестаёт слать на него трафик. Контейнер продолжает работать, держит соединения, пишет логи, ждёт восстановления зависимости. Когда база вернулась — readiness прошла, под снова в эндпоинтах, никаких перезапусков и никакой лавины. Это принципиальная разница в последствиях: startup и liveness убивают контейнер и отдают его restartPolicy, readiness только снимает с балансировки.
startupProbe я обычно вешаю на отдельный эндпоинт, а не на тот же, что liveness. Документация допускает использовать одну и ту же проверку, и для простых приложений это нормально. Но если у вас есть явный признак «инициализация завершена» — миграции применены, кэш собран, конфигурация вычитана — лучше проверять именно его. Иначе вы рискуете получить успешную startupProbe в момент, когда HTTP-порт уже открыт, а половина инициализации ещё идёт: startup закончится, включится жёсткая liveness, и вы вернётесь к тому, с чего начали, только диагностировать станет сложнее.
- /livez — процесс жив и не завис; никаких сетевых вызовов наружу;
- /readyz — готов обслуживать: пул к БД, брокер, обязательные зависимости;
- /startupz — инициализация завершена: миграции, прогрев, загрузка справочников;
- если разделять эндпоинты негде — startup и liveness могут смотреть в одну точку, readiness всё равно отдельно.
Где это ломается на практике
CPU-лимиты. Самый коварный сценарий: контейнеру выставлен limits.cpu: 500m, на старте JVM или Node.js пытаются съесть всё, что дадут, упираются в throttling — и HTTP-эндпоинт отвечает секундами. Проба валится по таймауту, контейнер убивают, при перезапуске ситуация повторяется. Со стороны это выглядит как «приложение не стартует в кластере, а локально работает». Лечится либо повышением timeoutSeconds, либо, честнее, нормальным лимитом. Проверить просто: kubectl top pod в момент старта покажет упор в потолок, а в cgroup-метриках будет расти container_cpu_cfs_throttled_seconds_total.
Exec-пробы. exec запускает процесс внутри контейнера на каждой итерации. С periodSeconds: 5 на пятидесяти подах это десять процессов в секунду на ноду, и на слабых нодах это заметно. Плюс до Kubernetes 1.20 timeoutSeconds для exec-проб фактически не соблюдался (сейчас это регулирует feature gate ExecProbeTimeout, который включён), а неаккуратный скрипт умеет накапливать зомби-процессы. Я использую exec только там, где HTTP нет физически — иначе httpGet или tcpSocket. Помните разницу: httpGet считает успехом код 200-399, tcpSocket — сам факт открытия порта (что почти ничего не доказывает: порт слушает и у полностью зависшего приложения), grpc — статус SERVING в ответе.
Sidecar-контейнеры и init-контейнеры. Пробы задаются для каждого контейнера отдельно, и startupProbe у основного контейнера не знает ничего про sidecar. Классика: приложение стартует за 40 секунд, а sidecar с прокси — за 90, приложение не может достучаться до внешнего мира и валит собственную startup-проверку. Если инициализация действительно последовательная — выносите её в init-контейнер, там она честно блокирует старт основного, и никакие пробы для этого не нужны.
И ещё одна деталь, про которую мало кто знает: terminationGracePeriodSeconds можно задать на уровне самой пробы. Формулировка API: «Optional duration in seconds the pod needs to terminate gracefully upon probe failure». Это переопределяет значение пода именно для случая убийства по liveness/startup. Полезно, когда штатное завершение у вас длинное (дренаж соединений, тридцать секунд), но зависший по liveness процесс дренировать бессмысленно — и вы не хотите ждать эти тридцать секунд на каждом рестарте.
- CPU-лимит на старте → троттлинг → таймауты проб: поднимайте лимит или timeoutSeconds;
- exec-пробы с малым periodSeconds заметно грузят ноды — предпочитайте httpGet или grpc;
- tcpSocket доказывает только открытый порт, а не работоспособность приложения;
- зависимость от sidecar на старте — выносите последовательную инициализацию в init-контейнер;
- probe-level terminationGracePeriodSeconds позволяет не ждать долгий graceful shutdown при убийстве по пробе.
Порядок внедрения: что делать сейчас, на что можно забить
Первым делом — замерить реальное время старта. Не спросить у разработчиков, не взять из головы, а запустить контейнер и засечь. Второе — развести эндпоинты в приложении; если это невозможно прямо сейчас, ставьте startupProbe на существующий healthcheck, это всё равно лучше, чем initialDelaySeconds: 300. Третье — выставить окно с двукратным запасом. Четвёртое — сжать liveness до тридцати секунд детекта. На этом 90 % боли уходит.
На что можно забить со спокойной совестью. На тонкую настройку successThreshold в readiness — единица работает нормально почти везде, дребезг readiness это отдельная и редкая болезнь. На grpc-пробы, если у вас нет gRPC-сервисов, — это не «более современный» способ, это просто другой протокол. На probe-level terminationGracePeriodSeconds, пока у вас не длинный graceful shutdown. И на попытки выразить сложную бизнес-логику готовности внутри healthcheck: чем сложнее эндпоинт, тем больше шансов, что он сам станет источником отказа.
Отдельно проговорю спорный момент, где единого мнения в индустрии нет. Часть практиков считает, что livenessProbe вообще не нужна большинству приложений: если процесс падает — его и так перезапустит kubelet, а зависания, которые лечатся рестартом, в современных стеках редки. Аргумент сильный, и мой опыт его частично подтверждает: неправильно настроенная liveness приносит больше вреда, чем правильно настроенная — пользы. Я всё-таки её ставлю, но с одним условием — она должна быть предельно тупой и без зависимостей. Если вы не можете написать такую проверку, лучше не ставить liveness совсем и обойтись readiness.
И последнее. Пробы — это не то, что настраивают один раз. Каждая тяжёлая миграция, каждая новая зависимость на старте, каждое изменение лимитов сдвигает время инициализации. Заведите привычку: правите старт приложения — перепроверяйте окно startupProbe. Иначе через полгода вы снова прочитаете в чате «релиз не выкатывается, откатываемся» и потратите вечер на то, что решается одним числом в манифесте.
- Замерить холодный старт контейнера без проб — в спокойное время и под нагрузкой;
- Добавить startupProbe с окном failureThreshold × periodSeconds ≥ 2 × худшего старта;
- Убрать initialDelaySeconds из livenessProbe — он больше не нужен;
- Поднять timeoutSeconds: 3-5 для startup, 2-3 для liveness;
- Вынести все внешние зависимости из liveness в readiness;
- Проверить kubectl describe pod после первого деплоя — Events должны быть чистыми.
Частые вопросы
Можно ли оставить и startupProbe, и initialDelaySeconds у livenessProbe?
Технически можно, но смысла нет. initialDelaySeconds отсчитывается от старта контейнера, а пока startupProbe не прошла, liveness не выполняется вовсе. Если старт занял дольше заданной задержки, liveness начнёт работу сразу после успеха startupProbe и задержка ни на что не повлияет; если короче — она лишь добавит слепое окно. Появилась startupProbe — убирайте initialDelaySeconds из liveness, иначе манифест просто вводит в заблуждение того, кто будет читать его после вас.
startupProbe выполняется при каждом перезапуске контейнера или только при первом старте?
При каждом запуске контейнера, включая перезапуск после падения. Она перестаёт выполняться только после своего первого успеха в рамках текущего запуска. То есть после рестарта весь цикл повторяется заново — и это правильно, потому что после рестарта приложение снова проходит инициализацию.
Что будет, если startupProbe так и не пройдёт за отведённое окно?
По достижении failureThreshold kubelet убивает контейнер, и дальше применяется restartPolicy пода. При restartPolicy: Always вы получите бесконечный цикл перезапусков с нарастающей паузой — тот самый CrashLoopBackOff. Поведение здесь такое же, как при провале liveness.
Нужна ли startupProbe, если у меня уже есть readinessProbe с большим failureThreshold?
Нужна, если есть livenessProbe. Провал readiness не убивает контейнер — под просто не получает трафик, поэтому readiness сама по себе от CrashLoopBackOff не спасает. Убивает контейнер именно liveness, и заблокировать её на время старта умеет только startupProbe.
Какое окно startupProbe считать разумным?
Берите худшее наблюдаемое время холодного старта и умножайте на два. Официальный пример из документации — failureThreshold: 30 при periodSeconds: 10, то есть 300 секунд; для тяжёлых приложений с миграциями я спокойно ставлю 600. Слишком большое окно стоит вам только задержки обнаружения битого релиза, слишком маленькое — несостоявшегося деплоя.
Почему проба падает по таймауту, хотя эндпоинт отвечает быстро?
Скорее всего, у вас дефолтный timeoutSeconds: 1 и упор в CPU-лимит на старте. Пока идут миграции и прогрев, контейнер троттлится, и ответ, который в установившемся режиме занимает 20 миллисекунд, растягивается до секунд. Поднимите timeoutSeconds до 3-5 для startupProbe и проверьте метрику троттлинга cgroup.
Можно ли просто уменьшить паузу CrashLoopBackOff, чтобы под быстрее восстанавливался?
Можно, но это не лечение. По умолчанию пауза начинается с 10 секунд, удваивается и упирается в 300 секунд. С Kubernetes 1.35 feature gate KubeletCrashLoopBackOffMax в статусе beta и включён по умолчанию — потолок можно опустить на ноде через crashLoopBackOff.maxContainerRestartPeriod (от 1 до 300 секунд). Alpha-гейт ReduceDefaultCrashLoopBackOffDecay (с 1.33, выключен по умолчанию) меняет кривую на 1 и 60 секунд. Если контейнер убивает неправильная проба, это лишь ускорит цикл убийств — сначала чините пробу.
Источники
- Kubernetes: Liveness, Readiness, and Startup Probes (v1.35) — Концепция проверок состояния, взаимодействие startupProbe с liveness/readiness, последствия отказа каждой пробы, probe-level terminationGracePeriodSeconds. https://v1-35.docs.kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes/
- Kubernetes: Configure Liveness, Readiness and Startup Probes — Раздел «Protect slow starting containers with startup probes» — пример failureThreshold: 30 и periodSeconds: 10 (окно 300 секунд), таблица полей с значениями по умолчанию и минимумами. https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/
- Kubernetes API Reference — Pod v1 core, объект Probe — Описания полей successThreshold, failureThreshold, timeoutSeconds (по умолчанию 1 секунда) и terminationGracePeriodSeconds («Optional duration in seconds the pod needs to terminate gracefully upon probe failure»). https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/
- Kubernetes: Pod Lifecycle — Container restarts — Экспоненциальная задержка перезапуска (старт 10 с, удвоение, потолок 300 с), разделы Reduced container restart delay (ReduceDefaultCrashLoopBackOffDecay: 1 с / 60 с) и Configurable container restart delay (maxContainerRestartPeriod от 1s до 300s). https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/
- KEP-4603: Tune CrashLoopBackOff (kubernetes/enhancements) — Предложение по изменению кривой CrashLoopBackOff, сброс счётчика после 10 минут работы; конфигурируемый потолок на ноде вынесен в KEP-5593. https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/4603-tune-crashloopbackoff
- The liveness probe that makes your uptime worse — Kube IT Consulting — Разбор эффекта одновременного перезапуска реплик при зависимостях внутри liveness; правило «A liveness probe must only test conditions that a restart can fix». https://kube-it-consulting.com/blog/kubernetes-probes-that-make-uptime-worse/
