← Назад к вопросам

Как не навредить и дать возможность остановиться сервису в случае отказа

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

  1. Используй server.shutdown=graceful — это минимум
  2. Установи правильные таймауты — 30s обычно достаточно
  3. Обновляй health check — перед shutdown
  4. Ждите завершения потоков — используй awaitTermination
  5. Закрывай ресурсы — БД, очереди, файлы
  6. Логируй процесс — важно для отладки
  7. Тестируй shutdown — убедись что работает
  8. Используй PreDestroy — для очистки ресурсов

Graceful shutdown — это не опция, а обязательная часть production-ready приложения!

Как не навредить и дать возможность остановиться сервису в случае отказа | PrepBro