Pourquoi les microservices modernes préfèrent gRPC à REST
Dans une architecture monolithique, les composants communiquent via des appels de méthodes en mémoire, instantanés et hautement fiables. Toutefois, lors du passage à une architecture de microservices, ces composants sont séparés par les limites du réseau. La communication devient un appel réseau hors processus (Inter-Process Communication ou IPC).
Depuis des années, REST (Representational State Transfer) sur HTTP/1.1 avec charges utiles JSON est la norme par défaut pour la création d’API Web. Bien que REST soit excellent pour les services Web publics et l’interaction client-serveur, il introduit des goulots d’étranglement importants lorsqu’il est utilisé pour une communication interne de service à service à haute fréquence et à faible latence.
C’est pourquoi les microservices modernes évoluent rapidement vers gRPC (Google Remote Procedure Call). Dans cet article, nous analyserons les limites de REST, décortiquerons les piliers architecturaux de gRPC et découvrirons une implémentation complète d’un service gRPC en Java.
1. Les goulots d’étranglement de REST dans les microservices
REST alimente le Web depuis plus de deux décennies. Cependant, ses technologies sous-jacentes ne sont pas optimisées pour les architectures distribuées internes :
A. La surcharge du JSON en texte brut
JSON est lisible par l’homme, ce qui facilite le débogage, mais il est extrêmement inefficace pour la communication de machine à machine :
- Coût de sérialisation/désérialisation : l’analyse des jetons de chaîne de texte et leur conversion en objets consomment des cycles CPU importants.
- Grande taille de charge utile : les clés JSON sont répétées dans chaque requête (par exemple,
{"transactionId": "123", "amount": 99.99}). Pour les systèmes traitant des millions de requêtes par seconde, ces métadonnées redondantes gaspillent une immense bande passante réseau.
B. Limites de connexion HTTP/1.1
REST fonctionne généralement sur HTTP/1.1, ce qui présente plusieurs inefficacités structurelles :
- Blocage en tête de ligne (HoL) : sur une seule connexion TCP, un client doit attendre la réponse de la requête en cours avant d’envoyer la suivante.
- Épuisement du pooling de connexions : pour gérer les demandes simultanées, les clients doivent ouvrir plusieurs connexions TCP. Cela entraîne une surcharge importante des ressources du système d’exploitation, un épuisement des sockets et des pénalités de performances de démarrage lent, car les connexions sont continuellement ouvertes et fermées.
C. Contrats API faibles
Les API REST ne disposent pas d’un contrat intégré au moment de la compilation. Bien qu’OpenAPI/Swagger aide à documenter les API, elles sont distinctes de la base de code. Il est facile pour un développeur back-end de modifier le nom d’un champ JSON et d’interrompre accidentellement les services en aval sans avertissement au moment de la compilation.
2. Les piliers architecturaux de gRPC
Introduit par Google en 2015, gRPC est un framework RPC open source hautes performances conçu spécifiquement pour les applications cloud natives. Il résout les goulots d’étranglement de REST à l’aide de trois technologies clés :
A. Tampons de protocole (Protobuf)
Au lieu de JSON, gRPC utilise des Protocol Buffers comme langage de définition d’interface (IDL) et format de sérialisation des messages.
- Protobuf est un mécanisme de sérialisation binaire hautement optimisé.
- Les champs sont sérialisés sous forme de petites balises numériques plutôt que de noms de texte.
- Les charges utiles sont jusqu’à 70 à 80 % plus petites que JSON, et la sérialisation est jusqu’à 10 fois plus rapide, réduisant considérablement l’utilisation du processeur et de la bande passante.
B. Multiplexage HTTP/2
gRPC utilise HTTP/2 comme protocole de transport, apportant plusieurs optimisations de performances :
- Véritable multiplexage : plusieurs requêtes et réponses peuvent être entrelacées simultanément sur une seule connexion TCP de longue durée, éliminant ainsi complètement le blocage en tête de ligne.
- Compression d’en-tête (HPACK) : les en-têtes HTTP sont compressés, réduisant encore davantage la taille de la requête.
- Prise en charge du streaming : HTTP/2 prend en charge de manière native le streaming client unidirectionnel, le streaming serveur et le streaming bidirectionnel complet.
C. Génération de code et contrats stricts
En définissant la structure de l’API dans un fichier .proto, gRPC génère des stubs clients et des classes de base serveur dans des dizaines de langages (Java, Go, Python, C++, etc.). Le contrat est sécurisé au moment de la compilation ; si le serveur met à jour son interface, les builds client échouent immédiatement si elles ne correspondent pas au nouveau contrat.
3. Implémentation Java : créer un service gRPC
Créons une application Java gRPC pratique. Nous mettrons en œuvre un service de traitement des paiements à haut débit dans lequel un client soumet une demande de paiement et reçoit une confirmation de transaction.
Étape 1 : Définir le contrat API (payment.proto)
Nous commençons par définir notre interface de service et nos structures de messages dans un fichier Protocol Buffer.
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.ghaznix.grpc.payment";
option java_outer_classname = "PaymentProto";
package payment;
// The Payment Service definition
service PaymentService {
// Unary RPC: Sends a single payment request and receives a confirmation
rpc ProcessPayment (PaymentRequest) returns (PaymentResponse);
}
// The Payment Request message
message PaymentRequest {
string transaction_id = 1;
string customer_id = 2;
double amount = 3;
string currency = 4;
}
// The Payment Response message
message PaymentResponse {
string transaction_id = 1;
string status = 2;
string message = 3;
string timestamp = 4;
}
Étape 2 : implémenter la logique côté serveur
Après avoir compilé le fichier .proto (généralement géré automatiquement par les plugins Maven ou Gradle gRPC), l’outil génère la classe PaymentServiceImplBase. Nous étendons cette classe pour implémenter la logique métier.
package com.ghaznix.grpc.payment;
import io.grpc.stub.StreamObserver;
import java.time.Instant;
public class PaymentServiceImpl extends PaymentServiceGrpc.PaymentServiceImplBase {
@Override
public void processPayment(PaymentRequest request, StreamObserver<PaymentResponse> responseObserver) {
System.out.println("Received payment request for customer: " + request.getCustomerId()
+ " with amount: " + request.getAmount() + " " + request.getCurrency());
// Process payment logic (simulation)
String status = request.getAmount() > 0 ? "SUCCESS" : "DECLINED";
String message = status.equals("SUCCESS") ? "Payment processed successfully." : "Invalid payment amount.";
PaymentResponse response = PaymentResponse.newBuilder()
.setTransactionId(request.getTransactionId())
.setStatus(status)
.setMessage(message)
.setTimestamp(Instant.now().toString())
.build();
// Send the response to the client
responseObserver.onNext(response);
// Signal that the RPC execution is complete
responseObserver.onCompleted();
}
}
Étape 3 : Amorcer le serveur gRPC
Ensuite, nous créons une classe de serveur pour démarrer le serveur gRPC sur un port spécifique et enregistrons notre implémentation de service.
package com.ghaznix.grpc.payment;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class PaymentServer {
private Server server;
public void start() throws IOException {
int port = 50051;
server = ServerBuilder.forPort(port)
.addService(new PaymentServiceImpl())
.build()
.start();
System.out.println("gRPC Server started, listening on port " + port);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.err.println("*** Shutting down gRPC server since JVM is shutting down");
try {
PaymentServer.this.stop();
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
System.err.println("*** Server shut down");
}));
}
public void stop() throws InterruptedException {
if (server != null) {
server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
}
}
public void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
public static void main(String[] args) throws IOException, InterruptedException {
final PaymentServer server = new PaymentServer();
server.start();
server.blockUntilShutdown();
}
}
Étape 4 : Implémenter le client (stubs de blocage et asynchrones)
L’un des principaux avantages de gRPC est qu’il génère deux types de stubs pour le client :
- BlockingStub : exécution synchrone et bloquante (similaire aux appels REST standard).
- Stub : exécution asynchrone et non bloquante à l’aide d’observateurs de rappel.
package com.ghaznix.grpc.payment;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.util.concurrent.TimeUnit;
public class PaymentClient {
private final ManagedChannel channel;
private final PaymentServiceGrpc.PaymentServiceBlockingStub blockingStub;
public PaymentClient(String host, int port) {
// Create a communication channel to the server
this.channel = ManagedChannelBuilder.forAddress(host, port)
.usePlaintext() // Disables TLS for local development
.build();
// Create the synchronous blocking stub
this.blockingStub = PaymentServiceGrpc.newBlockingStub(channel);
}
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
public void executePayment(String txId, String customerId, double amount) {
PaymentRequest request = PaymentRequest.newBuilder()
.setTransactionId(txId)
.setCustomerId(customerId)
.setAmount(amount)
.setCurrency("USD")
.build();
System.out.println("Sending payment request for: " + txId);
// Execute unary synchronous call
PaymentResponse response = blockingStub.processPayment(request);
System.out.println("Server Response: " + response.getStatus() + " | " + response.getMessage());
}
public static void main(String[] args) throws InterruptedException {
PaymentClient client = new PaymentClient("localhost", 50051);
try {
client.executePayment("TX-99081", "customer-abc-123", 250.75);
} finally {
client.shutdown();
}
}
}
4. Comparaison architecturale : gRPC vs REST
Le tableau ci-dessous présente les principales caractéristiques des deux paradigmes :
| Élément architectural | REST (Transfert d’État Représentatif) | gRPC (appel de procédure à distance Google) |
|---|---|---|
| Protocole | HTTP/1.1 (standard), HTTP/2 (facultatif) | HTTP/2 (exigence stricte) |
| Format de charge utile | Texte brut JSON, XML, HTML | Tampons de protocole binaire (Protobuf) |
| Paradigme API | Ressources (chemins URI + GET/POST/PUT/DELETE) | Procédures distantes (appels de méthodes/fonctions) |
| Qualité du contrat | Loose (OpenAPI/Swagger est facultatif/externe) | Strict (Défini dans les fichiers .proto lors de la compilation) |
| Multiplexage | Non (blocage de tête de ligne dans HTTP/1.1) | Oui (plusieurs flux sur une seule connexion TCP) |
| Types de diffusion en continu | Événements unidirectionnels envoyés par le serveur (SSE) uniquement | Client, serveur et streaming bidirectionnel |
| Génération de code | Outils externes nécessaires (par exemple, Swagger Codegen) | Intégré via le compilateur Protobuf (protoc) |
| Prise en charge du navigateur | Universel (Accès direct au client Web) | Limité (nécessite une traduction proxy grpc-web) |
5. Quand choisir gRPC plutôt que REST ?
Malgré les énormes avantages de gRPC en termes de performances, ce n’est pas une solution miracle. Le choix du protocole dépend du contexte au sein de votre architecture système :
- Utilisez gRPC pour le maillage de microservices interne : pour les communications de service à service à volume élevé, la faible latence, la compacité binaire et les capacités de streaming de gRPC en font le choix idéal. Il réduit la surcharge du processeur interne du serveur et accélère les temps de réponse globaux.
- Utilisez REST pour les API Edge/publiques : étant donné que les navigateurs Web ne peuvent pas effectuer nativement d’appels gRPC sans couches de traduction, REST reste le meilleur choix pour les API publiques, les intégrations avec des webhooks tiers et la communication directe des navigateurs clients vers la périphérie de votre architecture.
En combinant les deux (exposer REST au niveau API Gateway et tirer parti de gRPC pour la communication interne des microservices), les architectes peuvent créer des systèmes à la fois hautement interopérables et ultra-rapides.