Опирается на правила:
R-SHUT-CFG-1…R-SHUT-CFG-4иR-SHUT-CFG-X1из Graceful Shutdown Style Guide → раздел 1. JVM/Spring конфигурация.
Важно знать
server.shutdown=gracefulобязателен — без негоforceShutdown()на SIGTERM, активные запросы получают 502.spring.lifecycle.timeout-per-shutdown-phase: 30s— баланс между «успеть дрейнить» и «не SIGKILL».- SIGTERM hook ставит
ApplicationAvailability.readinessState = OUT_OF_SERVICEпервым.management.health.{livenessstate,readinessstate}.enabled: true— отдельные probes.- Свой
AtomicBoolean shuttingDown— не интегрируется с health, k8s не узнает.- Без правильной конфигурации rolling deploy = шторм 502 на клиентах.
Graceful shutdown — не «одна настройка», а оркестрированная последовательность: переключить readiness в OUT_OF_SERVICE → дать k8s 10 секунд убрать из endpoints → дожать активные запросы → закрыть pool → exit. UCP формулирует минимальный набор Spring-настроек, которые покрывают эту последовательность.
server.shutdown=graceful
R-SHUT-CFG-1: первая и главная настройка.
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
Что делает:
- При SIGTERM Spring не вызывает
Tomcat.stop()немедленно. - Tomcat connector перестаёт принимать новые connections.
- Активные HTTP requests продолжают обрабатываться до
timeout-per-shutdown-phase. - Только потом полное закрытие.
Без server.shutdown: graceful — Tomcat.stop() сразу прерывает все workers, активные requests получают IOException → клиент видит 502 Bad Gateway или Connection reset.
timeout-per-shutdown-phase
R-SHUT-CFG-2: явный timeout per Spring lifecycle phase.
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
Дефолт — 30 секунд. Прописываем явно, чтобы видеть в config.
Trade-off:
- < 20s — мало для дрейна Tomcat thread-pool под нагрузкой. Активные long-running запросы прерываются.
- > 45s — риск SIGKILL внутри
terminationGracePeriodSeconds: 60(см. Kubernetes). Бюджет не помещается. - 30s — sweet spot для типичных REST API (большинство запросов < 1s, p99 ~5s).
Если у вас есть долгие синхронные endpoints — не увеличивайте timeout, а декомпозируйте endpoints (см. R-SHUT-HTTP-3 в HTTP drain).
ApplicationAvailability — единственный источник правды
R-SHUT-CFG-3: Spring Boot 2.3+ имеет встроенный механизм.
@Component
@RequiredArgsConstructor
@Slf4j
public class ShutdownAvailabilityListener {
private final ApplicationEventPublisher events;
@EventListener(ContextClosedEvent.class)
public void onShutdown() {
log.info("Graceful shutdown started, switching readiness to REFUSING_TRAFFIC");
events.publishEvent(new AvailabilityChangeEvent<>(this, ReadinessState.REFUSING_TRAFFIC));
}
}
Что происходит:
- Spring получает
ContextClosedEvent(SIGTERM trigger-нул). - Listener publishит
ReadinessState.REFUSING_TRAFFIC. /actuator/health/readinessначинает возвращать503 Service Unavailable.- K8s readiness probe (опрос каждые 5s) видит fail.
- K8s endpoints-controller убирает pod из Service routing.
- Через 1-2 секунды новый трафик перестаёт приходить.
Spring Boot делает шаг 1-2 автоматически через GracefulShutdown.AvailabilityState-listener. Свой listener дублирует — но даёт явный лог Graceful shutdown started и страховку при изменении дефолтов Spring.
Probes enabled
R-SHUT-CFG-4: отдельные endpoints для liveness и readiness.
management:
endpoint:
health:
probes:
enabled: true
show-details: when-authorized
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
Что это даёт:
/actuator/health/liveness— process alive (JVM не deadlocked)./actuator/health/readiness— ready to accept traffic (зависит отApplicationAvailability).
На shutdown нужен именно readiness=503, не liveness:
- Readiness=503 → k8s убирает из endpoints (стопит трафик).
- Liveness=503 → k8s перезапускает pod (что нам не нужно, мы и так shutdown-имся).
Подробнее различие — Observability → health checks.
Что запрещено
Свой AtomicBoolean shuttingDown
R-SHUT-CFG-X1: классический антипаттерн.
// КАТАСТРОФА — k8s ничего не знает
@Component
public class CustomShutdownState {
private static final AtomicBoolean SHUTTING_DOWN = new AtomicBoolean(false);
@EventListener(ContextClosedEvent.class)
public void onShutdown() {
SHUTTING_DOWN.set(true);
}
public static boolean isShuttingDown() {
return SHUTTING_DOWN.get();
}
}
Проблемы:
- Health-эндпоинты не знают —
/actuator/health/readinessпродолжает возвращать UP. - K8s не знает — readiness probe не падает, pod остаётся в endpoints, трафик продолжает приходить.
- Изолированные проверки — каждый компонент должен импортировать
CustomShutdownState, а в Spring компонентах часть может не знать о нём.
Корректно — ApplicationAvailability через события:
@Component
@RequiredArgsConstructor
public class SomeService {
private final ApplicationAvailability availability;
public void doWork() {
if (availability.getReadinessState() == ReadinessState.REFUSING_TRAFFIC) {
log.info("Refusing new work, shutdown in progress");
return;
}
// work
}
}
Что запрещено — таблица
| Антипаттерн | Правило | Что взамен |
|---|---|---|
AtomicBoolean shuttingDown руками | R-SHUT-CFG-X1 | ApplicationAvailability events |
server.shutdown отсутствует | R-SHUT-CFG-1 | graceful обязателен |
timeout-per-shutdown-phase < 20s | R-SHUT-CFG-2 | минимум 25s |
timeout-per-shutdown-phase > 45s | R-SHUT-CFG-2 | помещаться в 60s total budget |
volatile boolean isShuttingDown static-поле | R-SHUT-CFG-X1 | availability.getReadinessState() |
Probes disabled (probes.enabled: false) | R-SHUT-CFG-4 | отдельные liveness/readiness обязательны |
Custom WebServerCustomizer с awaitTermination(0) | R-SHUT-HTTP-X1 | дефолт Spring graceful |
Куда дальше
- Graceful Shutdown → раздел 1. JVM/Spring конфигурация — нормативные формулировки.
- HTTP drain — что происходит с активными запросами.
- Kafka shutdown — listener container stop.
- Kubernetes — preStop, terminationGracePeriodSeconds.
- Бюджеты и observability — раскладка 60-секундного budget.
- Observability → health checks — liveness vs readiness.