Graceful shutdown сервлет-контейнера в Kubernetes
Симптом знаком многим: при каждой выкатке в логах балансировщика появляется горстка ошибок 502 или сброшенных соединений. При этом в приложении включён server.shutdown=graceful, и по логам видно, что все начатые запросы честно завершились.
Что происходит при остановке пода
Когда под удаляется, Kubernetes делает две вещи параллельно: отправляет контейнеру SIGTERM и начинает убирать адрес пода из Endpoints. Пока это изменение дойдёт до kube-proxy на всех узлах и до ingress-контроллера, проходит от долей секунды до нескольких секунд.
Spring Boot на SIGTERM сразу перестаёт принимать новые соединения и ждёт активные запросы. Но балансировщики в этот момент ещё считают под живым и продолжают слать на него трафик — эти запросы и превращаются в ошибки.
Пауза в preStop
Решение — дать сети время забыть про под, прежде чем приложение начнёт останавливаться. Для этого служит хук preStop: он выполняется до отправки SIGTERM.
lifecycle:
preStop:
sleep:
seconds: 10
terminationGracePeriodSeconds: 45
Встроенное действие sleep появилось в Kubernetes 1.29 и включено по умолчанию с 1.30. В старых кластерах то же самое делают через exec с sleep 10, но для этого в образе должна быть оболочка — в distroless-образах её нет.
Как сложить таймауты
Время из terminationGracePeriodSeconds (по умолчанию 30 секунд) отсчитывается с момента начала остановки и включает в себя preStop. Поэтому бюджет складывается так:
- пауза в preStop — несколько секунд, чтобы обновились Endpoints;
spring.lifecycle.timeout-per-shutdown-phase— сколько приложение ждёт активные запросы (по умолчанию тоже 30 секунд);- небольшой запас на закрытие пулов и остановку контекста.
Если сумма больше grace period, Kubernetes просто пришлёт SIGKILL, и вся аккуратность пропадёт. В примере выше 10 + 30 секунд укладываются в 45 с запасом.
Проверка
Лучший тест — нагрузка во время выкатки: даже простой цикл с curl на сотню запросов в секунду сразу показывает, теряются ли запросы. Без такой проверки graceful shutdown легко считать работающим только потому, что его включили в настройках.