Perché i microservizi moderni preferiscono gRPC rispetto a REST
In un’architettura monolitica, i componenti comunicano tramite chiamate di metodi in memoria, che sono istantanee e altamente affidabili. Quando si passa a un’architettura a microservizi, tuttavia, questi componenti sono separati dai confini della rete. La comunicazione diventa una chiamata di rete fuori processo (Inter-Process Communication o IPC).
Per anni, REST (Representational State Transfer) su HTTP/1.1 con payload JSON è stato lo standard predefinito per la creazione di API Web. Sebbene REST sia eccellente per i servizi Web rivolti al pubblico e l’interazione da client a server, introduce colli di bottiglia significativi se utilizzato per comunicazioni interne da servizio a servizio ad alta frequenza e bassa latenza.
Questo è il motivo per cui i moderni microservizi si stanno rapidamente spostando verso gRPC (Google Remote Procedure Call). In questo articolo analizzeremo i limiti di REST, analizzeremo i pilastri dell’architettura di gRPC e analizzeremo un’implementazione completa di un servizio gRPC in Java.
1. I colli di bottiglia del REST nei microservizi
REST alimenta il web da oltre due decenni. Tuttavia, le tecnologie sottostanti non sono ottimizzate per le architetture distribuite interne:
A. Il sovraccarico del JSON in testo normale
JSON è leggibile dall’uomo, il che semplifica il debug, ma è estremamente inefficiente per la comunicazione da macchina a macchina:
- Costo di serializzazione/deserializzazione: l’analisi dei token di stringhe di testo e la loro conversione in oggetti consuma cicli CPU sostanziali.
- Grandi dimensioni del payload: le chiavi JSON vengono ripetute in ogni singola richiesta (ad esempio,
{"transactionId": "123", "amount": 99.99}). Per i sistemi che elaborano milioni di richieste al secondo, questi metadati ridondanti sprecano un’immensa larghezza di banda della rete.
B. Limitazioni della connessione HTTP/1.1
REST viene generalmente eseguito su HTTP/1.1, che presenta diverse inefficienze strutturali:
- Blocco Head-of-Line (HoL): su una singola connessione TCP, un client deve attendere la risposta della richiesta corrente prima di inviare quella successiva.
- Esaurimento pool di connessioni: per gestire richieste simultanee, i client devono aprire più connessioni TCP. Ciò si traduce in un notevole sovraccarico delle risorse del sistema operativo, nell’esaurimento dei socket e in penalità nelle prestazioni di avvio lento poiché le connessioni vengono aperte e chiuse continuamente.
C. Contratti API deboli
Le API REST non dispongono di un contratto integrato in fase di compilazione. Sebbene OpenAPI/Swagger aiuti a documentare le API, sono separate dalla base di codice. È facile per uno sviluppatore backend modificare il nome di un campo JSON e interrompere accidentalmente i servizi downstream senza avvisi in fase di compilazione.
2. I pilastri architettonici di gRPC
Introdotto da Google nel 2015, gRPC è un framework RPC open source ad alte prestazioni progettato specificamente per applicazioni native del cloud. Risolve i colli di bottiglia di REST utilizzando tre tecnologie chiave:
A. Buffer di protocollo (Protobuf)
Invece di JSON, gRPC utilizza Protocol Buffers come Interface Definition Language (IDL) e formato di serializzazione dei messaggi.
- Protobuf è un meccanismo di serializzazione binaria altamente ottimizzato.
- I campi vengono serializzati come piccoli tag numerici anziché come nomi di testo.
- I payload sono fino al 70-80% più piccoli di JSON e la serializzazione è fino a 10 volte più veloce, riducendo significativamente l’utilizzo della CPU e della larghezza di banda.
B. Multiplexing HTTP/2
gRPC utilizza HTTP/2 come protocollo di trasporto, apportando diverse ottimizzazioni delle prestazioni:
- True Multiplexing: richieste e risposte multiple possono essere interlacciate simultaneamente su una singola connessione TCP di lunga durata, eliminando completamente il blocco Head-of-Line.
- Compressione delle intestazioni (HPACK): le intestazioni HTTP vengono compresse, riducendo ulteriormente le dimensioni della richiesta.
- Supporto streaming: HTTP/2 supporta nativamente lo streaming client unidirezionale, lo streaming server e lo streaming bidirezionale completo.
C. Generazione di codici e contratti rigorosi
Definendo la struttura API in un file .proto, gRPC genera stub client e classi base server in decine di linguaggi (Java, Go, Python, C++, ecc.). Il contratto è sicuro in fase di compilazione; se il server aggiorna la sua interfaccia, le build del client falliscono immediatamente se non corrispondono al nuovo contratto.
3. Implementazione Java: creazione di un servizio gRPC
Costruiamo una pratica applicazione Java gRPC. Implementeremo un Servizio di elaborazione dei pagamenti ad alto rendimento in cui un cliente invia una richiesta di pagamento e riceve una conferma di transazione.
Passaggio 1: definire il contratto API (payment.proto)
Iniziamo definendo la nostra interfaccia di servizio e le strutture dei messaggi in un file Buffer di protocollo.
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;
}
Passaggio 2: implementare la logica lato server
Dopo aver compilato il file .proto (solitamente gestito automaticamente dai plugin Maven o Gradle gRPC), lo strumento genera la classe PaymentServiceImplBase. Estendiamo questa classe per implementare la logica aziendale.
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();
}
}
Passaggio 3: avviare il server gRPC
Successivamente, creiamo una classe server per avviare il server gRPC su una porta specifica e registrare l’implementazione del nostro servizio.
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();
}
}
Passaggio 4: implementare il client (stub di blocco e asincroni)
Uno dei principali vantaggi di gRPC è che genera due tipi di stub per il client:
- BlockingStub: esecuzione sincrona e bloccante (simile alle chiamate REST standard).
- Stub: esecuzione asincrona, non bloccante utilizzando osservatori di callback.
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. Confronto architettonico: gRPC vs. REST
La tabella seguente delinea le caratteristiche principali di entrambi i paradigmi:
| Caratteristica architettonica | REST (Trasferimento di Stato rappresentativo) | gRPC (Chiamata di procedura remota di Google) |
|---|---|---|
| Protocollo | HTTP/1.1 (standard), HTTP/2 (facoltativo) | HTTP/2 (requisito rigoroso) |
| Formato del carico utile | JSON, XML, HTML in testo semplice | Buffer del protocollo binario (Protobuf) |
| Paradigma API | Risorse (percorsi URI + GET/POST/PUT/DELETE) | Procedure remote (chiamate di metodi/funzioni) |
| Qualità contrattuale | Sciolto (OpenAPI/Swagger è opzionale/esterno) | Rigoroso (definito nei file .proto al momento della compilazione) |
| Multiplexing | No (blocco head-of-line in HTTP/1.1) | Sì (flussi multipli su una singola connessione TCP) |
| Tipi di streaming | Solo eventi inviati dal server unidirezionali (SSE) | Client, server e streaming bidirezionale |
| Generazione del codice | Sono necessari strumenti esterni (ad esempio Swagger Codegen) | Integrato tramite il compilatore Protobuf (protoc) |
| Supporto browser | Universale (accesso diretto al client Web) | Limitato (richiede la traduzione proxy grpc-web) |
5. Quando scegliere gRPC rispetto a REST?
Nonostante gli enormi vantaggi prestazionali di gRPC, non è una soluzione miracolosa. La scelta del protocollo dipende dal contesto all’interno dell’architettura del sistema:
- Utilizza gRPC per la mesh di microservizi interni: per comunicazioni da servizio a servizio a volume elevato, la bassa latenza, la compattezza binaria e le funzionalità di streaming di gRPC lo rendono la scelta migliore. Riduce il sovraccarico della CPU interna del server e accelera i tempi di risposta complessivi.
- Utilizza REST per API Edge/pubbliche: poiché i browser Web non possono effettuare chiamate gRPC in modo nativo senza livelli di traduzione, REST rimane la scelta migliore per le API pubbliche, le integrazioni con webhook di terze parti e la comunicazione diretta dai browser client al perimetro della tua architettura.
Combinando le due cose, ovvero esponendo REST a livello di gateway API e sfruttando gRPC per la comunicazione interna dei microservizi, gli architetti possono creare sistemi altamente interoperabili e incredibilmente veloci.