paint-brush
Maîtriser les arrêts gracieux dans Go : un guide complet pour Kubernetespar@gopher

Maîtriser les arrêts gracieux dans Go : un guide complet pour Kubernetes

par Alex6m2024/08/14
Read on Terminal Reader

Trop long; Pour lire

Ce guide se plongera dans le monde des arrêts gracieux, en se concentrant spécifiquement sur leur implémentation dans les applications Go exécutées sur Kubernetes.
featured image - Maîtriser les arrêts gracieux dans Go : un guide complet pour Kubernetes
Alex HackerNoon profile picture

Avez-vous déjà débranché le cordon d'alimentation de votre ordinateur par frustration ? Bien que cela puisse sembler être une solution rapide, cela peut entraîner une perte de données et une instabilité du système. Dans le monde des logiciels, un concept similaire existe : l'arrêt brutal. Cet arrêt brutal peut causer des problèmes tout comme son homologue physique. Heureusement, il existe une meilleure solution : l'arrêt progressif.


En intégrant un arrêt progressif, nous envoyons une notification préalable au service. Cela lui permet de répondre aux demandes en cours, d'enregistrer potentiellement les informations d'état sur le disque et, en fin de compte, d'éviter la corruption des données lors de l'arrêt.


Ce guide se plongera dans le monde des arrêts gracieux, en se concentrant spécifiquement sur leur implémentation dans les applications Go exécutées sur Kubernetes.

Signaux dans les systèmes Unix

L'un des outils clés pour réaliser des arrêts en douceur dans les systèmes basés sur Unix est le concept de signaux, qui sont, en termes simples, un moyen simple de communiquer une chose spécifique à un processus, à partir d'un autre processus. En comprenant le fonctionnement des signaux, nous pouvons les exploiter pour mettre en œuvre des procédures d'arrêt contrôlées dans nos applications, garantissant un processus d'arrêt fluide et sécurisé pour les données.


Il existe de nombreux signaux, et vous pouvez les trouver ici , mais nous nous intéressons uniquement aux signaux d'arrêt :

  • SIGTERM - envoyé à un processus pour demander sa fin. Le plus couramment utilisé, et nous y reviendrons plus tard.
  • SIGKILL - « quitter immédiatement », ne peut pas être interféré.
  • SIGINT - signal d'interruption (tel que Ctrl+C)
  • SIGQUIT - signal de sortie (tel que Ctrl+D)


Ces signaux peuvent être envoyés par l'utilisateur (Ctrl+C / Ctrl+D), par un autre programme/processus, ou par le système lui-même (noyau / OS), par exemple, un SIGSEGV alias erreur de segmentation est envoyé par le système d'exploitation.


Notre service pour cochons d'Inde

Pour explorer le monde des arrêts progressifs dans un cadre pratique, créons un service simple avec lequel nous pouvons expérimenter. Ce service « cobaye » aura un point de terminaison unique qui simule un travail réel (nous ajouterons un léger délai) en appelant la commande INCR de Redis. Nous fournirons également une configuration Kubernetes de base pour tester la manière dont la plateforme gère les signaux d'arrêt.


L'objectif ultime : garantir que notre service gère correctement les arrêts sans perdre de requêtes/données. En comparant le nombre de requêtes envoyées en parallèle avec la valeur finale du compteur dans Redis, nous pourrons vérifier si notre implémentation d'arrêt gracieux est réussie.

Nous n'entrerons pas dans les détails de la configuration du cluster Kubernetes et de Redis, mais vous pouvez trouver la configuration complète dans notre référentiel Github .


Le processus de vérification est le suivant :

  1. Déployez les applications Redis et Go sur Kubernetes.
  2. Utilisez vegeta pour envoyer 1000 requêtes (25/s sur 40 secondes).
  3. Pendant que Vegeta est en cours d'exécution, initialisez une mise à jour continue de Kubernetes en mettant à jour la balise d'image.
  4. Connectez-vous à Redis pour vérifier le « compteur », il devrait être à 1 000.


Commençons par notre serveur HTTP Go de base.

arrêt-matériel/main.go

 package main import ( "net/http" "os" "time" "github.com/go-redis/redis" ) func main() { redisdb := redis.NewClient(&redis.Options{ Addr: os.Getenv("REDIS_ADDR"), }) server := http.Server{ Addr: ":8080", } http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) server.ListenAndServe() } func processRequest(redisdb *redis.Client) { // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") }

Lorsque nous exécutons notre procédure de vérification à l'aide de ce code, nous verrons que certaines requêtes échouent et que le compteur est inférieur à 1000 (le nombre peut varier à chaque exécution).


Ce qui signifie clairement que nous avons perdu certaines données lors de la mise à jour continue. 😢

Gestion des signaux dans Go

Go fournit un package de signaux qui vous permet de gérer les signaux Unix. Il est important de noter que par défaut, les signaux SIGINT et SIGTERM provoquent la fermeture du programme Go. Et pour que notre application Go ne se ferme pas aussi brusquement, nous devons gérer les signaux entrants.

Il existe deux options pour le faire.


Utilisation du canal :

 c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM)


En utilisant le contexte (approche privilégiée de nos jours) :

 ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop()


