Опирается на правила:
R-SHUT-OBS-1…R-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.- Лог причины SIGTERM —
kubectl 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) | 10s | lifecycle.preStop |
| Spring graceful (HTTP drain) | до 25s | spring.lifecycle.timeout-per-shutdown-phase |
| TaskScheduler / @Async | до 20s | spring.task.scheduling.shutdown.await-termination-period |
| Kafka listener container | до 15s | spring.kafka.listener.shutdown-timeout |
| Total max | до 70s | terminationGracePeriodSeconds = 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: 500→100. 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 events | R-SHUT-OBS-X1 | INFO-level |
Нет app_shutdown_duration_seconds метрики | R-SHUT-OBS-2 | gauge обязательно |
| Нет structured лога SIGTERM received | R-SHUT-OBS-2 | Graceful shutdown started |
terminationGracePeriodSeconds: 90+ | R-SHUT-OBS-1 | 60s, сокращать scope операций |
| Все timeouts на максимум одновременно | R-SHUT-OBS-1 | распределение по phases |
| Попытка определить SIGTERM причину в коде | R-SHUT-OBS-3 | kubectl describe pod |
Метрика без service tag | R-SHUT-OBS-2 | стандартные tags |
| Нет alert на shutdown_duration > 50s | R-SHUT-OBS-2 | proactive 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.