TechLead
Leccion 8 de 30
7 min de lectura
Diseño de Sistemas

Colas de Mensajes y Streaming de Eventos

Domina las colas de mensajes y el streaming de eventos con comparaciones de Kafka, RabbitMQ y SQS, garantías de entrega, colas de mensajes muertos y patrones arquitectónicos

Por Qué Importan las Colas de Mensajes

En un sistema distribuido, los servicios necesitan comunicarse entre sí. El enfoque más simple son las llamadas HTTP síncronas: el Servicio A envía una solicitud al Servicio B y espera una respuesta. Pero esto crea acoplamiento fuerte — si el Servicio B es lento o está caído, el Servicio A se bloquea o falla.

Las colas de mensajes desacoplan servicios introduciendo un intermediario. En lugar de llamar al Servicio B directamente, el Servicio A publica un mensaje en una cola. El Servicio B consume mensajes de la cola a su propio ritmo. Este simple cambio proporciona enormes beneficios para la fiabilidad, escalabilidad y resiliencia del sistema.

Beneficios de las Colas de Mensajes

  • Desacoplamiento: Los productores y consumidores son independientes. Pueden desplegarse, escalarse y actualizarse por separado.
  • Buffer: Las colas absorben picos de tráfico. Si los consumidores son lentos, los mensajes se acumulan en la cola en lugar de abrumar al servicio.
  • Resiliencia: Si un consumidor se cae, los mensajes persisten en la cola. Cuando el consumidor reinicia, retoma donde lo dejó.
  • Escalabilidad: Agrega más consumidores para procesar mensajes en paralelo. La cola actúa como un mecanismo natural de distribución de trabajo.
  • Procesamiento asíncrono: Mueve tareas que consumen tiempo (envío de emails, procesamiento de imágenes, generación de reportes) fuera del ciclo solicitud-respuesta.

Patrones de Mensajería

1. Punto a Punto (Cola)

Un mensaje se envía a una cola y es consumido por exactamente un consumidor. Si múltiples consumidores están escuchando, cada mensaje se entrega a solo uno de ellos. Este es el patrón clásico de cola de trabajo usado para distribuir tareas entre workers.

// Point-to-Point Architecture
//
// Producer ---> [ Queue ] ---> Consumer 1
//                         ---> Consumer 2  (each message goes to only ONE consumer)
//                         ---> Consumer 3
//
// Example: Order processing
// [Order Service] -> [order-processing-queue] -> [Payment Worker 1]
//                                             -> [Payment Worker 2]
// Each order is processed by exactly one worker

2. Publicar/Suscribir (Tema)

Un mensaje se publica en un tema y se entrega a todos los suscriptores. Cada suscriptor recibe una copia de cada mensaje. Esto se usa cuando múltiples servicios necesitan reaccionar al mismo evento.

// Publish/Subscribe Architecture
//
// Producer ---> [ Topic ] ---> Subscriber 1 (gets ALL messages)
//                         ---> Subscriber 2 (gets ALL messages)
//
// Example: User signup event
// [Auth Service] -> [user-signup topic] -> [Email Service]     (sends welcome email)
//                                       -> [Analytics Service] (tracks signup)
// ALL services receive the signup event

3. Grupos de Consumidores (Patrón Kafka)

Kafka combina ambos patrones con grupos de consumidores. Un tema se divide en particiones. Dentro de un grupo de consumidores, cada partición es consumida por exactamente un consumidor (punto a punto dentro del grupo). Pero múltiples grupos de consumidores pueden suscribirse al mismo tema (pub/sub entre grupos).

Kafka, RabbitMQ y SQS Comparados

Estos son los tres sistemas de mensajería más populares, cada uno diseñado para diferentes casos de uso.

Comparación de Sistemas de Mensajería

Característica Apache Kafka RabbitMQ AWS SQS
ModeloLog distribuidoBroker de mensajesCola gestionada
Retención de mensajesConfigurable (días/semanas)Hasta ser consumidoHasta 14 días
RendimientoMillones/segDecenas de miles/segCasi ilimitado
OrdenamientoPor particiónPor colaSolo colas FIFO
ReplayeoSí (buscar por offset)NoNo
OperacionesComplejo (ZK/KRaft)ModeradoCero (gestionado)
ProtocoloBinario personalizadoAMQP, MQTT, STOMPHTTP/HTTPS
Mejor paraStreaming de eventos, logs, alto volumenColas de tareas, enrutamiento, RPCCola simple, serverless

Ordenamiento de Mensajes y Garantías de Entrega

Entender las garantías de entrega es crítico para construir sistemas distribuidos correctos. Hay tres niveles:

Entrega A Lo Más Una Vez

Los mensajes pueden perderse pero nunca se entregan dos veces. El productor envía un mensaje y no espera confirmación, o el consumidor confirma antes de procesar. Si algo falla, el mensaje se pierde. Esta es la opción más rápida pero solo es adecuada cuando la pérdida de mensajes es aceptable (ej. métricas, logging).

Entrega Al Menos Una Vez

