← Назад к вопросам
Как не навредить и дать возможность остановиться сервису в случае отказа
2.7 Senior🔥 141 комментариев
#REST API и микросервисы
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Graceful Shutdown - как остановить сервис без вреда при отказе
Это критически важный аспект production систем. Грубое завершение сервиса (kill -9) может привести к потере данных, повреждению БД и некорректному состоянию системы. Правильный Graceful Shutdown обеспечивает безопасное завершение.
Проблемы при грубом shutdown
Сценарий БЕЗ graceful shutdown:
1. В БД пишется транзакция
2. Сервис получает SIGKILL
3. Транзакция не завершена
4. При перезапуске:
- Данные повреждены или неполные
- Orphaned locks в БД
- Broken connections в пуле
Graceful Shutdown в Spring Boot
1. Автоматический shutdown (Spring Boot 2.3+)
Spring Boot автоматически обрабатывает SIGTERM:
# application.properties
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
// При shutdown (SIGTERM) автоматически:
// 1. Прекращает принимать новые запросы
// 2. Ждёт завершения текущих запросов
// 3. Закрывает БД connections
// 4. Выключает потокпулы
}
}
2. Обработка shutdown события
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.util.concurrent.ExecutorService;
@Component
public class ShutdownHandler implements ApplicationListener<ContextClosedEvent> {
@Autowired
private ExecutorService executorService;
@Autowired
private DataSource dataSource;
@Override
public void onApplicationEvent(ContextClosedEvent event) {
System.out.println("Graceful shutdown started");
// 1. Остановить фоновые потоки
shutdownExecutorService();
// 2. Закрыть connections
closeDataSource();
System.out.println("Graceful shutdown completed");
}
private void shutdownExecutorService() {
executorService.shutdown();
try {
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
private void closeDataSource() {
try {
if (dataSource instanceof HikariDataSource) {
((HikariDataSource) dataSource).close();
}
} catch (Exception e) {
System.err.println("Error closing datasource: " + e.getMessage());
}
}
}
3. Health Check для Load Balancer
Под время graceful shutdown, health endpoint должен вернуть ошибку, чтобы LB перестал отправлять запросы:
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicBoolean;
@Component
public class ReadinessHealthCheck implements HealthIndicator {
private final AtomicBoolean isShuttingDown = new AtomicBoolean(false);
@Override
public Health health() {
if (isShuttingDown.get()) {
// LB видит эту ошибку и перестаёт отправлять запросы
return Health.down().withDetail("reason", "Graceful shutdown in progress").build();
}
return Health.up().build();
}
public void markAsShuttingDown() {
isShuttingDown.set(true);
}
}
@Component
public class ShutdownListener implements ApplicationListener<ContextClosedEvent> {
@Autowired
private ReadinessHealthCheck healthCheck;
@Override
public void onApplicationEvent(ContextClosedEvent event) {
// Сразу отключаем от LB
healthCheck.markAsShuttingDown();
// Ждём немного, чтобы LB заметил изменение
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Обработка сигналов (SIGTERM, SIGINT)
import java.util.concurrent.atomic.AtomicBoolean;
@Component
public class SignalHandler {
private final AtomicBoolean shutdownRequested = new AtomicBoolean(false);
@PostConstruct
public void registerSignalHandlers() {
// SIGTERM - graceful shutdown
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("SIGTERM received");
shutdownRequested.set(true);
// Spring обработает это автоматически
}));
// SIGINT (Ctrl+C)
Signal.handle(new Signal("INT"), signal -> {
System.out.println("SIGINT received (Ctrl+C)");
shutdownRequested.set(true);
});
}
public boolean isShuttingDown() {
return shutdownRequested.get();
}
}
Connection Pool Graceful Closure
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost/mydb");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000); // 30s
config.setIdleTimeout(600000); // 10m
config.setMaxLifetime(1800000); // 30m
return new HikariDataSource(config);
}
@PreDestroy
public void closeDataSource(DataSource dataSource) {
if (dataSource instanceof HikariDataSource) {
System.out.println("Closing database connections...");
((HikariDataSource) dataSource).close();
}
}
}
Graceful Shutdown для Async Tasks
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-task-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60); // Ждать 60 сек
executor.initialize();
return executor;
}
}
Graceful Shutdown для Message Queue (Kafka)
@Component
public class KafkaShutdownHandler {
@Autowired
private KafkaListenerEndpointRegistry kafkaListenerEndpointRegistry;
@PreDestroy
public void onShutdown() {
System.out.println("Stopping Kafka listeners");
// Gracefully stop all Kafka listeners
kafkaListenerEndpointRegistry.stop();
System.out.println("Kafka listeners stopped");
}
}
Timeout Management
# application.properties
# Web server
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s
# Database
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.max-lifetime=1800000
# Async
spring.task.execution.thread-name-prefix=async-
spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=10
# Scheduled tasks
spring.task.scheduling.pool.size=5
Docker / Kubernetes Graceful Shutdown
FROM openjdk:17-slim
WORKDIR /app
COPY app.jar .
# PID 1 важно для получения SIGTERM
ENTRYPOINT ["java", "-jar", "app.jar"]
apiVersion: v1
kind: Pod
metadata:
name: java-app
spec:
containers:
- name: app
image: my-app:latest
lifecycle:
preStop:
exec:
# Даём сервису время на graceful shutdown
command: ["/bin/sh", "-c", "sleep 15"]
terminationGracePeriodSeconds: 30 # Максимальное время ожидания
Complete Example
@Component
public class GracefulShutdownManager {
private static final Logger log = LoggerFactory.getLogger(GracefulShutdownManager.class);
private final AtomicBoolean isShuttingDown = new AtomicBoolean(false);
@Autowired
private ReadinessHealthCheck healthCheck;
@Autowired
private ExecutorService executorService;
@PreDestroy
public void gracefulShutdown() throws InterruptedException {
log.info("Starting graceful shutdown");
// 1. Отключить от Load Balancer
isShuttingDown.set(true);
healthCheck.markAsShuttingDown();
log.info("Marked as shutting down for LB");
// 2. Подождать, чтобы LB заметил и перестал отправлять
Thread.sleep(3000);
// 3. Завершить текущие запросы (Spring это делает автоматически)
log.info("Waiting for in-flight requests to complete");
// 4. Остановить фоновые потоки
log.info("Shutting down executor service");
executorService.shutdown();
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
log.warn("Executor service shutdown timeout, forcing shutdown");
executorService.shutdownNow();
}
log.info("Graceful shutdown completed");
}
public boolean isShuttingDown() {
return isShuttingDown.get();
}
}
Best Practices
- Используй server.shutdown=graceful — это минимум
- Установи правильные таймауты — 30s обычно достаточно
- Обновляй health check — перед shutdown
- Ждите завершения потоков — используй awaitTermination
- Закрывай ресурсы — БД, очереди, файлы
- Логируй процесс — важно для отладки
- Тестируй shutdown — убедись что работает
- Используй PreDestroy — для очистки ресурсов
Graceful shutdown — это не опция, а обязательная часть production-ready приложения!