NotifyContext renvoie une copie du contexte parent marqué comme terminé (son canal Terminé est fermé) lorsqu'un des signaux répertoriés arrive, lorsque la fonction stop() renvoyée est appelée ou lorsque le canal Terminé du contexte parent est fermé, selon la première éventualité.


Il y a quelques problèmes avec notre implémentation actuelle du serveur HTTP :

  1. Nous avons une goroutine processRequest lente, et comme nous ne gérons pas le signal de terminaison, le programme se termine automatiquement, ce qui signifie que toutes les goroutines en cours d'exécution sont également terminées.
  2. Le programme ne ferme aucune connexion.


Réécrivons-le.


arrêt-gracieux/main.go

 package main // imports var wg sync.WaitGroup func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop() // redisdb, server http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { wg.Add(1) go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) // make it a goroutine go server.ListenAndServe() // listen for the interrupt signal <-ctx.Done() // stop the server if err := server.Shutdown(context.Background()); err != nil { log.Fatalf("could not shutdown: %v\n", err) } // wait for all goroutines to finish wg.Wait() // close redis connection redisdb.Close() os.Exit(0) } func processRequest(redisdb *redis.Client) { defer wg.Done() // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") }


Voici le résumé des mises à jour :

  • Ajout de signal.NotifyContext pour écouter le signal de fin SIGTERM.
  • Introduction d'un sync.WaitGroup pour suivre les demandes en cours (goroutines processRequest).
  • J'ai enveloppé le serveur dans une goroutine et utilisé server.Shutdown avec un contexte pour arrêter gracieusement d'accepter de nouvelles connexions.
  • J'ai utilisé wg.Wait() pour garantir que toutes les requêtes en cours (goroutines processRequest) se terminent avant de continuer.
  • Nettoyage des ressources : ajout de redisdb.Close() pour fermer correctement la connexion Redis avant de quitter.
  • Sortie propre : os.Exit(0) utilisé pour indiquer une terminaison réussie.

Maintenant, si nous répétons notre processus de vérification, nous verrons que les 1000 demandes sont traitées correctement. 🎉


Frameworks Web / Bibliothèque HTTP

Les frameworks comme Echo, Gin, Fiber et autres génèrent une goroutine pour chaque requête entrante, lui donnant un contexte, puis appellent votre fonction/gestionnaire en fonction du routage que vous avez décidé. Dans notre cas, il s'agirait de la fonction anonyme donnée à HandleFunc pour le chemin « /incr ».


Lorsque vous interceptez un signal SIGTERM et demandez à votre infrastructure de s'arrêter correctement, deux choses importantes se produisent (pour simplifier à l'extrême) :

  • Votre framework cesse d'accepter les requêtes entrantes
  • Il attend que toutes les requêtes entrantes existantes se terminent (en attendant implicitement la fin des goroutines).


Remarque : Kubernetes arrête également de diriger le trafic entrant de l'équilibreur de charge vers votre pod une fois qu'il l'a étiqueté comme en cours de terminaison.

Facultatif : Délai d'arrêt

Mettre fin à un processus peut être complexe, surtout s'il implique de nombreuses étapes, comme la fermeture de connexions. Pour garantir le bon déroulement des opérations, vous pouvez définir un délai d'expiration. Ce délai d'expiration agit comme un filet de sécurité, quittant le processus en douceur s'il prend plus de temps que prévu.


 shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() go func() { if err := server.Shutdown(shutdownCtx); err != nil { log.Fatalf("could not shutdown: %v\n", err) } }() select { case <-shutdownCtx.Done(): if shutdownCtx.Err() == context.DeadlineExceeded { log.Fatalln("timeout exceeded, forcing shutdown") } os.Exit(0) }

Cycle de vie de la résiliation de Kubernetes

Puisque nous avons utilisé Kubernetes pour déployer notre service, examinons plus en détail la manière dont il met fin aux pods. Une fois que Kubernetes décide de mettre fin au pod, les événements suivants se produisent :

  1. Le pod est défini sur l’état « Terminé » et supprimé de la liste des points de terminaison de tous les services.
  2. Le hook preStop est exécuté s'il est défini.
  3. Le signal SIGTERM est envoyé au pod. Mais bon, maintenant notre application sait quoi faire !
  4. Kubernetes attend une période de grâce ( terminatedGracePeriodSeconds ), qui est de 30 s par défaut.
  5. Le signal SIGKILL est envoyé au pod, et le pod est retiré.

Comme vous pouvez le constater, si vous disposez d’un processus de terminaison de longue durée, il peut être nécessaire d’augmenter le paramètre terminateGracePeriodSeconds , ce qui laisse à votre application suffisamment de temps pour s’arrêter correctement.

Conclusion

Les arrêts progressifs préservent l'intégrité des données, assurent une expérience utilisateur fluide et optimisent la gestion des ressources. Grâce à sa riche bibliothèque standard et à l'accent mis sur la concurrence, Go permet aux développeurs d'intégrer sans effort des pratiques d'arrêt progressif, une nécessité pour les applications déployées dans des environnements conteneurisés ou orchestrés comme Kubernetes.

Vous pouvez trouver le code Go et les manifestes Kubernetes dans notre référentiel Github .

Ressources