Dlaczego graceful shutdown jest ważny?
Gdy aplikacja zostaje wyłączona w środku przetwarzania requestu, użytkownik może stracić dane lub otrzymać błąd 500. W środowisku produkcyjnym, gdzie deployments odbywają się regularnie, graceful shutdown to różnica między zadowolonymi użytkownikami a support tickets o zaginionych transakcjach.
Z perspektywy biznesowej oznacza to zero downtime deployments, lepsze user experience i mniej problemów operacyjnych podczas aktualizacji systemu.
Co się nauczysz:
- Jak włączyć graceful shutdown w Spring Boot 2.3
- Konfigurację timeout i grace period
- Implementację custom shutdown hooks
- Monitoring procesu wyłączania aplikacji
- Best practices dla różnych typów aplikacji
Wymagania wstępne:
- Spring Boot 2.x (znajomość podstaw)
- Pojęcie o HTTP requestach i lifecycle
- Podstawy Java i web applications
- Doświadczenie z deploymentami aplikacji
Czym jest graceful shutdown?
Gdy aplikacja otrzymuje sygnał wyłączenia (SIGTERM), graceful shutdown:
1. **Przestaje przyjmować nowe requesty**
2. **Kończy aktywne requesty** w określonym czasie
3. **Wywołuje cleanup hooks** (zamykanie połączeń DB, cache, etc.)
4. **Wyłącza się** po zakończeniu wszystkich operacji
Konfiguracja graceful shutdown w Spring Boot 2.3
Podstawowa konfiguracja w application.yml:
server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s
Alternatywnie w application.properties:
server.shutdown=graceful spring.lifecycle.timeout-per-shutdown-phase=30s
Jak to działa w praktyce?
Przykład kontrolera który symuluje długotrwałe operacje:
@RestController public class OrderController { private static final Logger logger = LoggerFactory.getLogger(OrderController.class); @PostMapping("/api/orders") public ResponseEntityprocessOrder(@RequestBody OrderRequest request) { logger.info("Starting order processing for: {}", request.getOrderId()); try { // Symulacja długotrwałego przetwarzania (np. płatność, walidacja) Thread.sleep(10000); // 10 sekund OrderResponse response = OrderResponse.builder() .orderId(request.getOrderId()) .status("COMPLETED") .processedAt(LocalDateTime.now()) .build(); logger.info("Order processing completed: {}", request.getOrderId()); return ResponseEntity.ok(response); } catch (InterruptedException e) { logger.warn("Order processing interrupted: {}", request.getOrderId()); return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) .body(OrderResponse.builder() .orderId(request.getOrderId()) .status("PROCESSING_INTERRUPTED") .build()); } } }
Custom shutdown hooks
Możesz dodać własne operacje cleanup używając @PreDestroy lub custom hooks:
@Service public class OrderProcessingService { private final ExecutorService executorService = Executors.newFixedThreadPool(10); private final Logger logger = LoggerFactory.getLogger(OrderProcessingService.class); @PreDestroy public void cleanup() { logger.info("Starting graceful shutdown of OrderProcessingService"); executorService.shutdown(); try { // Czekaj maksymalnie 20 sekund na zakończenie zadań if (!executorService.awaitTermination(20, TimeUnit.SECONDS)) { logger.warn("Some tasks didn't complete within timeout, forcing shutdown"); executorService.shutdownNow(); } } catch (InterruptedException e) { logger.error("Shutdown interrupted", e); executorService.shutdownNow(); Thread.currentThread().interrupt(); } logger.info("OrderProcessingService shutdown completed"); } }
Alternative approach z DisposableBean:
@Component public class DatabaseConnectionManager implements DisposableBean { private final Logger logger = LoggerFactory.getLogger(DatabaseConnectionManager.class); @Override public void destroy() throws Exception { logger.info("Closing database connections gracefully"); // Zamknij connection pools // Flush pending transactions // Clear caches logger.info("Database connections closed"); } }
Monitoring graceful shutdown
Dodaj logging żeby monitorować proces wyłączania:
logging: level: org.springframework.boot.web.embedded.tomcat: INFO org.springframework.context.support: DEBUG pattern: console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
Przykład custom ActuatorEndpoint dla monitoring:
@Component @Endpoint(id = "shutdown-status") public class ShutdownStatusEndpoint { private volatile boolean shutdownInitiated = false; private LocalDateTime shutdownStartTime; @EventListener public void handleContextClosedEvent(ContextClosedEvent event) { shutdownInitiated = true; shutdownStartTime = LocalDateTime.now(); } @ReadOperation public MapshutdownStatus() { Map status = new HashMap<>(); status.put("shutdownInitiated", shutdownInitiated); if (shutdownStartTime != null) { status.put("shutdownStartTime", shutdownStartTime); status.put("shutdownDuration", Duration.between(shutdownStartTime, LocalDateTime.now()).getSeconds()); } return status; } }
Różne strategie dla różnych aplikacji
Typ aplikacji | Recommended timeout | Specjalne uwagi |
---|---|---|
REST API | 30-60s | Krótkie requesty, szybkie wyłączenie |
Batch processing | 5-15min | Długotrwałe operacje, checkpoint saving |
WebSocket apps | 60-120s | Notyfikacja klientów o rozłączeniu |
Message consumers | 2-5min | Dokończenie przetwarzania wiadomości |
Testing graceful shutdown
Prosty test integracyjny:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class GracefulShutdownTest { @Autowired private TestRestTemplate restTemplate; @LocalServerPort private int port; @Test void shouldCompleteRequestsBeforeShutdown() throws Exception { // Uruchom długotrwały request w osobnym wątku CompletableFuture> future = CompletableFuture.supplyAsync(() -> { return restTemplate.postForEntity( "http://localhost:" + port + "/api/long-running-task", new TaskRequest("test"), String.class ); }); // Poczekaj chwilę i wyślij sygnał shutdown Thread.sleep(1000); // W prawdziwym teście użyj SpringApplication.exit() lub podobne // Sprawdź czy request się ukończył pomyślnie ResponseEntity response = future.get(45, TimeUnit.SECONDS); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } }
Spring Boot przerywa wszystkie aktywne operacje i wyłącza aplikację. Aktywne requesty otrzymają status 503 Service Unavailable lub connection reset.
Tak, Spring Boot 2.3+ obsługuje graceful shutdown dla wszystkich embedded servers: Tomcat, Jetty, Undertow i Netty.
Praktycznie wcale. Overhead występuje tylko podczas wyłączania aplikacji, w normalnej pracy nie ma różnicy w wydajności.
Tak, ustaw server.shutdown=immediate w konfiguracji. To przywraca poprzednie zachowanie (natychmiastowe wyłączenie).
Uruchom aplikację, wyślij długotrwały request, a następnie zatrzymaj aplikację przez IDE lub Ctrl+C. Sprawdź logi czy request się ukończył.
Tak, ale pamiętaj że docker stop domyślnie daje 10 sekund na wyłączenie. Użyj docker stop -t 60 dla dłuższego timeout.
Przydatne zasoby:
- Spring Boot 2.3.0 Release Notes
- Spring Boot Graceful Shutdown Documentation
- GitHub Issue – Graceful Shutdown Feature Request
- Baeldung – Spring Boot Graceful Shutdown
🚀 Zadanie dla Ciebie
Stwórz prostą Spring Boot aplikację z endpoint który symuluje 15-sekundowe przetwarzanie. Skonfiguruj graceful shutdown z timeout 20 sekund. Przetestuj scenario: uruchom request, zatrzymaj aplikację i sprawdź czy request się ukończy. Dodaj custom @PreDestroy hook który loguje informacje o zamykaniu.
Jak planujesz wykorzystać graceful shutdown w swoich projektach? Podziel się swoimi doświadczeniami z deployment w komentarzach!