Кейс: pod убивали по liveness, пока JVM стояла в GC-паузе

Сервис под нагрузкой периодически ловил рестарт пода без единой ошибки в логах приложения. kubelet просто убивал контейнер и поднимал новый — событие в событиях namespace, не в логах сервиса.

Причина: G1 mixed collection на большом heap иногда занимал больше времени, чем timeoutSeconds у liveness probe. HTTP-эндпоинт /health не успевал ответить за отведённое окно — не потому что сервис завис, а потому что JVM буквально стояла в GC.

kubelet интерпретировал таймаут как «процесс не отвечает» и рестартовал pod. В моменте это выглядело как случайные обрывы соединений у клиентов — ровно на тех запросах, что летели в момент рестарта.

Правили с двух сторон: увеличили timeoutSeconds и failureThreshold у liveness с запасом на худшую паузу G1, и вынесли /health в отдельный лёгкий обработчик, не зависящий от того же потокового пула, что обслуживает бизнес-логику.

Отдельный вопрос на этом месте — почему readiness и liveness нельзя настраивать одинаково. Liveness должен быть терпимым: его задача — поймать по-настоящему зависший процесс, а не наказывать сервис за нормальную GC-паузу. Readiness наоборот может быть строже — временно вывести pod из балансировки безопаснее, чем убить его.

GC-паузу можно спутать с зависанием процесса только если liveness настроен без запаса на худший случай, а не на средний.

Тренажёр: 600 вопросов, мок с таймером, план повторов

senior·base — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки