Lors de la création de systèmes distribués avec Go et gRPC, la disponibilité est primordiale. Il ne suffit pas de démarrer un serveur et d'espérer qu'il reste en marche. Dans les environnements cloud-natifs modernes, les outils d'orchestration comme Kubernetes ou les maillages de services comme Istio reposent fortement sur une gestion précise du cycle de vie. Cela nécessite de mettre en œuvre des protocoles de vérification de santé robustes et de garantir que les services s'arrêtent de manière progressive pour éviter la perte de données ou la rupture des connexions.
Dans cet article, nous explorerons comment intégrer le protocole standard grpc-health dans vos serveurs Go et comment gérer les signaux pour un arrêt propre. Nous irons au-delà des implémentations de base pour aborder les défis de concurrence, tels que les conditions de course lors de l'arrêt.
Mise en œuvre du service de vérification de santé gRPC
L'écosystème gRPC dispose d'un protocole de vérification de santé standardisé défini dans grpc/health/v1. Au lieu d'écrire des points de terminaison personnalisés pour vérifier si votre service est prêt, vous devez implémenter cette interface standard. Cela permet aux équilibreurs de charge et aux orchestrateurs d'interroger le service sans avoir besoin de connaître la logique interne de votre domaine métier.
Pour commencer, vous devez installer le package de santé. En supposant que vous utilisez les modules Go, exécutez :
go get google.golang.org/grpc/health
go get google.golang.org/grpc/health/grpc_health_v1
Une fois installé, vous pouvez enregistrer le service de santé avec votre serveur gRPC. Le service de santé maintient une carte des noms de services vers leurs statuts de santé. Par défaut, vous enregistrez le service sous une chaîne vide ou le nom spécifique du service défini dans votre protobuf.
Voici une implémentation pratique d'un vérificateur de santé qui surveille l'état global de votre application :
package main
import (
"log"
"net"
"time"
"google.golang.org/grpc"
health "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/health"
)
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
// Enregistrez vos services métier ici
// pb.RegisterMyServiceServer(s, &myServer{})
// Initialiser le service de santé
healthSvc := health.NewServer()
// Définir le statut par défaut sur SERVING
// Cela indique aux orchestrateurs que le serveur est prêt
healthSvc.SetServingStatus("", health.CheckResponse_SERVING)
// Enregistrer le service de santé avec le serveur gRPC
health.RegisterHealthServer(s, healthSvc)
log.Println("Démarrage du serveur gRPC sur 50051...")
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Cette configuration permet à tout client d'appeler grpc.health.v1.Health.Check pour déterminer si le serveur accepte le trafic. Bien que l'implémentation par défaut soit simple, dans des scénarios complexes, vous pourriez vouloir substituer la méthode Check pour vérifier la connectivité à la base de données ou la disponibilité des services en aval avant de signaler "SERVING".
Gestion de l'arrêt progressif
L'arrêt brutal d'un serveur gRPC peut entraîner des requêtes partiellement traitées, des connexions rompues et des incohérences de données. L'arrêt progressif implique l'arrêt de l'acceptation des nouvelles RPC, le vidage des connexions existantes, puis l'arrêt de l'écouteur.
La bibliothèque standard de Go fournit os/signal pour gérer les signaux du système d'exploitation tels que SIGINT (Ctrl+C) et SIGTERM (utilisé par Kubernetes). Cependant, le flux d'arrêt standard souffre souvent d'une condition de course : l'application peut tenter d'arrêter le service de santé avant que le serveur gRPC ne soit complètement arrêté, ce qui entraîne des erreurs ou des interblocages.
Pour résoudre ce problème, nous devons nous assurer que le service de santé est arrêté après que le serveur gRPC principal a terminé son arrêt. Voici un motif robuste pour gérer cela :
package main
import (
"context"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
"google.golang.org/grpc"
health "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/health"
)
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
healthSvc := health.NewServer()
healthSvc.SetServingStatus("", health.CheckResponse_SERVING)
health.RegisterHealthServer(s, healthSvc)
// Démarrer le serveur dans un goroutine
go func() {
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}()
// Créer un canal pour écouter les signaux d'interruption
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
// Bloquer jusqu'à ce qu'un signal soit reçu
sig := <-stop
log.Printf("Signal reçu : %v, initiation de l'arrêt progressif...", sig)
// Créer un contexte avec une temporisation pour le processus d'arrêt
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 1. Arrêter l'acceptation des nouvelles RPC
s.GracefulStop()
// 2. Mettre à jour le statut de santé sur NOT_SERVING immédiatement
// Cela assure que les équilibreurs de charge cessent de router le trafic
healthSvc.SetServingStatus("", health.CheckResponse_NOT_SERVING)
// 3. Arrêter le service de santé
// Note : Cela doit se produire après s.GracefulStop() ou dans une routine séparée
// pour éviter les conditions de course.
healthSvc.Shutdown()
log.Println("Arrêt terminé.")
}
Meilleures pratiques pour la production
Lors de la mise en œuvre de ces motifs, gardez à l'esprit les points suivants :
- Les temporisations sont critiques : Définissez toujours une durée maximale pour votre période de grâce d'arrêt. Si votre service se bloque, vous ne voulez pas que l'orchestrateur attende indéfiniment.
- Granularité de la vérification de santé : Pour les microservices critiques, envisagez de mettre en œuvre séparément les sondes de vivacité (liveness) et de préparation (readiness). Les vérifications de vivacité redémarrent les conteneurs défectueux, tandis que les vérifications de préparation empêchent le trafic d'atteindre les instances non prêtes.
- Sécurité de la concurrence : L'ordre des opérations lors de l'arrêt est important. Assurez-vous toujours que les services internes sont notifiés ou arrêtés dans un ordre de dépendance pour éviter les fuites de ressources.
Conclusion
La mise en œuvre des vérifications de santé gRPC et des arrêts progressifs n'est pas seulement une bonne pratique ; c'est une nécessité pour construire des microservices résilients en Go. En tirant parti du protocole de santé standard et en gérant soigneusement la gestion des signaux, vous garantissez que vos services s'intègrent parfaitement aux outils d'infrastructure modernes. Cette approche minimise les temps d'arrêt, empêche les échecs de requêtes partielles et fournit une visibilité claire sur la santé de votre système.