Comunicación entre microservicios: REST y gRPC

Video
25 min~9 min lectura

Reproductor de video

Comunicación entre microservicios: REST y gRPC

En una arquitectura de microservicios, los componentes individuales no existen en el vacío. Su valor y funcionalidad emergen de su capacidad para comunicarse entre sí de manera eficiente, confiable y escalable. Esta lección se adentra en los dos paradigmas de comunicación más prevalentes para este fin: REST (Representational State Transfer) y gRPC (Google Remote Procedure Call). Comprender sus fortalezas, debilidades y casos de uso óptimos es fundamental para diseñar sistemas distribuidos robustos con Go. Mientras que REST, basado en HTTP/1.1 y JSON, es ubicuo y fácil de inspeccionar, gRPC, construido sobre HTTP/2 y Protocol Buffers, ofrece un rendimiento superior y semántica más estricta para comunicaciones servicio-a-servicio. Aquí aprenderás a implementar ambos, a decidir cuándo usar cada uno y a evitar las trampas comunes en la comunicación distribuida.

Concepto Clave: REST vs. gRPC - El Lenguaje de los Servicios

Imagina que necesitas coordinar equipos de construcción en dos continentes. REST sería como intercambiar cartas formales (solicitudes HTTP) escritas en un lenguaje humano común como inglés (JSON). Cada carta especifica claramente el destinatario (URL), la acción deseada (verbo HTTP: GET, POST, PUT, DELETE) y los detalles en un formato que ambos pueden leer e interpretar, aunque con cierto margen para la ambigüedad. Es flexible y casi cualquier equipo puede participar, pero el proceso de escribir, enviar, leer y responder cada carta es relativamente lento y verboso.

Por otro lado, gRPC es como tener una línea telefónica digital dedicada y de alta velocidad (HTTP/2) entre los equipos, donde ambos usan un manual de procedimientos predefinido y binario extremadamente eficiente (Protocol Buffers). En lugar de enviar cartas descriptivas, un equipo puede simplemente "llamar a una función" (procedimiento remoto) del otro equipo, pasando parámetros binarios compactos. El contrato (el archivo .proto) define exactamente qué funciones existen y qué datos se pueden pasar, eliminando ambigüedades. Es mucho más rápido y eficiente para conversaciones frecuentes y estructuradas, pero requiere que ambos lados entiendan el mismo protocolo específico desde el principio.

Tip: No se trata de que uno sea "mejor" que el otro, sino de cuál es la herramienta correcta para el trabajo. Usa REST para APIs públicas, integración con navegadores o sistemas heterogéneos. Elige gRPC para comunicación interna entre microservicios bajo tu control, especialmente donde la latencia, el throughput o la eficiencia de los datos son críticos.

Cómo funciona en la práctica: Un escenario de e-commerce

Consideremos un sistema de e-commerce descompuesto en microservicios: Servicio de Pedidos, Servicio de Catálogo y Servicio de Usuarios. Cuando un usuario finaliza una compra, el Servicio de Pedidos debe crear un nuevo pedido. Para ello, necesita: 1) Verificar el inventario y obtener el precio actual de cada producto (Catálogo), y 2) Validar y obtener la dirección de envío del usuario (Usuarios).

Con un enfoque REST, el Servicio de Pedidos haría solicitudes HTTP independientes. Primero, un POST /api/orders con un cuerpo JSON que contiene los IDs de producto. Durante el procesamiento, internamente haría una llamada GET /api/catalog/products/{id} para cada producto y otra llamada GET /api/users/{userId}/shipping-address. Cada una de estas llamadas implica serializar/deserializar JSON, manejar cabeceras HTTP y potencialmente abrir nuevas conexiones TCP. Es un patrón claro y fácil de depurar con herramientas como curl o Postman, pero puede sufrir de "latencia de la red" debido a las múltiples idas y vueltas.

