Por qué los microservicios modernos prefieren gRPC a REST

Por qué los microservicios modernos prefieren gRPC a REST

En una arquitectura monolítica, los componentes se comunican mediante llamadas a métodos en memoria, que son instantáneas y altamente confiables. Sin embargo, al pasar a una arquitectura de microservicios, estos componentes están separados por los límites de la red. La comunicación se convierte en una llamada de red fuera de proceso (Comunicación entre procesos o IPC).

Durante años, REST (Transferencia de estado representacional) sobre HTTP/1.1 con cargas útiles JSON ha sido el estándar predeterminado para crear API web. Si bien REST es excelente para servicios web de cara al público y para la interacción de cliente a servidor, introduce importantes cuellos de botella cuando se utiliza para comunicaciones internas de servicio a servicio de alta frecuencia y baja latencia.

Esta es la razón por la que los microservicios modernos están cambiando rápidamente hacia gRPC (llamada a procedimiento remoto de Google). En este artículo, analizaremos las limitaciones de REST, analizaremos los pilares arquitectónicos de gRPC y recorreremos una implementación completa de un servicio gRPC en Java.


1. Los cuellos de botella de REST en microservicios

REST ha impulsado la web durante más de dos décadas. Sin embargo, sus tecnologías subyacentes no están optimizadas para arquitecturas distribuidas internas:

A. La sobrecarga del JSON de texto sin formato

JSON es legible por humanos, lo que facilita la depuración, pero es extremadamente ineficiente para la comunicación de máquina a máquina:

  • Costo de serialización/deserialización: analizar tokens de cadenas de texto y convertirlos en objetos consume ciclos de CPU sustanciales.
  • Tamaño de carga útil grande: las claves JSON se repiten en cada solicitud (por ejemplo, {"transactionId": "123", "amount": 99.99}). Para los sistemas que procesan millones de solicitudes por segundo, estos metadatos redundantes desperdician un inmenso ancho de banda de la red.

B. Limitaciones de conexión HTTP/1.1

REST normalmente se ejecuta en HTTP/1.1, lo que presenta varias ineficiencias estructurales:

  • Bloqueo de cabecera de línea (HoL): en una única conexión TCP, un cliente debe esperar la respuesta de la solicitud actual antes de enviar la siguiente.
  • Agotamiento de la agrupación de conexiones: para manejar solicitudes simultáneas, los clientes deben abrir varias conexiones TCP. Esto da como resultado una importante sobrecarga de recursos del sistema operativo, agotamiento de sockets y penalizaciones de rendimiento de inicio lento a medida que las conexiones se abren y cierran continuamente.

C. Contratos de API débiles

Las API REST carecen de un contrato integrado en tiempo de compilación. Si bien OpenAPI/Swagger ayuda a documentar las API, están separadas del código base. Es fácil para un desarrollador backend cambiar el nombre de un campo JSON y interrumpir accidentalmente los servicios posteriores sin advertencias en tiempo de compilación.


2. Los pilares arquitectónicos de gRPC

Introducido por Google en 2015, gRPC es un marco RPC de código abierto y alto rendimiento diseñado específicamente para aplicaciones nativas de la nube. Resuelve los cuellos de botella de REST utilizando tres tecnologías clave:

Los 3 pilares arquitectónicos de gRPC: búfer de protocolo, transporte HTTP/2 y generación de código

A. Búfers de protocolo (Protobuf)

En lugar de JSON, gRPC utiliza Búferes de protocolo como lenguaje de definición de interfaz (IDL) y formato de serialización de mensajes.

  • Protobuf es un mecanismo de serialización binaria altamente optimizado.
  • Los campos se serializan como pequeñas etiquetas numéricas en lugar de nombres de texto.
  • Las cargas útiles son hasta 70-80 % más pequeñas que JSON y la serialización es hasta 10 veces más rápida, lo que reduce significativamente el uso de CPU y ancho de banda.

B. Multiplexación HTTP/2

gRPC utiliza HTTP/2 como protocolo de transporte, lo que aporta varias optimizaciones de rendimiento:

  • Multiplexación verdadera: Se pueden entrelazar múltiples solicitudes y respuestas simultáneamente a través de una única conexión TCP de larga duración, eliminando por completo el bloqueo de cabecera de línea.
  • Compresión de encabezados (HPACK): los encabezados HTTP se comprimen, lo que reduce aún más el tamaño de la solicitud.
  • Soporte de transmisión: HTTP/2 admite de forma nativa transmisión unidireccional de cliente, transmisión de servidor y transmisión bidireccional completa.

C. Generación de código y contratos estrictos

Al definir la estructura API en un archivo .proto, gRPC genera stubs de cliente y clases base de servidor en docenas de lenguajes (Java, Go, Python, C++, etc.). El contrato es seguro en tiempo de compilación; Si el servidor actualiza su interfaz, las compilaciones del cliente fallan inmediatamente si no coinciden con el nuevo contrato.


3. Implementación de Java: creación de un servicio gRPC

Construyamos una aplicación Java gRPC práctica. Implementaremos un Servicio de procesamiento de pagos de alto rendimiento donde un cliente envía una solicitud de pago y recibe una confirmación de la transacción.

Paso 1: Definir el contrato API (payment.proto)

Comenzamos definiendo nuestra interfaz de servicio y estructuras de mensajes en un archivo de búfer de protocolo.

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

Paso 2: implementar la lógica del lado del servidor

Después de compilar el archivo .proto (generalmente manejado automáticamente por los complementos Maven o Gradle gRPC), la herramienta genera la clase PaymentServiceImplBase. Ampliamos esta clase para implementar la lógica empresarial.

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

Paso 3: Iniciar el servidor gRPC

A continuación, creamos una clase de servidor para iniciar el servidor gRPC en un puerto específico y registrar la implementación de nuestro servicio.

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

Paso 4: Implementar el cliente (stubs de bloqueo y asíncronos)

Un beneficio importante de gRPC es que genera dos tipos de resguardos para el cliente:

  1. BlockingStub: ejecución de bloqueo síncrona (similar a las llamadas REST estándar).
  2. Stub: ejecución asincrónica y sin bloqueo mediante observadores de devolución de llamada.
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. Comparación arquitectónica: gRPC versus REST

La siguiente tabla describe las características centrales de ambos paradigmas:

Característica arquitectónica REST (Transferencia Estatal Representativa) gRPC (llamada a procedimiento remoto de Google)
Protocolo HTTP/1.1 (Estándar), HTTP/2 (Opcional) HTTP/2 (requisito estricto)
Formato de carga útil Texto sin formato JSON, XML, HTML Búfers de protocolo binario (Protobuf)
Paradigma API Recursos (rutas URI + GET/POST/PUT/DELETE) Procedimientos Remotos (Llamadas a Métodos/Funciones)
Calidad del contrato Suelto (OpenAPI/Swagger es opcional/externo) Estricto (definido en archivos .proto durante la compilación)
Multiplexación No (bloqueo de encabezado de línea en HTTP/1.1) Sí (varias secuencias en una única conexión TCP)
Tipos de transmisión Solo eventos unidireccionales enviados por el servidor (SSE) Cliente, servidor y streaming bidireccional
Generación de código Herramientas externas necesarias (por ejemplo, Swagger Codegen) Integrado a través del compilador Protobuf (protoc)
Soporte del navegador Universal (Acceso directo al cliente web) Limitado (Requiere traducción proxy grpc-web)

5. ¿Cuándo elegir gRPC en lugar de REST?

A pesar de las enormes ventajas de rendimiento de gRPC, no es una solución milagrosa. La elección del protocolo depende del contexto dentro de la arquitectura de su sistema:

  • Utilice gRPC para malla de microservicios interna: para comunicaciones de servicio a servicio de gran volumen, la baja latencia, la compacidad binaria y las capacidades de transmisión de gRPC lo convierten en la opción superior. Reduce la sobrecarga de la CPU del servidor interno y acelera los tiempos de respuesta generales.
  • Use REST para API públicas/de borde: debido a que los navegadores web no pueden realizar llamadas gRPC de forma nativa sin capas de traducción, REST sigue siendo la mejor opción para API públicas, integraciones con webhooks de terceros y comunicación directa desde los navegadores del cliente hasta el borde de su arquitectura.

Al combinar los dos (exponer REST en el nivel de API Gateway y aprovechar gRPC para la comunicación interna de microservicios), los arquitectos pueden crear sistemas que sean altamente interoperables y ultrarrápidos.


Explore más conocimientos sobre desarrollo de software e ingeniería backend en el Blog de Ghaznix →