Los mensajes se garantiza que se entregan al menos una vez pero pueden entregarse múltiples veces. El consumidor confirma solo después de procesar exitosamente el mensaje. Si la confirmación se pierde, el mensaje será re-entregado. Esta es la opción más común, pero los consumidores deben ser idempotentes — procesar el mismo mensaje dos veces debe producir el mismo resultado.

// At-least-once delivery with idempotent consumer
class OrderProcessor {
  private processedIds: Set<string> = new Set();
  private db: Database;

  async processMessage(message: OrderMessage): Promise<void> {
    // Idempotency check - have we already processed this?
    const existing = await this.db.query(
      'SELECT id FROM processed_orders WHERE order_id = $1',
      [message.orderId]
    );

    if (existing.rows.length > 0) {
      console.log(`Order ${message.orderId} already processed, skipping`);
      return; // Idempotent - safely skip duplicate
    }

    // Process the order
    await this.db.transaction(async (tx) => {
      await tx.query('INSERT INTO orders VALUES ($1, $2, $3)',
        [message.orderId, message.userId, message.amount]);

      // Record that we processed this message
      await tx.query('INSERT INTO processed_orders (order_id) VALUES ($1)',
        [message.orderId]);
    });
  }
}

Entrega Exactamente Una Vez

Cada mensaje se entrega exactamente una vez — sin pérdida, sin duplicados. Este es el santo grial de la mensajería pero es extremadamente difícil de lograr en sistemas distribuidos. Kafka soporta semántica exactamente-una-vez dentro del ecosistema Kafka usando productores idempotentes y consumidores transaccionales, pero la verdadera entrega exactamente-una-vez de extremo a extremo requiere un diseño cuidadoso a lo largo de todo el pipeline.

Colas de Mensajes Muertos (DLQ)

Una cola de mensajes muertos es una cola especial donde se envían los mensajes que no pueden procesarse exitosamente. En lugar de reintentar indefinidamente o descartar mensajes fallidos, los enrutas a una DLQ para inspección posterior e intervención manual.

// Dead letter queue pattern
class MessageConsumer {
  private mainQueue: Queue;
  private deadLetterQueue: Queue;
  private maxRetries: number = 3;

  async consumeMessage(message: Message): Promise<void> {
    const retryCount = message.headers['x-retry-count'] || 0;

    try {
      await this.processMessage(message);
      await this.mainQueue.ack(message);
    } catch (error) {
      if (retryCount >= this.maxRetries) {
        // Max retries exceeded - send to dead letter queue
        console.error(`Message ${message.id} failed after ${this.maxRetries} retries`);
        await this.deadLetterQueue.publish({
          ...message,
          headers: {
            ...message.headers,
            'x-original-error': error.message,
            'x-failed-at': new Date().toISOString(),
          },
        });
        await this.mainQueue.ack(message); // Remove from main queue
      } else {
        // Retry with exponential backoff
        const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
        await this.mainQueue.nack(message, {
          delay,
          headers: { 'x-retry-count': retryCount + 1 },
        });
      }
    }
  }
}

Usa Colas de Mensajes Cuando:

  • Procesamiento asíncrono: Tareas que no necesitan una respuesta inmediata: enviar emails, generar PDFs, procesar imágenes, sincronizar datos a analíticas.
  • Nivelación de carga: Tienes tráfico explosivo (ej. ventas flash) y necesitas suavizar el procesamiento a lo largo del tiempo.
  • Arquitectura dirigida por eventos: Múltiples servicios necesitan reaccionar al mismo evento (registro de usuario, pedido realizado, pago recibido).
  • Desacoplamiento de servicios: Quieres que los servicios evolucionen independientemente sin acoplamiento fuerte via llamadas API directas.
  • Entrega confiable: Necesitas procesamiento garantizado de tareas críticas incluso si los servicios se caen temporalmente.

NO Uses Colas de Mensajes Cuando:

  • Necesitas respuestas síncronas: Si el cliente necesita una respuesta inmediata (ej. "¿Este nombre de usuario está disponible?"), una llamada API directa es más simple y rápida.
  • Solicitud-respuesta simple: Para operaciones CRUD básicas dentro de un monolito o entre dos servicios fuertemente acoplados, una llamada directa está bien.
  • Sistemas de baja complejidad: Agregar una cola de mensajes a un sistema que no la necesita introduce sobrecarga operativa (monitoreo, alertas, planificación de capacidad) sin beneficio suficiente.

Mejores Prácticas Arquitectónicas

  • Siempre diseña consumidores idempotentes. En sistemas distribuidos, los mensajes duplicados son inevitables. Tus consumidores deben manejarlos con gracia.
  • Configura colas de mensajes muertos. No dejes que mensajes envenenados bloqueen tu pipeline de procesamiento. Enrútalos a una DLQ para investigación.
  • Monitorea la profundidad de la cola. Una profundidad de cola creciente significa que los consumidores no están manteniendo el ritmo. Configura alertas para umbrales de profundidad de cola.
  • Usa contrapresión. Si los consumidores están abrumados, ralentiza o pausa a los productores en lugar de dejar que la cola crezca sin límite.
  • Elige la herramienta correcta. Usa Kafka para streaming de eventos de alto rendimiento, RabbitMQ para enrutamiento complejo y colas de tareas, SQS para integración serverless simple.

Continuar Aprendiendo