Con un enfoque gRPC, los contratos se definen primero en archivos .proto. El Servicio de Catálogo podría definir un procedimiento remoto GetProductBatch que acepte una lista de IDs y devuelva una lista de detalles de productos en una sola llamada. El Servicio de Pedidos, generando código cliente a partir del archivo .proto, simplemente llamaría a catalogClient.GetProductBatch(ctx, &productIDs) como si fuera una función local. HTTP/2 permite multiplexar varias solicitudes en una sola conexión persistente, y los Protocol Buffers codifican los datos en un formato binario mucho más pequeño que JSON. Esto reduce drásticamente la latencia y el uso de ancho de banda en comunicaciones intensivas.

Código en acción: Implementando un cliente y servidor gRPC en Go

Veamos una implementación concreta. Primero, definimos el contrato en un archivo Protocol Buffer. Luego, implementaremos un servidor gRPC simple en Go y un cliente que lo consuma.

Paso 1: Definición del contrato (archivo .proto)

syntax = "proto3";

package catalog;

option go_package = "/catalogpb";

service ProductService {
  rpc GetProduct (GetProductRequest) returns (Product);
  rpc GetProductBatch (GetProductBatchRequest) returns (GetProductBatchResponse);
}

message GetProductRequest {
  string product_id = 1;
}

message GetProductBatchRequest {
  repeated string product_ids = 1; // 'repeated' significa una lista/array
}

message GetProductBatchResponse {
  repeated Product products = 1;
}

message Product {
  string id = 1;
  string name = 2;
  string description = 3;
  double price = 4;
  int32 stock = 5;
}

Paso 2: Generación de código Go y servidor gRPC

Tras compilar el archivo .proto con el compilador protoc, generamos las interfaces y estructuras Go. Aquí está la implementación del servidor:

package main

import (
    "context"
    "log"
    "net"
    "sync"

    "google.golang.org/grpc"
    "tu-proyecto/catalogpb" // Importa el código generado
)

type server struct {
    catalogpb.UnimplementedProductServiceServer
    mu       sync.RWMutex
    products map[string]*catalogpb.Product
}

func (s *server) GetProduct(ctx context.Context, req *catalogpb.GetProductRequest) (*catalogpb.Product, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    product, exists := s.products[req.ProductId]
    if !exists {
        return nil, grpc.Errorf(codes.NotFound, "producto no encontrado")
    }
    return product, nil
}

func (s *server) GetProductBatch(ctx context.Context, req *catalogpb.GetProductBatchRequest) (*catalogpb.GetProductBatchResponse, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    response := &catalogpb.GetProductBatchResponse{}
    for _, id := range req.ProductIds {
        if product, exists := s.products[id]; exists {
            response.Products = append(response.Products, product)
        }
        // Nota: Podrías decidir devolver un error si algún ID no existe.
    }
    return response, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("falló al escuchar: %v", err)
    }
    s := grpc.NewServer()
    catalogpb.RegisterProductServiceServer(s, &server{
        products: map[string]*catalogpb.Product{
            "P001": {Id: "P001", Name: "Laptop", Description: "Laptop de 16 pulgadas", Price: 1299.99, Stock: 50},
            "P002": {Id: "P002", Name: "Mouse", Description: "Mouse inalámbrico", Price: 29.99, Stock: 200},
        },
    })
    log.Printf("Servidor gRPC escuchando en %s", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("falló al servir: %v", err)
    }
}

Paso 3: Cliente gRPC en el Servicio de Pedidos

package main

import (
    "context"
    "log"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "tu-proyecto/catalogpb"
)

func main() {
    // 1. Establecer conexión
    conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("no se pudo conectar: %v", err)
    }
    defer conn.Close()

    // 2. Crear cliente
    client := catalogpb.NewProductServiceClient(conn)

    // 3. Preparar contexto con timeout
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // 4. Llamada al método unario GetProduct
    product, err := client.GetProduct(ctx, &catalogpb.GetProductRequest{ProductId: "P001"})
    if err != nil {
        log.Fatalf("error en GetProduct: %v", err)
    }
    log.Printf("Producto único: %s - Precio: $%.2f", product.Name, product.Price)

    // 5. Llamada al método de lote GetProductBatch
    batchResp, err := client.GetProductBatch(ctx, &catalogpb.GetProductBatchRequest{
        ProductIds: []string{"P001", "P002"},
    })
    if err != nil {
        log.Fatalf("error en GetProductBatch: %v", err)
    }
    log.Printf("Recibido lote de %d productos", len(batchResp.Products))
    for _, p := range batchResp.Products {
        log.Printf("  -> %s (Stock: %d)", p.Name, p.Stock)
    }
}

Errores comunes y cómo evitarlos

La comunicación entre servicios introduce una nueva capa de complejidad y puntos de fallo. Estos son algunos errores frecuentes:

1. Falta de timeouts y circuit breakers: Realizar llamadas a servicios externos sin timeouts puede llevar a que un hilo (o goroutine) quede bloqueado indefinidamente si el servicio remoto está caído o lento. Esto puede agotar rápidamente los recursos de tu aplicación. Solución: Siempre usa contextos con timeout o deadline en cada llamada, tanto en REST (con http.Client.Timeout) como en gRPC (con context.WithTimeout). Implementa patrones de resiliencia como Circuit Breaker (usando librerías como go-breaker o hystrix-go) para evitar "golpear" repetidamente a un servicio que está fallando.

2. Acoplamiento temporal y fallos en cascada: Si el Servicio A llama sincrónicamente al B, y B llama a C, una falla o lentitud en C puede propagarse hacia atrás, colapsando toda la cadena. Solución: Diseña para la resiliencia. Usa timeouts agresivos, reintentos con backoff exponencial (solo para errores idempotentes) y colas asíncronas (con algo como RabbitMQ o Kafka) para operaciones que no requieren respuesta inmediata. Considera el patrón Bulkhead para aislar recursos.

3. Manejo inadecuado de versionado en APIs: Cambiar un campo en un mensaje Protobuf o en un esquema JSON de respuesta sin una estrategia clara puede romper a los clientes existentes. Solución: Para REST, usa versionado en la URL (e.g., /api/v1/resource) o en las cabeceras. Para gRPC, sigue las reglas de evolución de Protobuf (nuevos campos son opcionales, no reutilizar o cambiar el tipo de números de campo). Planifica la compatibilidad hacia atrás y comunica los cambios.

4. Ignorar la serialización/deserialización: Asumir que el proceso de convertir structs a JSON/Protobuf y viceversa es trivial puede llevar a problemas de rendimiento (usar reflección de manera ineficiente) o errores (campos omitidos). Solución: En Go, para JSON, usa etiquetas struct (`json:"nombre_campo"`) y considera librerías como json-iterator/go para alto rendimiento. Para gRPC, deja que el código generado se encargue de ello, pero sé consciente del costo de copiar datos entre estructuras.

Checklist de dominio

Antes de considerar dominada esta lección, asegúrate de poder verificar los siguientes puntos:

  • Puedo explicar la diferencia fundamental entre el modelo de comunicación de REST (recursos/verbos HTTP) y gRPC (procedimientos remotos/contratos).
  • He escrito y compilado un archivo .proto simple que define un servicio con al menos dos métodos RPC y sus mensajes.
  • He implementado un servidor gRPC en Go que cumple con la interfaz generada a partir de un archivo .proto.
  • He construido un cliente gRPC en Go que se conecta a un servidor, maneja timeouts y procesa las respuestas correctamente.
  • Puedo listar al menos tres ventajas de gRPC sobre REST para comunicación interna entre microservicios (velocidad, eficiencia binaria, HTTP/2, streaming nativo).
  • Puedo listar al menos dos ventajas de REST sobre gRPC (universalidad, facilidad de debug, compatibilidad con navegadores).
  • Sé cómo configurar un timeout tanto para una petición HTTP REST (usando http.Client) como para una llamada gRPC (usando context.Context).
  • Entiendo los riesgos de las llamadas sincrónicas en cascada y conozco al menos un patrón (ej., colas asíncronas, circuit breaker) para mitigarlos.
De lección a portfolio

Convertí esta lección en una habilidad visible para entrevistas.

Guardá el curso, completá los ejercicios y conectá esta habilidad con una ruta de empleo, data, IA, programación o marketing.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión