Por que os microsserviços modernos preferem o gRPC ao REST

Por que os microsserviços modernos preferem o gRPC ao REST

Em uma arquitetura monolítica, os componentes se comunicam por meio de chamadas de métodos na memória, que são instantâneas e altamente confiáveis. Ao migrar para uma arquitetura de microsserviços, entretanto, esses componentes são separados por limites de rede. A comunicação torna-se uma chamada de rede fora de processo (Comunicação entre processos ou IPC).

Durante anos, REST (Representational State Transfer) sobre HTTP/1.1 com cargas JSON tem sido o padrão padrão para a criação de APIs da Web. Embora REST seja excelente para serviços da Web voltados ao público e interação cliente-servidor, ele introduz gargalos significativos quando usado para comunicação interna de serviço a serviço de alta frequência e baixa latência.

É por isso que os microsserviços modernos estão migrando rapidamente para gRPC (Google Remote Procedure Call). Neste artigo, analisaremos as limitações do REST, dissecaremos os pilares arquitetônicos do gRPC e percorreremos uma implementação completa de um serviço gRPC em Java.


1. Os gargalos do REST em microsserviços

REST impulsiona a web há mais de duas décadas. No entanto, as suas tecnologias subjacentes não são otimizadas para arquiteturas distribuídas internas:

A. A sobrecarga do JSON de texto simples

JSON é legível por humanos, o que facilita a depuração, mas é extremamente ineficiente para comunicação máquina a máquina:

  • Custo de serialização/desserialização: analisar tokens de string de texto e convertê-los em objetos consome ciclos de CPU substanciais.
  • Grande tamanho de carga: as chaves JSON são repetidas em cada solicitação (por exemplo, {"transactionId": "123", "amount": 99.99}). Para sistemas que processam milhões de solicitações por segundo, esses metadados redundantes desperdiçam uma imensa largura de banda da rede.

B. Limitações de conexão HTTP/1.1

REST normalmente é executado em HTTP/1.1, que apresenta diversas ineficiências estruturais:

  • Bloqueio Head-of-Line (HoL): Em uma única conexão TCP, um cliente deve aguardar a resposta da solicitação atual antes de enviar a próxima.
  • Esgotamento do pool de conexões: para lidar com solicitações simultâneas, os clientes devem abrir diversas conexões TCP. Isso resulta em sobrecarga significativa de recursos do sistema operacional, esgotamento de soquetes e penalidades de desempenho de inicialização lenta, à medida que as conexões são abertas e fechadas continuamente.

C. Contratos de API fracos

As APIs REST não possuem um contrato integrado em tempo de compilação. Embora o OpenAPI/Swagger ajude a documentar APIs, eles são separados da base de código. É fácil para um desenvolvedor de back-end alterar o nome de um campo JSON e interromper acidentalmente os serviços downstream sem avisos em tempo de compilação.


2. Os pilares arquitetônicos do gRPC

Introduzido pelo Google em 2015, o gRPC é uma estrutura RPC de código aberto e de alto desempenho projetada especificamente para aplicativos nativos da nuvem. Ele resolve os gargalos do REST usando três tecnologias principais:

Os três pilares arquitetônicos do gRPC: buffers de protocolo, transporte HTTP/2 e geração de código

A. Buffers de protocolo (Protobuf)

Em vez de JSON, o gRPC usa Buffers de protocolo como sua linguagem de definição de interface (IDL) e formato de serialização de mensagens.

  • Protobuf é um mecanismo de serialização binária altamente otimizado.
  • Os campos são serializados como pequenas tags numéricas em vez de nomes de texto.
  • As cargas são até 70-80% menores que o JSON, e a serialização é até 10x mais rápida, reduzindo significativamente o uso de CPU e largura de banda.

B. Multiplexação HTTP/2

O gRPC utiliza HTTP/2 como protocolo de transporte, trazendo diversas otimizações de desempenho:

  • Multiplexação Verdadeira: Múltiplas solicitações e respostas podem ser intercaladas simultaneamente em uma única conexão TCP de longa duração, eliminando completamente o bloqueio Head-of-Line.
  • Compressão de cabeçalho (HPACK): os cabeçalhos HTTP são compactados, reduzindo ainda mais o tamanho da solicitação.
  • Suporte a streaming: HTTP/2 suporta nativamente streaming de cliente unidirecional, streaming de servidor e streaming bidirecional completo.

C. Geração de código e contratos rígidos

Ao definir a estrutura da API em um arquivo .proto, o gRPC gera stubs de cliente e classes base de servidor em dezenas de linguagens (Java, Go, Python, C++, etc.). O contrato é seguro em tempo de compilação; se o servidor atualizar sua interface, as compilações do cliente falharão imediatamente se não corresponderem ao novo contrato.


3. Implementação Java: Construindo um Serviço gRPC

Vamos construir um aplicativo Java gRPC prático. Implementaremos um Serviço de Processamento de Pagamentos de alto rendimento, onde um cliente envia uma solicitação de pagamento e recebe uma confirmação de transação.

Etapa 1: Definir o contrato de API (payment.proto)

Começamos definindo nossa interface de serviço e estruturas de mensagens em um arquivo 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;
}

Etapa 2: Implementar a lógica do lado do servidor

Depois de compilar o arquivo .proto (geralmente tratado automaticamente pelos plug-ins Maven ou Gradle gRPC), a ferramenta gera a classe PaymentServiceImplBase. Estendemos esta classe para implementar a lógica de negócios.

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();
    }
}

Etapa 3: inicializar o servidor gRPC

A seguir, criamos uma classe de servidor para iniciar o servidor gRPC em uma porta específica e registrar nossa implementação de serviço.

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();
    }
}

Etapa 4: Implementar o cliente (bloqueio e stubs assíncronos)

Um grande benefício do gRPC é que ele gera dois tipos de stubs para o cliente:

  1. BlockingStub: execução síncrona e bloqueadora (semelhante às chamadas REST padrão).
  2. Stub: execução assíncrona e sem bloqueio usando observadores de retorno de chamada.
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. Comparação de arquitetura: gRPC vs. REST

A tabela abaixo descreve as principais características de ambos os paradigmas:

Característica arquitetônica REST (Transferência de Estado Representacional) gRPC (chamada de procedimento remoto do Google)
Protocolo HTTP/1.1 (padrão), HTTP/2 (opcional) HTTP/2 (requisito estrito)
Formato de carga útil JSON de texto simples, XML, HTML Buffers de protocolo binário (Protobuf)
Paradigma de API Recursos (caminhos URI + GET/POST/PUT/DELETE) Procedimentos Remotos (chamadas de métodos/funções)
Qualidade do contrato Loose (OpenAPI/Swagger é opcional/externo) Estrito (definido em arquivos .proto na compilação)
Multiplexação Não (bloqueio de cabeçalho em HTTP/1.1) Sim (múltiplos fluxos em uma única conexão TCP)
Tipos de streaming Somente eventos unidirecionais enviados pelo servidor (SSE) Streaming de cliente, servidor e bidirecional
Geração de código Ferramentas externas necessárias (por exemplo, Swagger Codegen) Integrado via compilador Protobuf (protoc)
Suporte ao navegador Universal (acesso direto do cliente web) Limitado (requer tradução de proxy grpc-web)

5. Quando escolher gRPC em vez de REST?

Apesar das enormes vantagens de desempenho do gRPC, ele não é uma solução mágica. A escolha do protocolo depende do contexto da arquitetura do seu sistema:

  • Use gRPC para malha interna de microsserviços: para comunicação entre serviços de alto volume, a baixa latência, a compactação binária e os recursos de streaming do gRPC o tornam a escolha superior. Reduz a sobrecarga interna da CPU do servidor e acelera os tempos de resposta gerais.
  • Use REST para APIs públicas/de borda: como os navegadores da Web não podem fazer chamadas gRPC nativamente sem camadas de tradução, o REST continua sendo a melhor opção para APIs públicas, integrações com webhooks de terceiros e comunicação direta dos navegadores do cliente até a borda da sua arquitetura.

Ao combinar os dois – expor o REST no nível do API Gateway e aproveitar o gRPC para comunicação interna de microsserviços – os arquitetos podem construir sistemas que são altamente interoperáveis ​​e extremamente rápidos.


Explore mais insights sobre desenvolvimento de software e engenharia de back-end no Blog Ghaznix →