Go Programming

Construire des microservices haute performance en Go avec gRPC

Dans le paysage moderne des systèmes distribués, la performance et l'efficacité sont non négociables. À mesure que les applications grandissent, la surcharge de communication entre les services peut devenir un goulot d'étranglement significatif. C'est là que gRPC brille. En tirant parti des Protocol Buffers (Protobuf) comme langage de définition d'interface (IDL) et d'HTTP/2 pour le transport, gRPC offre une alternative robuste, efficace et strictement typée aux API REST traditionnelles, ce qui en fait un choix idéal pour les microservices Go.

Pourquoi choisir gRPC pour Go ?

Go est renommé pour son modèle de concurrence et sa bibliothèque standard, ce qui en fait un concurrent de premier plan pour l'infrastructure backend. Lorsqu'il est associé à gRPC, les développeurs bénéficient de :

  • Façonnage fort (Strong Typing) : Défini dans les fichiers .proto, garantissant la stabilité du contrat.
  • Efficacité binaire : La sérialisation Protobuf est nettement plus petite et plus rapide que JSON.
  • Streaming natif : Support intégré pour le streaming client, serveur et bidirectionnel.
  • Performance : Le multiplexage HTTP/2 réduit la surcharge des connexions.

Définir le contrat du service

La fondation de tout service gRPC est le fichier .proto. Ce fichier définit les structures de données et les méthodes RPC. Définissons un service simple pour un service Utilisateur.

1. La définition Protobuf

Créez un fichier nommé user.proto. Nous allons définir une structure de message pour l'utilisateur et une interface de service.

syntax = "proto3";

package user;

option go_package = "github.com/example/service/user";

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

service UserService {
  rpc GetUser (GetUserRequest) returns (User) {}
  rpc ListUsers (ListUsersRequest) returns (stream User) {}
}

message GetUserRequest {
  int32 id = 1;
}

message ListUsersRequest {
  string filter = 1;
}

Notez l'utilisation de stream User dans la méthode ListUsers. Cela indique un streaming côté serveur, où le serveur envoie une séquence de réponses à une seule requête client.

Génération de code

Une fois votre fichier .proto prêt, vous devez générer le code Go qui implémente les interfaces. Assurez-vous d'avoir le compilateur protoc et les plugins Go installés.

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

protoc --go_out=. --go-grpc_out=. user.proto

Cette commande génère deux fichiers : user.pb.go (contenant les structs de message) et user_grpc.pb.go (contenant les stubs client et serveur).

Implémentation du serveur

Maintenant, implémentons l'interface UserService en Go. Nous allons créer un serveur qui satisfait l'interface générée.

package main

import (
    "context"
    "log"
    "net"

    pb "github.com/example/service/user"
    "google.golang.org/grpc"
)

// server implémente le UserService
type server struct {
    pb.UnimplementedUserServiceServer
}

func (s *server) GetUser(ctx context.Context, in *pb.GetUserRequest) (*pb.User, error) {
    // Récupération de données fictives
    return &pb.User{
        Id:    in.Id,
        Name:  "John Doe",
        Email: "john@example.com",
    }, nil
}

func (s *server) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
    // Liste fictive d'utilisateurs
    users := []*pb.User{
        {Id: 1, Name: "Alice", Email: "alice@example.com"},
        {Id: 2, Name: "Bob", Email: "bob@example.com"},
    }
    
    for _, user := range users {
        if err := stream.Send(user); err != nil {
            return err
        }
    }
    return nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    s := grpc.NewServer()
    pb.RegisterUserServiceServer(s, &server{})

    log.Println("Server listening on :50051")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

Construction du client

Avec le serveur en cours d'exécution, un client peut se connecter et invoquer des RPC. Voici comment vous appelleriez les méthodes GetUser et ListUsers.

package main

import (
    "context"
    "log"

    pb "github.com/example/service/user"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func main() {
    // Établir une connexion au serveur
    conn, err := grpc.Dial(":50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()

    c := pb.NewUserServiceClient(conn)

    // Appeler un RPC unaire
    ctx, cancel := context.WithTimeout(context.Background(), 1000000)
    defer cancel()
    
    r, err := c.GetUser(ctx, &pb.GetUserRequest{Id: 1})
    if err != nil {
        log.Fatalf("could not get user: %v", err)
    }
    log.Printf("User: %+v", r)

    // Appeler un RPC streaming serveur
    stream, err := c.ListUsers(ctx, &pb.ListUsersRequest{Filter: "all"})
    if err != nil {
        log.Fatalf("could not list users: %v", err)
    }
    
    for {
        msg, err := stream.Recv()
        if err == nil {
            log.Printf("User: %+v", msg)
        } else {
            break
        }
    }
}

Conclusion

L'intégration de gRPC dans votre architecture de microservices Go fournit un outil puissant pour construire des systèmes évolutifs et haute performance. Bien que REST reste une norme polyvalente pour les API exposées au public, l'efficacité et la sécurité des types de gRPC le rendent supérieur pour la communication interne entre services. En maîtrisant les définitions Protobuf et en tirant parti des primitives de concurrence de Go, les développeurs peuvent construire des systèmes distribués robustes qui résistent à l'épreuve de l'échelle.

Share: