Perché i microservizi moderni preferiscono gRPC rispetto a REST

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:

I 3 pilastri dell'architettura di gRPC: buffer di protocollo, trasporto HTTP/2 e generazione di codice

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:

  1. BlockingStub: esecuzione sincrona e bloccante (simile alle chiamate REST standard).
  2. 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.


Scopri ulteriori approfondimenti sullo sviluppo software e sull’ingegneria backend sul blog di Ghaznix →