Un mécanisme de graceful shutdown doit garantir :
- l'absence d'interruption brutale des requêtes en cours ;
- le refus des nouvelles connexions dès le début de l'arrêt ;
- la libération propre des ressources (connexions base de données, sockets, threads, etc.).
Réponse courte : oui, le besoin est nativement couvert par Spring Boot depuis la version 2.3. Il y a très peu de code à écrire — essentiellement de la configuration — plus quelques précautions côté infrastructure, qui constituent la vraie difficulté.
server:
shutdown: graceful # défaut = immediate
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # défaut = 30sComportement obtenu :
- les requêtes en cours disposent du délai configuré pour se terminer ;
- les nouvelles requêtes sont refusées.
Le refus des nouvelles connexions dépend du serveur embarqué :
| Serveur | Comportement à l'arrêt |
|---|---|
| Tomcat | arrêt de l'acceptation au niveau réseau |
| Jetty | arrêt de l'acceptation au niveau réseau |
| Reactor Netty | arrêt de l'acceptation au niveau réseau |
| Undertow | accepte la connexion mais répond immédiatement 503 |
- Ne fonctionne pas pour un déploiement en WAR dans un conteneur de servlets externe (le cycle de vie est alors piloté par le conteneur).
timeout-per-shutdown-phaseest un timeout, pas une garantie : au-delà du délai, l'arrêt est forcé et les requêtes restantes sont interrompues.
Spring Boot enregistre par défaut un shutdown hook JVM qui appelle context.close().
L'arrêt du serveur web intervient dans une phase SmartLifecycle antérieure à la destruction des beans. Les requêtes en vol conservent donc l'accès au DataSource : HikariCP, les caches et les clients HTTP sont fermés après. Aucune action nécessaire pour ces composants.
Sans configuration explicite, les tâches @Async / @Scheduled en cours sont interrompues brutalement, même si les requêtes HTTP se terminent proprement.
spring:
task:
execution:
shutdown:
await-termination: true
await-termination-period: 20s
scheduling:
shutdown:
await-termination: true
await-termination-period: 20s@PreDestroysuffit dans la plupart des cas.- Pour maîtriser l'ordre d'arrêt par rapport au connecteur HTTP, implémenter
SmartLifecycleavec une phase explicite.
@Component
class MonComposant implements SmartLifecycle {
private volatile boolean running;
@Override
public void start() {
this.running = true;
}
@Override
public void stop() {
// libération ordonnée des ressources
this.running = false;
}
@Override
public boolean isRunning() {
return this.running;
}
@Override
public int getPhase() {
// phase supérieure = arrêté plus tôt
return Integer.MAX_VALUE - 1;
}
}Le mécanisme ne se déclenche que sur réception d'un SIGTERM. Un SIGKILL court-circuite tout.
- Utiliser la forme exec de l'entrypoint, sinon la JVM tourne sous un shell qui ne relaie pas le signal :
ENTRYPOINT ["java", "-jar", "app.jar"]docker stopn'accorde que 10 s par défaut avantSIGKILL: aligner cette valeur surtimeout-per-shutdown-phase.
docker stop --time=45 mon-conteneurDeux contraintes :
terminationGracePeriodSecondsdoit être supérieur àtimeout-per-shutdown-phase.- La suppression du pod des
Endpointsest asynchrone par rapport à l'envoi duSIGTERM. Sans précaution, le load balancer continue d'envoyer du trafic vers un pod qui refuse déjà les connexions — d'où des502/connection refusedpendant le déploiement.
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
terminationGracePeriodSeconds: 60Le sleep du preStop laisse le temps à la propagation de la suppression de l'endpoint avant que le SIGTERM ne soit envoyé.
Avec Actuator, exposer les probes et laisser le load balancer cesser de router avant l'arrêt effectif :
management:
endpoint:
health:
probes:
enabled: trueSpring Boot publie un AvailabilityChangeEvent(ReadinessState.REFUSING_TRAFFIC) au début de l'arrêt, ce qui fait basculer /actuator/health/readiness en OUT_OF_SERVICE.
Il est également possible de le déclencher manuellement en amont pour un drain contrôlé :
AvailabilityChangeEvent.publish(context, ReadinessState.REFUSING_TRAFFIC);Un test d'intégration suffit à vérifier le comportement : lancer une requête lente, appeler context.close() en parallèle, et vérifier que la réponse arrive complète.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class GracefulShutdownTest {
@Autowired
private ConfigurableApplicationContext context;
@LocalServerPort
private int port;
@Test
void requeteEnCoursTermineeMalgreArret() throws Exception {
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:" + port + "/slow"))
.build();
var future = client.sendAsync(request, BodyHandlers.ofString());
Thread.sleep(500); // laisser la requête démarrer
context.close(); // déclenche le graceful shutdown
var response = future.get(30, TimeUnit.SECONDS);
assertThat(response.statusCode()).isEqualTo(200);
}
}| Exigence | Couverture |
|---|---|
| Ne pas interrompre les requêtes en cours | server.shutdown: graceful + timeout-per-shutdown-phase |
| Refuser les nouvelles connexions | idem (comportement natif du serveur embarqué) |
| Libérer proprement les ressources | shutdown hook natif + task.*.shutdown.await-termination + @PreDestroy / SmartLifecycle |
Les sections 1 et 2 relèvent de la pure configuration et couvrent les trois exigences fonctionnelles. L'essentiel de l'effort réel porte sur la section 3 : un graceful shutdown parfaitement configuré côté Spring est inopérant si le processus reçoit un SIGKILL, ou si l'orchestrateur continue de lui router du trafic pendant l'arrêt.