Опирается на правила: R-SHUT-OBS-1R-SHUT-OBS-3 и R-SHUT-OBS-X1 из Graceful Shutdown Style Guide → раздел 8. Бюджеты и observability.

Важно знать

  • 60s total budget = preStop 10s + Spring graceful до 25s + Scheduler до 20s + Kafka до 15s.
  • Phases частично параллельны — реальный wall clock 30-40s, бюджет даёт запас.
  • Не помещается? — сократить scope операций (batch 100 → 20), не увеличивать budget.
  • Метрика app_shutdown_duration_seconds (gauge) + structured лог start/end.
  • Лог причины SIGTERMkubectl describe pod показывает, в коде только факт.
  • HikariPool Shutdown initiated на ERROR — антипаттерн, это нормальный INFO.
  • Без observability — первое падение под нагрузкой = чёрный ящик.

Graceful shutdown — distributed coordination, и если он сломался в проде, нужно понять где именно. Сколько секунд занял HTTP drain, сколько Kafka, сколько scheduled-tasks? Без метрик и логов — каждый инцидент расследуется через kubectl logs + догадки. UCP формулирует один gauge + структурный лог как минимум.

Раскладка 60s budget

R-SHUT-OBS-1: cumulative timeline.

ЭтапДлительностьПараметр
preStop sleep (kube-proxy distribution)10slifecycle.preStop
Spring graceful (HTTP drain)до 25sspring.lifecycle.timeout-per-shutdown-phase
TaskScheduler / @Asyncдо 20sspring.task.scheduling.shutdown.await-termination-period
Kafka listener containerдо 15sspring.kafka.listener.shutdown-timeout
Total maxдо 70sterminationGracePeriodSeconds = 60

70s > 60s — кажется, не помещается. Но Spring выполняет phases частично параллельно:

T=0     SIGTERM
T=0     Spring publishes ContextClosedEvent
T=0     Phase START (parallel):
        ├── Kafka listener.stop() (до 15s)
        ├── TaskScheduler.shutdown() (до 20s)
        └── Tomcat graceful (до 25s)
T=25s   Все 3 phase завершены (max от 3)
T=25s   ApplicationContext.close() (cleanup beans, DataSource)
T=25-30s exit

Реальный wall-clock — 25-40s. Бюджет 60s оставляет запас на медленные shutdown-ы под нагрузкой.

Если не помещается

Не увеличивать budget до 90s, 120s. Причины:

  • Длинный shutdown = длинный rolling deploy. Окно «две версии кода против одной БД» становится дольше → больше схем-incompatibility issues.
  • K8s default kubelet shutdown timeout — 30s. Если pod не завершается за этот срок — kubectl drain зависает.

Сократить scope операций:

  • Kafka batch max.poll.records: 500100. Listener завершает 100 за 5s вместо 25.
  • @Async tasks с long cascade → разбить на короче.
  • @Scheduled с heavy job → разбить на batch 50 вместо 500.

Метрика app_shutdown_duration_seconds

R-SHUT-OBS-2: gauge + structured log.

@Component
@RequiredArgsConstructor
@Slf4j
public class ShutdownObserver {

    private final MeterRegistry meterRegistry;
    @Value("${terminationGracePeriodSeconds:60}")
    private long terminationGracePeriodSeconds;

    private volatile long shutdownStartMs;

    @EventListener(ContextClosedEvent.class)
    public void onShutdown() {
        shutdownStartMs = System.currentTimeMillis();
        log.info("Graceful shutdown started, deadline={}s", terminationGracePeriodSeconds);
        meterRegistry.gauge(
            "app_shutdown_duration_seconds",
            this,
            obs -> (System.currentTimeMillis() - obs.shutdownStartMs) / 1000.0
        );
    }
}

После shutdown — финальный лог:

@PreDestroy
public void onPreDestroy() {
    var durationMs = System.currentTimeMillis() - shutdownStartMs;
    log.info("Graceful shutdown completed in {}ms", durationMs);
}

В Prometheus:

# Распределение длительности shutdown по сервисам
max by (service) (app_shutdown_duration_seconds)

# Алерт если близко к budget
max(app_shutdown_duration_seconds) > 50

Без gauge — расследование «почему deploy таймауты» через прокликивание десятков pod-логов вручную.

Лог причины SIGTERM

R-SHUT-OBS-3: где смотреть.

Spring не знает, почему получил SIGTERM:

  • Rolling deploy?
  • HPA scale-down?
  • Manual kubectl delete pod?
  • OOM killer (sysctl vm.overcommit_memory)?
  • Node maintenance?

В коде только записываем факт:

@EventListener(ContextClosedEvent.class)
public void onShutdown() {
    log.info("SIGTERM received, starting graceful shutdown");
}

Контекст ищется через kubectl describe pod <pod-name>:

Events:
  Type    Reason     Age    From               Message
  ----    ------     ----   ----               -------
  Normal  Killing    2m     kubelet            Stopping container app
  Normal  ScalingReplicaSet  10m  deployment-controller  Scaled down replica set order-service-7c8d

Или через k8s audit log для более глубокого расследования.

Не пытаемся определить причину в коде — это infrastructure-info, не application-info.

Что запрещено

Логирование на ERROR обычных shutdown-событий

R-SHUT-OBS-X1:

// КАТАСТРОФА — alert channel заспамлен каждым deploy
2026-05-26 10:30:00 ERROR HikariPool-1 - Shutdown initiated...
2026-05-26 10:30:00 ERROR Closing JPA EntityManagerFactory for persistence unit 'default'
2026-05-26 10:30:00 ERROR org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor - Shutting down...

Это нормальные события. Если они на ERROR — каждый rolling deploy генерирует десяток алертов в Slack/PagerDuty. Команда привыкает игнорировать alerts → реальный инцидент пропускается.

logback-spring.xml — задавать правильные уровни:

<logger name="com.zaxxer.hikari" level="INFO"/>
<logger name="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor" level="INFO"/>
<logger name="org.springframework.kafka.listener.KafkaMessageListenerContainer" level="INFO"/>

ERROR в shutdown — только если что-то реально пошло не так (force-shutdown, lost connections, etc).

Что запрещено — таблица

АнтипаттернПравилоЧто взамен
ERROR-логи на normal shutdown eventsR-SHUT-OBS-X1INFO-level
Нет app_shutdown_duration_seconds метрикиR-SHUT-OBS-2gauge обязательно
Нет structured лога SIGTERM receivedR-SHUT-OBS-2Graceful shutdown started
terminationGracePeriodSeconds: 90+R-SHUT-OBS-160s, сокращать scope операций
Все timeouts на максимум одновременноR-SHUT-OBS-1распределение по phases
Попытка определить SIGTERM причину в кодеR-SHUT-OBS-3kubectl describe pod
Метрика без service tagR-SHUT-OBS-2стандартные tags
Нет alert на shutdown_duration > 50sR-SHUT-OBS-2proactive alert

Куда дальше

  • Graceful Shutdown → раздел 8. Бюджеты и observability — нормативные формулировки.
  • JVM/Spring конфигурация — timeout-per-shutdown-phase.
  • HTTP drain — preStop 10s, до 25s drain.
  • Kafka shutdown — до 15s listener.
  • Scheduled / Async / outbox — до 20s scheduler.
  • Kubernetes — terminationGracePeriodSeconds: 60.
  • Observability → metrics — стандартные tags.
  • Observability → logging — INFO-уровень для normal events.