Опирается на правила: R-SHUT-CFG-1R-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: gracefulTomcat.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));
    }
}

Что происходит:

  1. Spring получает ContextClosedEvent (SIGTERM trigger-нул).
  2. Listener publishит ReadinessState.REFUSING_TRAFFIC.
  3. /actuator/health/readiness начинает возвращать 503 Service Unavailable.
  4. K8s readiness probe (опрос каждые 5s) видит fail.
  5. K8s endpoints-controller убирает pod из Service routing.
  6. Через 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-X1ApplicationAvailability events
server.shutdown отсутствуетR-SHUT-CFG-1graceful обязателен
timeout-per-shutdown-phase < 20sR-SHUT-CFG-2минимум 25s
timeout-per-shutdown-phase > 45sR-SHUT-CFG-2помещаться в 60s total budget
volatile boolean isShuttingDown static-полеR-SHUT-CFG-X1availability.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.