Go Programming

Maîtriser l'observabilité : Implémenter le traçage distribué dans les microservices Go gRPC

Lorsque les organisations migrent d'architectures monolithiques vers des microservices, la complexité du débogage et de la surveillance augmente de façon exponentielle. Dans un paysage de microservices, une seule requête utilisateur peut traverser des dizaines de services, de bases de données et de files d'attente de messages. Sans visibilité sur ces interactions, identifier les goulots d'étranglement de performance ou les causes racines des pannes devient un problème de recherche d'une aiguille dans une botte de foin. C'est là qu'interviennent le traçage distribué et l'observabilité. Ce guide explore comment mettre en œuvre une observabilité robuste dans les services Go gRPC à l'aide d'outils standard de l'industrie tels qu'OpenTelemetry et Jaeger.

Pourquoi la journalisation standard ne suffit pas

La journalisation traditionnelle fournit un instantané des événements individuels au sein d'un service. Cependant, les journaux sont cloisonnés. Lorsqu'une requête échoue sur plusieurs services, il est notoirement difficile de corrélérer les entrées de journal pour reconstituer le chemin complet de la transaction. Le traçage distribué résout ce problème en attribuant un trace_id unique à chaque requête entrante et en le propageant à travers les limites des services. Cela permet aux ingénieurs de visualiser l'intégralité du cycle de vie d'une requête, de comprendre la contribution de chaque saut à la latence et de localiser précisément où les choses ont mal tourné.

Mise en place de la pile d'instrumentation

Pour cette implémentation, nous utiliserons OpenTelemetry (OTel), le framework neutre en termes de fournisseur standard pour l'observabilité, et Jaeger comme backend pour collecter et visualiser les traces. Tout d'abord, assurez-vous que Jaeger est en cours d'exécution dans votre environnement, généralement via Docker :

docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 14250:14250 \
  -p 9411:9411 \
  jaegertracing/all-in-one:latest

Instrumentation du serveur gRPC

L'implémentation du traçage dans les services Go gRPC nécessite d'injecter un middleware dans la chaîne d'intercepteurs gRPC. L'objectif est de capturer automatiquement le contexte de trace, les informations sur les spans et les métadonnées pour chaque appel RPC. Nous utiliserons le package go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc, qui effectue la majeure partie du travail.

Voici un exemple pratique de la manière de configurer un serveur gRPC avec l'instrumentation OpenTelemetry :

package main

import (
    "context"
    "log"
    "net"

    pb "your-project/proto" // Code gRPC généré

    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/jaeger"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
    "google.golang.org/grpc"
)

func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
    // Créer l'exportateur Jaeger
    exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://localhost:14268/api/traces")))
    if err != nil {
        return nil, err
    }

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(resource.NewWithAttributes(
            semconv.ServiceNameKey.String("my-grpc-server"),
        )),
    )
    otel.SetTracerProvider(tp)
    return tp, nil
}

func main() {
    ctx := context.Background()
    tp, err := initTracer(ctx)
    if err != nil {
        log.Fatalf("Impossible d'initialiser le traceur : %v", err)
    }
    defer tp.Shutdown(ctx)

    // Créer le serveur gRPC avec le middleware d'instrumentation otelgrpc
    opts := []grpc.ServerOption{
        grpc.StatsHandler(otelgrpc.NewServerHandler()),
    }
    s := grpc.NewServer(opts...)
    
    pb.RegisterMyServiceServer(s, &myServer{})

    listen, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("échec de l'écoute : %v", err)
    }
    log.Printf("serveur à l'écoute sur %v", listen.Addr())
    if err := s.Serve(listen); err != nil {
        log.Fatalf("échec du service : %v", err)
    }
}

La ligne clé ici est grpc.StatsHandler(otelgrpc.NewServerHandler()). Cela crée automatiquement des spans pour les RPC entrants et sortants, attache le contexte de trace aux métadonnées et enregistre les métriques de latence.

Instrumentation du client gRPC

Côté client, la configuration est symétrique. Vous devez vous assurer que le contexte de trace est propagé au service suivant dans la chaîne. En utilisant la même bibliothèque otelgrpc, nous configurons le dialer du client :

func newClient(ctx context.Context) (pb.MyServiceClient, *grpc.ClientConn, error) {
    conn, err := grpc.DialContext(ctx, "localhost:50051",
        grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
        return nil, nil, err
    }
    client := pb.NewMyServiceClient(conn)
    return client, conn, nil
}

En injectant le gestionnaire de statistiques du client, la bibliothèque gRPC injecte automatiquement les en-têtes de parent de trace dans les requêtes sortantes. Cela garantit que lorsque le serveur reçoit l'appel, il peut poursuivre la trace existante au lieu d'en démarrer une nouvelle.

Avancé : Attributs personnalisés et Spans

Bien que l'instrumentation automatique couvre les bases, une observabilité avancée nécessite souvent d'ajouter un contexte spécifique à l'entreprise. Vous pouvez le faire en créant des spans manuels. Par exemple, si votre gestionnaire gRPC effectue une requête de base de données, vous pourriez vouloir ajouter un span enfant pour suivre cette opération spécifique :

tracer := otel.Tracer("my-service")
ctx, span := tracer.Start(ctx, "database_query")
defer span.End()

// Effectuer l'opération DB ici
// Le span enregistrera automatiquement l'heure de début/fin et le statut

De plus, vous pouvez définir des attributs sur les spans pour capturer des métadonnées telles que les identifiants utilisateur ou les messages d'erreur, ce qui rend le filtrage et la recherche dans Jaeger beaucoup plus puissants.

Conclusion

Implémenter le traçage distribué dans les microservices Go gRPC n'est pas seulement une bonne pratique ; c'est une nécessité pour maintenir la fiabilité du système à grande échelle. En tirant parti d'OpenTelemetry et de Jaeger, les développeurs peuvent obtenir des informations approfondies sur le comportement du système, réduire le temps moyen de résolution (MTTR) des incidents et optimiser les performances. Les extraits de code fournis démontrent qu'avec des bibliothèques modernes, la barrière à l'entrée est faible. Commencez par instrumenter vos services dès aujourd'hui et construisez la fondation d'observabilité que votre architecture de microservices mérite.

Share: