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:
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:
- BlockingStub: execução síncrona e bloqueadora (semelhante às chamadas REST padrão).
- 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 →