- Le graceful shutdown est une procédure par laquelle, après avoir reçu un signal d’arrêt, l’application bloque les nouvelles requêtes, termine celles en cours, puis libère les ressources
- En Go, le package
os/signal permet de gérer directement des signaux d’arrêt comme SIGINT et SIGTERM, et signal.NotifyContext rend aussi possible un contrôle de l’arrêt basé sur context
- Lors de l’arrêt d’un serveur HTTP, il est plus sûr de bloquer le trafic via l’échec de la readiness probe avant d’appeler
Server.Shutdown(), puis d’attendre quelques secondes avant d’exécuter le shutdown
- Tous les handlers doivent pouvoir détecter le signal d’arrêt du context et s’interrompre, ce qui peut être géré de façon unifiée via
BaseContext ou un middleware
- Après la réception d’un signal d’arrêt, les ressources externes comme la base de données, le message broker ou le cache doivent être libérées volontairement, et leur enregistrement avec
defer facilite la gestion de l’ordre d’arrêt
Qu’est-ce que le graceful shutdown ?
- Le graceful shutdown est le processus par lequel une application, au moment de s’arrêter, bloque les nouvelles requêtes, attend la fin des requêtes en cours, puis libère ses ressources
- Cet article traite principalement des serveurs HTTP et des environnements conteneurisés, mais il s’agit d’un concept applicable à toutes les applications
1. Gestion des signaux d’arrêt
- Sur les systèmes de type Unix,
SIGTERM, SIGINT et SIGHUP sont utilisés comme signaux d’arrêt
- Le runtime Go arrête par défaut l’application à la réception de
SIGTERM ou SIGINT, mais on peut les gérer directement avec os/signal.Notify
- L’utilisation d’un canal bufferisé (capacité 1) permet d’éviter la perte de signaux pendant l’initialisation
- Depuis Go 1.16,
signal.NotifyContext simplifie le contrôle des signaux via context
2. Prendre en compte le temps d’arrêt
- Dans Kubernetes, une période de grâce de 30 secondes est accordée par défaut pour l’arrêt (
terminationGracePeriodSeconds)
- Pour un arrêt sûr, il est recommandé de prévoir une marge de 20 % et de terminer les opérations d’arrêt en moins de 25 secondes
3. Arrêter d’accepter de nouvelles requêtes
http.Server.Shutdown() bloque les nouvelles connexions et attend la fin des requêtes existantes
- Dans un environnement Kubernetes, on commence par faire échouer la readiness probe afin de bloquer l’arrivée du trafic, puis on attend un peu avant d’exécuter le shutdown
- Le handler de readiness peut s’appuyer sur une variable globale indiquant l’état d’arrêt afin de retourner un HTTP 503
4. Finaliser le traitement des requêtes
- Il faut définir un timeout approprié sur le context dédié à l’arrêt (
context.WithTimeout)
- Si le context de shutdown expire, les connexions restantes sont forcées à se fermer
- Tous les handlers doivent être conçus pour détecter le signal d’arrêt et pouvoir s’interrompre à l’aide de
context.Context
- Pour cela, on peut injecter le context d’arrêt dans toutes les requêtes via un middleware ou
BaseContext
5. Libération des ressources
- Si l’on ferme les ressources immédiatement après réception du signal d’arrêt, cela peut poser problème aux handlers encore en cours d’exécution
- Une fois le shutdown terminé, il faut libérer les connexions à la base de données, le message broker, le cache, etc.
- En utilisant
defer en Go, on peut exécuter les routines d’arrêt dans l’ordre inverse de l’initialisation, ce qui facilite la gestion des dépendances
- Au-delà des ressources que l’OS nettoie automatiquement, comme la mémoire ou les descripteurs de fichiers, certaines nécessitent un arrêt explicite, comme le flush de données ou le rollback de transactions
Résumé de l’exemple complet
- Réception du signal d’arrêt avec
signal.NotifyContext
- Implémentation de l’endpoint de readiness
/healthz
- Injection du context d’arrêt dans toutes les requêtes avec
BaseContext
- Exécution du shutdown après 5 secondes d’attente de la readiness
- Inclusion d’un fallback de fermeture forcée en cas d’échec de l’appel à
server.Shutdown
Références et ressources associées
1 commentaires
Avis Hacker News
Dans Kubernetes, la mise à jour des IP cibles du load balancer peut parfois prendre du temps. Dans 90 % des cas, le vrai problème est de vérifier que le trafic est effectivement drainé
preStopglobal a nettement amélioré le taux de HTTP 503SIGTERM, ce qui simplifie le traitement côté applicationLorsqu’on utilise
log.Fatal, le contenu desdefern’est pas exécutélog.Fatalappelleos.Exitet termine immédiatement le processuspanic, le contenu desdeferest bien exécutéQuand l’endpoint Prometheus
/metricsest scrapé périodiquement, les métriques enregistrées entre le dernier scrape et l’arrêt du processus peuvent ne pas être propagéesSi un système distribué dépend de l’arrêt propre du client, il peut tomber en panne de manière grave
Il manque des explications sur la façon de redémarrer une application sans couper les connexions, quand une nouvelle instance de service reçoit les sockets de l’instance précédente
La discussion sur la liveness est insuffisante
Un programme qui ne sait pas gérer proprement des commandes comme ctrl c est mal conçu
Elixir conçoit les processus comme de petits processus de VM, ce qui évite d’avoir à créer délibérément des routines d’arrêt propre
Quelqu’un a créé dans son projet une petite bibliothèque pour gérer l’arrêt propre
Après la mise à jour de la readiness probe, il faut attendre quelques secondes afin que le système n’envoie plus de nouvelles requêtes
SIGTERM, mais ce n’est pas un gros problème