gRPC s'est imposé comme le standard pour la communication inter-services haute performance. Avec cette adoption vient le besoin de mécanismes robustes de health checking.
Le gRPC Health Checking Protocol standardise la façon dont les services rapportent leur état de santé. Ce protocole facilite l'intégration avec Kubernetes et les service meshes.
Qu'est-ce que le Protocole Health Checking gRPC ?
Le gRPC Health Checking Protocol est une spécification définissant un service gRPC standard pour rapporter l'état de santé. Il utilise Protocol Buffers pour garantir l'interopérabilité.
Définition du service
Le service Health est défini en protobuf :
syntax = "proto3";
package grpc.health.v1;
service Health {
// Check synchrone
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
// Watch streaming - reçoit les changements en temps réel
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
message HealthCheckRequest {
string service = 1; // Vide pour le statut global
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
SERVICE_UNKNOWN = 3; // Utilisé par Watch uniquement
}
ServingStatus status = 1;
}
Les deux méthodes
Le service expose deux méthodes complémentaires :
| Méthode | Description | Usage |
|---|---|---|
Check | Vérification synchrone | Load balancers, probes |
Watch | Stream de changements | Réaction temps réel |
Statuts possibles
Les statuts sont clairement définis :
- UNKNOWN : Statut inconnu (valeur par défaut)
- SERVING : Le service accepte les requêtes
- NOT_SERVING : Le service refuse les requêtes
- SERVICE_UNKNOWN : Le service demandé n'existe pas
Granularité par service
Un serveur gRPC peut héberger plusieurs services logiques :
// Requête pour le statut global
healthClient.Check(ctx, &grpc_health_v1.HealthCheckRequest{
Service: "", // Vide = statut global
})
// Requête pour un service spécifique
healthClient.Check(ctx, &grpc_health_v1.HealthCheckRequest{
Service: "mypackage.MyService",
})
Pourquoi le Health Checking gRPC est Essentiel
Intégration Kubernetes native
Kubernetes supporte nativement les probes gRPC depuis la version 1.24 :
apiVersion: v1
kind: Pod
spec:
containers:
- name: grpc-server
ports:
- containerPort: 50051
livenessProbe:
grpc:
port: 50051
service: "" # Statut global
initialDelaySeconds: 10
periodSeconds: 5
readinessProbe:
grpc:
port: 50051
service: "mypackage.MyService"
periodSeconds: 3
Load balancing intelligent
Les load balancers gRPC-aware utilisent le protocole pour détecter les instances unhealthy :
# Configuration Envoy
health_checks:
- timeout: 1s
interval: 5s
grpc_health_check:
service_name: "mypackage.MyService"
Avantage de Watch
La méthode Watch offre une détection instantanée :
// Au lieu de poller toutes les 5 secondes
stream, _ := healthClient.Watch(ctx, &grpc_health_v1.HealthCheckRequest{})
for {
resp, _ := stream.Recv()
// Notification immédiate des changements
handleStatusChange(resp.Status)
}
Comment Implémenter le Health Checking gRPC
Implémentation en Go
Utilisez le package grpc-go/health :
package main
import (
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
)
func main() {
server := grpc.NewServer()
// Créer le service de santé
healthServer := health.NewServer()
// Enregistrer le service Health
grpc_health_v1.RegisterHealthServer(server, healthServer)
// Définir le statut initial
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
healthServer.SetServingStatus("mypackage.MyService", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
// Démarrer le serveur
go func() {
server.Serve(listener)
}()
// Une fois initialisé, passer à SERVING
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
healthServer.SetServingStatus("mypackage.MyService", grpc_health_v1.HealthCheckResponse_SERVING)
}
Gestion du cycle de vie
Implémentez la logique de changement de statut :
func (s *Server) Shutdown() {
// 1. Signaler NOT_SERVING immédiatement
s.healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
// 2. Attendre le drainage des connexions
time.Sleep(5 * time.Second)
// 3. Arrêter gracefully
s.grpcServer.GracefulStop()
}
Vérification des dépendances
Surveillez les dépendances de manière asynchrone :
func (s *Server) monitorDependencies(ctx context.Context) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
dbHealthy := s.checkDatabase()
cacheHealthy := s.checkCache()
if dbHealthy && cacheHealthy {
s.healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
} else {
s.healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
}
}
}
}
Test avec grpc-health-probe
Utilisez l'outil officiel pour tester :
# Installation
go install github.com/grpc-ecosystem/grpc-health-probe@latest
# Test du statut global
grpc-health-probe -addr=localhost:50051
# Test d'un service spécifique
grpc-health-probe -addr=localhost:50051 -service=mypackage.MyService
# Avec timeout
grpc-health-probe -addr=localhost:50051 -connect-timeout=1s -rpc-timeout=1s
Bonnes Pratiques Health Checking gRPC
Distinguez liveness et readiness
Utilisez des vérifications différentes :
// Liveness - minimal et rapide
healthServer.SetServingStatus("liveness", grpc_health_v1.HealthCheckResponse_SERVING)
// Readiness - vérifie les dépendances
func updateReadiness() {
if allDependenciesHealthy() {
healthServer.SetServingStatus("readiness", grpc_health_v1.HealthCheckResponse_SERVING)
} else {
healthServer.SetServingStatus("readiness", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
}
}
Shutdown graceful
Séquencez correctement l'arrêt :
func gracefulShutdown(s *Server, sig os.Signal) {
log.Printf("Received signal %v, starting graceful shutdown", sig)
// 1. Marquer comme NOT_SERVING
s.healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
// 2. Attendre que les probes détectent le changement
time.Sleep(s.config.GracePeriod)
// 3. Drainer les connexions existantes
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
done := make(chan struct{})
go func() {
s.grpcServer.GracefulStop()
close(done)
}()
select {
case <-done:
log.Println("Graceful shutdown completed")
case <-ctx.Done():
log.Println("Forcing shutdown")
s.grpcServer.Stop()
}
}
Timeouts appropriés
Configurez des timeouts raisonnables :
# Kubernetes probe configuration
livenessProbe:
grpc:
port: 50051
initialDelaySeconds: 15 # Temps de démarrage
periodSeconds: 10
timeoutSeconds: 3 # Doit répondre en 3s
failureThreshold: 3 # 3 échecs avant redémarrage
Logging des transitions
Loguez chaque changement de statut :
func (s *Server) setStatus(service string, status grpc_health_v1.HealthCheckResponse_ServingStatus) {
previousStatus := s.currentStatus[service]
s.healthServer.SetServingStatus(service, status)
s.currentStatus[service] = status
if previousStatus != status {
log.Printf("Health status changed: service=%s, from=%s, to=%s",
service, previousStatus, status)
}
}
Tests de scénarios de défaillance
Testez les comportements de health check :
func TestHealthCheckDuringDbOutage(t *testing.T) {
server := NewTestServer()
// Simuler une panne DB
server.mockDB.SetAvailable(false)
// Attendre la mise à jour du health check
time.Sleep(6 * time.Second)
// Vérifier le statut
resp, _ := server.healthClient.Check(ctx, &grpc_health_v1.HealthCheckRequest{})
assert.Equal(t, grpc_health_v1.HealthCheckResponse_NOT_SERVING, resp.Status)
}
Conclusion
Le protocole gRPC Health Checking standardise l'observabilité des services gRPC. En l'implémentant correctement, vos services s'intègrent naturellement avec :
- Kubernetes
- Service meshes (Istio, Linkerd)
- Load balancers (Envoy, Traefik)
Cette standardisation améliore la résilience globale de votre architecture microservices.