Skip to content

Instantly share code, notes, and snippets.

@enimiste
Last active August 6, 2026 16:31
Show Gist options
  • Select an option

  • Save enimiste/a84a495b60b555c188fcddbf94af95c9 to your computer and use it in GitHub Desktop.

Select an option

Save enimiste/a84a495b60b555c188fcddbf94af95c9 to your computer and use it in GitHub Desktop.
Graceful shutdown des composants HTTP — Spring Boot

Graceful shutdown des composants HTTP — Spring Boot

Contexte

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é.


1. Activation du graceful shutdown du serveur HTTP

server:
  shutdown: graceful          # défaut = immediate
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s   # défaut = 30s

Comportement 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

Limites à connaître

  • 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-phase est un timeout, pas une garantie : au-delà du délai, l'arrêt est forcé et les requêtes restantes sont interrompues.

2. Libération des ressources

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.

Exception : les pools de tâches

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

Ressources applicatives propres

  • @PreDestroy suffit dans la plupart des cas.
  • Pour maîtriser l'ordre d'arrêt par rapport au connecteur HTTP, implémenter SmartLifecycle avec 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;
    }
}

3. Le point réellement piégeux : les signaux

Le mécanisme ne se déclenche que sur réception d'un SIGTERM. Un SIGKILL court-circuite tout.

Docker

  • 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 stop n'accorde que 10 s par défaut avant SIGKILL : aligner cette valeur sur timeout-per-shutdown-phase.
docker stop --time=45 mon-conteneur

Kubernetes

Deux contraintes :

  1. terminationGracePeriodSeconds doit être supérieur à timeout-per-shutdown-phase.
  2. La suppression du pod des Endpoints est asynchrone par rapport à l'envoi du SIGTERM. Sans précaution, le load balancer continue d'envoyer du trafic vers un pod qui refuse déjà les connexions — d'où des 502 / connection refused pendant le déploiement.
lifecycle:
  preStop:
    exec:
      command: ["sh", "-c", "sleep 10"]
terminationGracePeriodSeconds: 60

Le sleep du preStop laisse le temps à la propagation de la suppression de l'endpoint avant que le SIGTERM ne soit envoyé.


4. Retirer l'instance du pool en amont (recommandé)

Avec Actuator, exposer les probes et laisser le load balancer cesser de router avant l'arrêt effectif :

management:
  endpoint:
    health:
      probes:
        enabled: true

Spring 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);

5. Validation

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);
    }
}

Synthèse

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment