SenseiTeoría desde cero0/32

5 · Datos y almacenamientoLección 4 de 5

Colas y streams: mensajes que esperan

Queues and streams: messages that wait

Cómo se deja un trabajo en espera sin perderlo, cómo se reparte entre muchos workers, qué pasa cuando uno se cae a la mitad y por qué un registro de eventos que solo crece deja que muchos sistemas lean lo mismo, cada uno a su ritmo.

⏱ 59 min lo esencial · 74 min todo16 conceptos nuevos2 calculadoras10 ejerciciosDatos y API
Secciones14
  1. 01Trabajo que puede esperar: productor, broker y consumidor
  2. 02Desacoplar: la cola aplana los picos
  3. 03Recibir no es borrar: visibility timeout, ack y DLQ
  4. 04Al menos una vez, a lo más una vez, exactamente una vez
  5. 05El stream de eventos: un registro que solo crece
  6. 06Particiones y grupos de consumidores
  7. 07Lag del consumidor: cuánto vas atrás
  8. 08Colas a fondo: CDC, outbox y cuál elegir extra
  9. 09Errores comunes
  10. 10En la vida real extra
  11. 11Ejercicios
  12. 12Lo que sigue
  13. 13Palabras que vas a oír
  14. 14Chequeo
La pregunta

Cuando algo no tiene que contestarse en el momento (reentrenar, avisar a un vendedor, guardar el log de cada predicción, leer un PDF largo), ¿cómo lo dejo esperando sin perderlo, cómo lo reparto entre muchos workers y qué pasa si uno se cae a la mitad?

Ana publica sus tenis y, en menos de un segundo, la app le sugiere «Tenis». Eso tiene que pasar mientras ella mira la pantalla. Pero alrededor de esa respuesta hay mucho trabajo que nadie espera:

  • Guardar una línea de log con la predicción, para el monitoreo.
  • Avisarle a Ana, horas después, que su publicación tiene 20 vistas.
  • Clasificar de una vez los ≈ 200 millones de fotos viejas del catálogo.
  • Reentrenar el modelo cada mes con las fotos que se etiquetaron.

Si todo eso se hiciera dentro de la request de Ana, ella esperaría minutos. Y si una máquina se apaga a la mitad, el trabajo se perdería sin que nadie lo note.

Esta lección responde tres preguntas sobre ese trabajo que puede esperar: cómo se guarda sin perderlo, cómo se reparte entre muchas máquinas y qué pasa cuando una se cae a la mitad. Las respuestas son las colas y los streams. Son de las piezas que más vas a dibujar en la Ronda.

01

Trabajo que puede esperar: productor, broker y consumidor

Esencial

Work that can wait: producer, broker and consumer

La primera pregunta de diseño de la Ronda (M01) es: ¿alguien espera la respuesta antes de continuar? Si sí, la request es síncrona: el que pide se queda esperando. Si no, casi siempre es más barato y más simple hacerlo después, en segundo plano.

Trabajo¿Alguien espera?Cómo se hace
La categoría de los tenis de AnaSí: ella sigue en el formularioSíncrono, en milisegundos
El log de esa predicciónNoSe escribe aparte, sin sumar latencia
Leer un PDF de 200 páginasSí, pero minutos, no milisegundosAsíncrono, con cola
Clasificar 200 M de fotos viejasNo: nadie las está mirandoUn trabajo por lotes (batch)

En la lección de APIs viste el patrón: la API acepta el trabajo en milisegundos, lo mete en una cola, responde 202 Accepted con un job_id, y un worker (trabajador) lo hace después. Ahí la cola era una caja negra. Ahora la abrimos.

Tres papeles

Una cola de mensajes (message queue) es un servicio que guarda mensajes en fila hasta que alguien los toma, los procesa y confirma que terminó. Alrededor de ella hay tres papeles:

  • El productor (producer): el programa que mete mensajes. En el ejemplo del PDF de la lección de APIs, es la API que recibió el POST /v1/jobs.
  • El consumidor (consumer): el programa que los saca y hace el trabajo. Casi siempre son varios workers iguales, copias del mismo programa.
  • El broker: el servicio que guarda los mensajes en medio. Ejemplos: Amazon SQS (Simple Queue Service, el de AWS, la nube de Amazon), RabbitMQ (un programa libre que instalas en tus máquinas) y Google Pub/Sub (el de la nube de Google).

Un mensaje es un paquete chico de datos, casi siempre un JSON: {"job_id": "8f3a", "pdf": "uploads/8f3a.pdf"}. Lleva la referencia al archivo, no el archivo: el PDF vive en el almacén, y el mensaje solo dice dónde buscarlo. Los brokers limitan el tamaño de cada mensaje: SQS acepta hasta 1 MiB desde agosto de 2025 (antes, 256 KiB), y Kafka 4.2 (Kafka es el registro de eventos que verás más adelante en esta lección) trae por omisión un tope de ≈ 1 MB (message.max.bytes, 1,048,588 bytes). Mover fotos de 10 MB por una cola sería caro y lento.

A ese truco se le llama claim check, como la ficha de un guardarropa: dejas el abrigo en el mostrador y te quedas con una ficha chica. El productor primero sube el archivo al almacén (el object storage de la lección de almacenamiento) y después mete en la cola un mensaje con su dirección. El worker usa esa dirección para bajarlo. Así la cola mueve unos cientos de bytes por trabajo. La analogía se rompe en un punto: en el guardarropa, al entregar la ficha te llevas el abrigo; aquí el archivo se queda en el almacén. Si el mismo mensaje llega dos veces (ya verás que pasa), el worker baja dos veces el mismo archivo y en el almacén no se duplica nada.

Qué gana cada uno

El productor no espera al consumidor: deja el mensaje y sigue. El consumidor trabaja a su ritmo: si está ocupado, el mensaje espera en la fila. Y ninguno de los dos necesita saber dónde vive el otro, cuántos son ni si están prendidos: los dos solo hablan con el broker. Si los consumidores se apagan diez minutos para instalar una versión nueva, los mensajes esperan en el broker hasta que vuelvan, y el productor ni se entera.

Cuando hay varios consumidores leyendo de la misma cola, compiten por los mensajes: cada mensaje se lo lleva uno solo. Para procesar más rápido, se agregan consumidores. El PDF de 200 páginas, a ≈ 1 s por página, tarda ≈ 200 s con un worker. Si cada página es un mensaje y hay 20 workers, tarda ≈ 10 s (M02).

Por qué el broker es un servicio aparte

¿No bastaría con una lista en la memoria del programa? Python trae una: queue.Queue, una fila segura para que varios hilos metan y saquen cosas a la vez. Aquí hay un productor que encola 12 trabajos y 4 consumidores que los reparten:

python
import queue, threading, time

cola = queue.Queue()                    # la fila, en la memoria de este proceso
hechos = []                             # lo que terminaron los consumidores
candado = threading.Lock()              # para que dos hilos no escriban a la vez

def consumidor():
    while True:
        trabajo = cola.get()            # espera hasta que haya un mensaje
        if trabajo is None:             # la señal de «ya no hay más»
            break
        time.sleep(0.1)                 # «procesar»: 0.1 s por trabajo
        with candado:
            hechos.append(trabajo["job_id"])
        cola.task_done()                # avisa a la fila: este ya quedó

inicio = time.perf_counter()
hilos = [threading.Thread(target=consumidor) for _ in range(4)]
for h in hilos:
    h.start()
for n in range(12):                     # el productor: 12 trabajos, sin esperar a nadie
    cola.put({"job_id": n, "pdf": f"uploads/{n}.pdf"})
print("el productor ya terminó; los consumidores siguen")
cola.join()                             # espera a que los 12 tengan su task_done
duracion = time.perf_counter() - inicio
for h in hilos:
    cola.put(None)                      # una señal de salida por consumidor
for h in hilos:
    h.join()

print("hechos:", len(hechos), "· repetidos:", len(hechos) - len(set(hechos)), "· perdidos:", 12 - len(set(hechos)))
print(f"en serie serían 1.2 s; con 4 consumidores, ≈ {round(1.2 / duracion)} veces más rápido")
Salida
el productor ya terminó; los consumidores siguen
hechos: 12 · repetidos: 0 · perdidos: 0
en serie serían 1.2 s; con 4 consumidores, ≈ 4 veces más rápido

Qué consumidor tomó cada trabajo cambia en cada corrida; los totales no. Funciona, pero tiene un límite serio: la fila vive en la RAM de un proceso. Si ese proceso se reinicia, los mensajes que esperaban desaparecen. Y solo la pueden leer los hilos de ese proceso, no los workers de otras máquinas.

Un broker resuelve las dos cosas. Guarda cada mensaje en disco, casi siempre con copias en varias máquinas, antes de decirle al productor «lo tengo». Y lo atiende por la red a cualquier consumidor, esté donde esté. Es la diferencia entre una fila para trabajar dentro de un programa y una fila en la que se puede confiar entre programas.

02

Desacoplar: la cola aplana los picos

Esencial

Decoupling: the queue flattens the peaks

Que el productor no espere al consumidor se llama desacoplar (decoupling): cada lado tiene su propia velocidad. Su efecto más útil se ve con números.

En la lección de unidades calculaste el tráfico de C01: ≈ 10 millones de fotos al día, ≈ 116 por segundo en promedio. Con un factor de pico de 3, la hora pico, a las 20 h, trae ≈ 350 fotos por segundo.

Ahora un supuesto, para el ejemplo: C01 agrega una revisión después de publicar, un segundo modelo que busca fotos prohibidas. Nadie espera su respuesta: si encuentra algo, se baja la publicación unos minutos después. ¿Cuántos workers compras?

Si fuera síncrona, tendrías que cubrir el pico: 350 fotos por segundo. Con workers de 20 fotos por segundo cada uno (un supuesto), son 350 ÷ 20 = 17.5, o sea 18 workers. Con una cola, puedes comprar 10 y dejar que la fila absorba la hora pico:

Una hora pico contra workers para 200 fotos por segundoLa cuenta
Llegan en la hora picoel factor de pico de 3 de la lección de unidades
116 × 3
≈ 350 por segundo
Procesan los workersun supuesto: 10 workers de 20 fotos por segundo
10 × 20
200 por segundo
Lo que crece la fila
350 − 200
150 por segundo
Fila al terminar la hora
150 × 3,600 s
540,000 fotos
Espera de la últimalo que hay delante, entre lo que sale por segundo
540,000 ÷ 200
2,700 s = 45 min
Vaciado, si después llegan 116 por segundo
540,000 ÷ (200 − 116)
≈ 6,400 s ≈ 1.8 h
Resultado
Con 10 workers en vez de 18, ninguna foto se pierde; la peor espera 45 minutos, y antes de la medianoche la fila está vacía.

La espera de la última foto sale de dividir lo que tiene delante entre lo que sale por segundo. Es la ley de Little despejada para el tiempo, W = L ÷ λ, con λ como el ritmo de salida. Es el tiempo en cola de la lección de latencia, ahora en minutos en vez de milisegundos.

La hora pico de C01 con una cola en medio, de las 19 a las 24 horas. Arriba, fotos por segundo: fuera del pico llegan 116 y salen 116. De las 20 a las 21 llegan 350, pero los workers solo procesan 200: la fila crece 150 por segundo. Desde las 21 vuelven a llegar 116 y los workers siguen a 200: la fila se vacía a 84 por segundo. Abajo, las fotos esperando: cero hasta las 20, suben en línea recta hasta 540,000 a las 21 y bajan hasta cero cerca de las 22:47. Con workers para el pico no habría fila, pero se pagarían 75 por ciento más workers todo el día. UNA HORA PICO CON UNA COLA EN MEDIO llegan salen: workers para 200/s Fotos por segundo 350 200 116 +150/s −84/s la fila se vacía crece Fotos esperando en la fila 540,000 a las 21 h la última espera 45 min vacía ≈ 22:47 19 h 20 21 22 23 24 h Con workers para 350/s no habría fila, pero pagarías 75 % más workers todo el día.
La hora pico de C01 contra workers para 200 fotos por segundo. Arriba, lo que llega y lo que sale. Abajo, la fila: crece mientras llega más de lo que sale, y se vacía cuando el tráfico baja.

La cuenta dice la regla: con una cola, la capacidad se compra para el promedio con margen, no para el pico, siempre que el negocio aguante la espera. Si la revisión tuviera que terminar en 5 minutos, 45 no sirven, y habría que comprar más workers o escalarlos con la fila (lo verás en la sección del lag).

Una condición no se negocia: en promedio, los workers tienen que procesar más de lo que llega. Si llegan 116 por segundo todo el día y procesan 100, la fila crece 16 por segundo para siempre: ≈ 1.4 millones de fotos atrasadas al día. Una cola esconde un pico; no esconde que falta capacidad.

Una hora pico contra una colaCalculadora

Los workers procesan siempre lo mismo; lo que llega sube en la hora pico y vuelve a bajar. Arranca con la revisión de fotos de C01: 116 por segundo fuera del pico, 350 en la hora pico y workers para 200.

Lo que crece la fila en el pico, por segundo150
Fotos en la fila al terminar el pico540,000
Espera de la última, en minutoslo que hay delante entre lo que sale por segundo45
Horas para vaciar la fila después del pico«—» = nunca: los workers no procesan ni lo de fuera del pico1.8
Workers de más para cubrir el pico sin colalo que costaría hacerlo síncrono75.0 %

Si lo que procesan los workers es menor que lo que llega fuera del pico, la fila crece para siempre: una cola esconde un pico, no la falta de capacidad.

Prueba una capacidad de 350: la fila nunca crece y la espera es cero, pero pagas 75 % más workers. Prueba 120: al final del pico esperan 828,000 fotos, la fila se vacía a 4 por segundo y tarda más de dos días.

Una cola no es lo mismo que una fila con tope

En la lección de latencia viste otra fila: la del batcher de Triton, que junta fotos durante 2 ms para hacer un batch, y la cola acotada del gateway, que rechaza lo que no cabe. Las dos son filas en la memoria de un proceso, como queue.Queue. Si Triton se reinicia, las fotos que esperaban se pierden, y está bien: quien pidió ya recibió un error y cae a su fallback.

Allá la fila debe ser corta, porque la request de Ana tiene 300 ms de vida, y por eso se rechaza rápido con backpressure. Aquí la fila puede ser larga, porque nadie mira el reloj, y lo que importa es no perder nada. Por eso esta fila vive en un broker.

Una cola de mensajes también tiene tope: la retención. SQS guarda un mensaje hasta 4 días por omisión y hasta 14 como máximo; después lo borra, aunque nadie lo haya procesado. Y una fila que crece sin parar es un aviso: la presión hacia atrás aquí no es un 503, sino una alerta y más workers.

03

Recibir no es borrar: visibility timeout, ack y DLQ

Esencial

Receiving is not deleting: visibility timeout, ack and DLQ

Ahora la tercera pregunta: ¿qué pasa si un worker se cae a la mitad?

Si el broker borrara el mensaje en cuanto lo entrega, el trabajo se perdería con el worker. Por eso no lo borra. Lo esconde.

  1. 1

    Recibir

    El worker pide un mensaje. El broker se lo da y lo marca en vuelo (in flight): sigue en la cola, pero nadie más lo ve.

  2. 2

    Esconderlo un rato

    El mensaje queda invisible durante el visibility timeout (tiempo de invisibilidad): 30 s por omisión en SQS. Es el plazo que tiene el worker para terminar.

  3. 3

    Confirmar

    Si el worker termina, avisa: es la confirmación o ack (de acknowledge, «acusar recibo»). En SQS, el ack es literalmente borrar el mensaje.

  4. 4

    Si no confirma, vuelve

    Si vence el plazo sin ack, porque el worker se cayó, se colgó o tardó de más, el mensaje vuelve a ser visible y otro worker lo toma.

Un mensaje en el tiempo, de 0 a 60 segundos, con un visibility timeout de 30 segundos. A los 0 segundos el worker 1 lo recibe y el mensaje queda en vuelo, invisible para los demás. A los 10 segundos el worker 1 se cae sin mandar el ack. El mensaje sigue escondido hasta los 30 segundos, cuando vence el plazo y reaparece. Lo recibe el worker 2, que lo procesa de los 30 a los 45 segundos y manda el ack. Solo entonces el mensaje se borra. Entre los 10 y los 30 segundos nadie trabajó en él: es el precio de un plazo de 30 segundos. UN MENSAJE, DOS WORKERS, PLAZO DE 30 S El mensaje, en la cola invisible, con el 1 con el 2 borrado 30 s: vence el plazo sin ack y reaparece Worker 1 se cae a los 10 s, sin ack Worker 2 procesa lo recibe otra vez ack ✓ 0 10 20 30 40 50 60 s Del 10 al 30 nadie trabaja en él: es el precio del plazo. Solo el ack borra el mensaje.
Un mensaje en el tiempo. El worker 1 lo recibe y se cae a los 10 s. El mensaje sigue escondido hasta los 30 s, cuando vence su visibility timeout; reaparece, lo toma el worker 2, termina y confirma. Solo entonces se borra.

Con este truco, un worker que se cae no pierde nada: el trabajo vuelve solo. Cada broker lo llama distinto: en Pub/Sub es el ack deadline (10 s por omisión, hasta 600 s); en RabbitMQ, el mensaje se reencola si se cierra el canal del consumidor sin ack, y en sus versiones actuales el broker lo cierra solo tras 30 minutos sin ack (consumer_timeout). La idea es la misma.

El plazo correcto

El plazo tiene que ser mayor que lo que tarda el trabajo. ¿Qué pasa si no?

Un video de 10 minutos con el plazo por omisión de SQS (M02)La cuenta
Lo que tarda el trabajo
dato
600 s
Visibility timeout
por omisión
30 s
A los 30 sel primer worker sigue trabajando, pero no ha confirmado
—
el mensaje reaparece
Copias empezadas antes de que termine la primerauna cada 30 s, si hay workers libres
600 ÷ 30
20
Resultado
Veinte workers procesando el mismo video. Nada falla, pero la cuenta de GPU sale 20 veces más alta y la cola nunca baja.

La regla de la Ronda: visibility timeout mayor que el p99 del tiempo de proceso, con margen, por ejemplo el doble. Si el p99 de un trabajo es de 4 minutos, un plazo de 8.

Para trabajos largos o muy variables, subir el plazo a horas tiene un costo: si el worker se muere de verdad, el mensaje tarda horas en volver. La salida es un latido (heartbeat): el worker vivo renueva su plazo cada tanto (en SQS, con ChangeMessageVisibility, más o menos a la mitad del plazo). Con un plazo de 2 minutos renovado cada minuto, un worker vivo conserva el mensaje lo que haga falta, y uno muerto lo libera en 2 minutos como mucho. SQS pone un techo de 12 horas contadas desde la primera recepción, y renovar el plazo no lo reinicia.

Para ir más hondoApagarse sin dejar trabajo a medias

Los workers también se apagan a propósito: Kubernetes quita uno cuando hay menos trabajo, o la nube reclama una máquina barata (spot): AWS avisa 2 minutos antes; Google Cloud, por omisión, unos 30 s. En los dos casos, el proceso recibe primero un SIGTERM, la señal amable de «por favor, termina». Un worker bien hecho, al recibirlo, termina el mensaje que tiene y no toma otro. Si en vez de eso se muere a la mitad, el mensaje vuelve al vencer su plazo y otro empieza de cero: no se pierde, pero se paga dos veces.

Kubernetes espera 30 s por omisión entre el SIGTERM y el SIGKILL, el apagado sin aviso. Si un trabajo tarda 10 minutos, ese margen no alcanza: o se sube el margen del pod, la unidad en que Kubernetes corre cada copia (terminationGracePeriodSeconds), o el trabajo se parte en pedazos más cortos, o se acepta repetirlo. M06 trae un caso feo: un autoscaler (el programa que prende y apaga copias según la carga) que baja de 8 workers a 1 de golpe manda SIGTERM a 7 que estaban a la mitad de su video.

Para ir más hondoPedir mensajes sin gastar: el long polling

Un worker que pregunta «¿hay algo?» cada 10 ms a una cola vacía gasta CPU y, en SQS, paga cada pregunta. Con long polling, la pregunta se queda abierta hasta 20 s: si llega un mensaje en ese rato, el broker contesta en el momento; si no, contesta «nada» al final. Es el polling de la lección de APIs, pero con la espera del lado del broker. SQS lo activa con WaitTimeSeconds; un worker normal pide además varios mensajes a la vez (hasta 10 en SQS), para no hacer un viaje de red por mensaje.

El mensaje venenoso

¿Y si el trabajo no falla por mala suerte, sino porque siempre falla? Un PDF corrupto que tumba al worker cada vez que lo abre es un mensaje venenoso (poison message). Cada vez que vence su plazo, vuelve, tumba a otro worker y vuelve otra vez. Sin un límite, se reentrega para siempre.

La cola cuenta cuántas veces entregó cada mensaje. Con una regla de reenvío (en SQS, la redrive policy con maxReceiveCount), después de N recepciones sin ack (por ejemplo, 5) el mensaje se mueve a otra cola: la dead-letter queue (DLQ, «cola de mensajes muertos»). Ahí ya no estorba. Una persona lo revisa, arregla el bug y lo reenvía a la cola original (redrive), o lo marca como fallido con su motivo.

Cuánto tarda un mensaje venenoso en llegar a la DLQ (M02)La cuenta
Visibility timeout
dato
15 min
Intentos antes de la DLQmaxReceiveCount
dato
5
Tiempo hasta la DLQsi cada intento se cuelga hasta el plazo
5 × 15 min
75 min
Resultado
Más de una hora de workers ocupados con un mensaje que nunca iba a salir. Si el worker se cae al instante, el mensaje igual espera su plazo para volver.

La DLQ lleva su propia alerta: más de 0 mensajes en la DLQ ya es algo que alguien tiene que mirar. Un detalle de M12: en las colas estándar de SQS, un mensaje conserva su fecha de llegada original cuando pasa a la DLQ y vence contando desde ahí, así que la DLQ necesita una retención más larga que la cola de la que viene.

Una cola de trabajos por dentro: recibir, esconder, confirmar · Toca cada caja para ver qué hace. Usa «Recorrer» para seguir un trabajo normal, un worker que se cae a la mitad y un mensaje venenoso.
  • API · el productor
    El productor: acepta el POST /v1/jobs, guarda el estado como queued, mete el mensaje y responde 202 en milisegundos. No espera a ningún worker.
  • Cola de trabajos · SQS · plazo de 2 min
    El broker guarda cada mensaje en disco, con copias, hasta que alguien lo confirma. Al entregarlo no lo borra: lo esconde durante el visibility timeout. Lleva la cuenta de cuántas veces entregó cada mensaje.
  • DLQ · tras 5 recepciones
    La dead-letter queue: a donde van los mensajes que se recibieron 5 veces (maxReceiveCount) sin ack. Lleva más retención que la cola original, para que dé tiempo de revisarlos y reenviarlos (redrive).
  • Worker 1 · OCR · ack al final
    Un consumidor: recibe un mensaje, baja el PDF del almacén, lo procesa, escribe el resultado y solo entonces confirma. Mientras trabaja, renueva el plazo con un latido.
  • Worker 2 · una copia idéntica
    Otra copia del mismo programa. Compite con el worker 1 por los mensajes: cada mensaje se lo lleva uno solo. Para procesar más rápido, se agregan copias.
  • Resultados · results/{job_id}.json
    Cada resultado va a una llave fija, derivada del job_id. Si un trabajo se repite, sobrescribe lo mismo: el efecto es idempotente aunque la entrega sea at-least-once.
  • Alertas · DLQ > 0 · edad > 5 min
    Dos alertas: la DLQ con más de 0 mensajes, y la edad del mensaje más viejo de la cola sostenida unos minutos. La profundidad sola no dice cuánto atraso hay.
04

Al menos una vez, a lo más una vez, exactamente una vez

Esencial

At least once, at most once, exactly once

Con el ack aparece una decisión: ¿el worker confirma antes o después de hacer el trabajo? Cada orden tiene su garantía de entrega.

  • Confirmar antes de trabajar es at-most-once (a lo más una vez). Si el worker se cae después del ack, el mensaje ya se borró: ese trabajo se perdió. Nunca hay repetidos, pero puede haber huecos. Sirve para lo que se puede perder sin daño, como una métrica más entre millones.
  • Confirmar después de trabajar es at-least-once (al menos una vez). Si el worker se cae entre terminar y confirmar, el mensaje vuelve y el trabajo se hace otra vez. Nunca se pierde nada, pero puede haber repetidos.
  • exactly-once (exactamente una vez) sería lo ideal: ni huecos ni repetidos.

Por qué lo normal es «al menos una vez»

El problema está en el instante entre hacer el trabajo y confirmarlo. El worker escribió el resultado y se cayó; o mandó el ack y la red lo perdió. El broker no tiene cómo distinguir «no lo hizo» de «lo hizo y no me avisó». Es el mismo timeout ambiguo de la lección de fallas de red. Ante la duda, el broker reentrega, porque perder un trabajo suele ser peor que repetirlo.

Por eso SQS estándar, Pub/Sub y Kafka, en su forma normal, entregan al menos una vez. Y SQS estándar, además, de vez en cuando entrega un mensaje dos veces aunque nadie se haya caído: guarda copias de cada mensaje en varias máquinas, y si una no estaba disponible cuando se recibió o se borró el mensaje, esa copia puede volver a salir.

Exactamente una vez, en la práctica

Un exactly-once de punta a punta, con efectos fuera del broker (un cobro, un correo, una fila en una base), en general no existe (M02). Lo que sí existe es el efecto de una sola vez: at-least-once más un consumidor idempotente, que hace lo mismo si procesa un mensaje una vez o diez. Tres maneras, de la lección de fallas de red:

  • Llevar la cuenta de lo hecho. Cada mensaje trae un id; el consumidor guarda los ids que ya procesó y descarta los repetidos. Es la llave de idempotencia, ahora del lado del consumidor.
  • Escribir en un lugar fijo. El resultado del trabajo 8f3a va siempre a results/8f3a.json. Si se repite, se sobrescribe con lo mismo.
  • Escribir el valor, no sumar. «El total de la ventana es 7» se puede repetir; «súmale 1 al total», no. En una base de datos se hace con un upsert: insertar si no existe, reemplazar si ya existe (lo viste en la lección de bases de datos).

Esta simulación procesa 1,000 mensajes con un consumidor que se cae el 5 % de las veces, y siempre en el peor momento: entre el ack y el trabajo, o entre el trabajo y el ack. Prueba los tres casos: ack antes, ack después y ack después descartando repetidos por id.

python
import random
from collections import deque

def correr(modo, deduplicar, n=1000, p_caida=0.05, semilla=7):
    random.seed(semilla)
    cola = deque(range(n))              # los ids de n mensajes
    entregas, efectos, descartados = 0, 0, 0
    vistos = set()                      # ids ya procesados (en la vida real, en una base)
    while cola:
        msg = cola.popleft()            # el broker entrega el mensaje
        entregas += 1
        se_cae = random.random() < p_caida
        if modo == "ack antes":         # at-most-once: confirma y luego trabaja
            if not se_cae:
                efectos += 1            # si se cae aquí, el mensaje ya se borró: perdido
            continue
        # at-least-once: trabaja y luego confirma
        if deduplicar and msg in vistos:
            descartados += 1            # repetido: se confirma sin volver a hacerlo
        else:
            efectos += 1
            vistos.add(msg)
        if se_cae:                      # se cayó antes del ack: el mensaje vuelve
            cola.append(msg)
    return entregas, efectos, descartados

for modo, dedup in (("ack antes", False), ("ack después", False), ("ack después", True)):
    e, f, d = correr(modo, dedup)
    etiqueta = modo + (" + dedup por id" if dedup else "")
    print(f"{etiqueta:<26} entregas {e:>5} · efectos {f:>5} · descartados {d:>2} · perdidos {max(0, 1000 - f):>2}")
Salida
ack antes                  entregas  1000 · efectos   942 · descartados  0 · perdidos 58
ack después                entregas  1060 · efectos  1060 · descartados  0 · perdidos  0
ack después + dedup por id entregas  1060 · efectos  1000 · descartados 60 · perdidos  0

Con ack antes se pierden 58 de 1,000. Con ack después no se pierde nada, pero salen 60 efectos de más (56 trabajos se hicieron dos veces y 2, tres veces): si cada uno mandaba un correo, 58 vendedores reciben el mismo aviso repetido. Descartando por id, salen exactamente 1,000 efectos.

Para ir más hondoPor qué nadie puede prometer «exactamente una vez»

Hay un problema clásico, el de los dos generales: dos ejércitos solo ganan si atacan a la vez, y se comunican con mensajeros que pueden ser capturados. El general A manda «atacamos al amanecer». Si no sabe si llegó, no ataca. B contesta «recibido», pero tampoco sabe si su respuesta llegó, así que pide confirmación de la confirmación… Ningún número finito de mensajes da certeza a los dos. Un worker y un broker están igual: el último ack siempre puede perderse.

Lo que sí existe son ayudas dentro de un sistema. SQS FIFO descarta los envíos repetidos con el mismo id durante 5 minutos: evita que un productor que reintenta meta el mismo mensaje dos veces. Kafka tiene productores idempotentes y transacciones (desde la versión 0.11, de 2017), que dan exactly-once mientras todo se lea y se escriba dentro de Kafka; Flink, un programa que procesa streams de eventos sin parar, lo usa al escribir de vuelta a Kafka. Pub/Sub lo ofrece en suscripciones de tipo pull (el consumidor pide los mensajes, en vez de que se los empujen), y solo si los consumidores se conectan desde la misma región, la zona del mundo donde la nube tiene sus centros de datos. En cuanto el efecto sale del broker (un correo, una base, una API de otro), vuelve la regla: consumidores idempotentes.

05

El stream de eventos: un registro que solo crece

Esencial

The event stream: a log that only grows

Una cola de trabajos tiene una regla: cuando un mensaje se confirma, desaparece. Sirve para repartir trabajos. Pero hay otro problema distinto.

En C07, la búsqueda del marketplace, cada vez que un vendedor publica, cambia un precio o vende algo, se produce un evento: un registro de algo que ya pasó, como «listing 42: vendido». Ese mismo evento le importa a tres sistemas: el que actualiza el índice de búsqueda, el que lo guarda en el data lake (el gran almacén de archivos de la lección de almacenamiento) para entrenar, y el monitoreo. Al segundo se le llama sink («sumidero»): un consumidor que solo copia los eventos a otro lugar. Con una cola, el primero que lo confirma lo borra y los otros dos nunca lo ven.

Un stream de eventos (event stream), también llamado log de eventos, resuelve eso. Es un registro que solo crece (append-only): cada evento nuevo se agrega al final y nada se borra al leerlo. Kafka, Amazon Kinesis y Azure Event Hubs (el de Microsoft) funcionan así.

El marcador de cada lector

Cada evento tiene su posición en el registro: el primero es el 0, el siguiente el 1, y así. Esa posición es su offset. Leer no saca nada: cada lector lleva su propio marcador, el offset del siguiente evento que le toca, como un separador en un libro.

Después de procesar, el lector guarda su marcador en el broker: es el commit del offset, «ya procesé hasta aquí». Si se cae, el que lo reemplaza sigue desde el último offset confirmado. Lo que se procesó después del último commit se vuelve a leer: también aquí la entrega es al menos una vez.

Un tópico es un registro que solo crece. Once casillas en fila, con sus offsets del 0 al 10, del más viejo a la izquierda al más nuevo a la derecha. El productor agrega cada evento nuevo al final: el último es el offset 10. Los offsets 0 y 1 ya pasaron la retención y se borraron. Arriba, una marca en el offset 7: el indexador va ahí. Abajo, una marca en el offset 4: el sink al lake, más lento, va ahí. Leer no borra nada; cada lector lleva su propio marcador y, si se cae, sigue desde el último offset que confirmó. UN TÓPICO: UN REGISTRO QUE SOLO CRECE ← más viejo el productor agrega al final indexador: offset 7 0 1 2 3 4 5 6 7 8 9 10 sink al lake: offset 4 0 y 1: pasaron la retención y se borraron, aunque alguien no los hubiera leído. Leer no borra nada. Cada lector guarda su offset con un commit.
Un tópico es un registro que solo crece. Cada evento tiene su offset. El indexador va en el offset 7; el sink al lake, más lento, en el 4. Cada uno lleva su propio marcador; leer no borra nada. Los eventos más viejos que la retención se borran por el principio.

Tópicos y retención

Los eventos se agrupan por tema en un tópico (topic): listing-events para los cambios del catálogo, txn-events para los pagos con tarjeta de C09, el caso de fraude. Un tópico es un registro con nombre, al que los productores escriben y del que los consumidores leen.

Como nada se borra al leer, el log se borraría solo por tiempo o por tamaño. Esa es su retención: Kafka guarda 7 días por omisión (log.retention.hours=168); Kinesis, 24 horas por omisión, ampliable hasta 365 días (pagando más). Lo que pasa de la retención se borra por el principio, se haya leído o no.

Que nada se borre al leer da tres cosas que una cola no da:

  • Varios lectores a su ritmo. El indexador va al día; el sink al lake va un minuto atrás; el monitoreo lee solo de vez en cuando. Ninguno estorba a los otros.
  • Releer. Si un bug escribió mal las features de ayer, el consumidor regresa su marcador al offset de ayer y procesa otra vez (replay), dentro de la retención.
  • Agregar un lector nuevo sin tocar a nadie. Un sistema nuevo empieza a leer desde el principio de la retención o desde el final, y el productor ni se entera.

Para reprocesar un año, la retención no alcanza: la fuente de verdad es el data lake, no Kafka (M09). El stream es transporte, no archivo.

Para ir más hondoCómo un log no pierde lo que ya aceptó

Kafka corre en un grupo de máquinas, un cluster; en su vocabulario, cada máquina del grupo es un broker. Cada pedazo del log se guarda en varias: con un factor de replicación de 3 (replication factor), hay 3 copias, casi siempre en 3 zonas distintas de la nube (centros de datos separados, cerca uno del otro). Una de las copias es la principal y las otras la siguen.

El productor elige cuánto esperar antes de dar un evento por guardado, con su ajuste acks. Con acks=all, el valor por omisión desde Kafka 3.0, Kafka contesta «lo tengo» solo cuando todas las copias que van al día lo escribieron; y con min.insync.replicas=2 (por omisión vale 1, así que hay que ponerlo), rechaza la escritura si menos de 2 copias van al día. Así, perder una máquina no pierde ningún evento que el productor dio por guardado. Lo pagas en latencia de escritura y en bytes: en C09, 6 MB/s de eventos son 18 MB/s guardados entre las 3 copias.

Dos cosas que se llaman streaming

En la lección de APIs, streaming era mandar una respuesta por pedazos sobre una conexión abierta: los tokens de un LLM que van apareciendo. Aquí «streaming» quiere decir procesar un flujo continuo de eventos guardados en un log, sin que nadie esté esperando una respuesta. La Ronda (M02) los separa con una tabla: uno es una conexión entre dos programas; el otro, un registro durable que muchos leen. Cuando en una entrevista alguien diga «streaming», pregunta cuál de los dos.

06

Particiones y grupos de consumidores

Esencial

Partitions and consumer groups

Un solo registro tiene un límite: vive en una máquina y, para conservar su orden, lo lee un solo consumidor a la vez. Si cada consumidor procesa ≈ 1,500 eventos por segundo y llegan 20,000, uno solo no alcanza. Por eso cada tópico se parte en varios registros independientes: sus particiones.

Una partición del tópico (topic partition) es uno de esos registros: un pedazo del tópico, con sus propios offsets, que vive en una de las máquinas en que corre Kafka (con copias en otras). Un tópico con 12 particiones son 12 logs que se escriben y se leen en paralelo.

La llave decide la partición

¿A qué partición va cada evento? El productor le pone una llave (key), por ejemplo el id del listing, y la biblioteca con la que el productor habla con Kafka calcula una función hash de la llave: una cuenta que convierte cualquier texto en un número, siempre el mismo para el mismo texto (la verás en la lección de tiempo y hashes). El residuo de dividir ese número entre el número de particiones da la partición: si la función diera 10 y hubiera 3 particiones, 10 ÷ 3 da 3 y sobra 1, así que el evento iría a la partición 1. Cada biblioteca puede usar una función distinta: la oficial de Kafka, escrita en el lenguaje Java, usa una llamada murmur2, y la que se usa desde Python (confluent-kafka, construida sobre una biblioteca en C) usa por omisión CRC32. Por eso dos productores escritos en lenguajes distintos pueden mandar la misma llave a particiones distintas, a menos que se configuren igual. Aquí, para el ejemplo, zlib.crc32, que viene con Python:

python
import zlib

PARTICIONES = 3
log = [[] for _ in range(PARTICIONES)]  # un tópico: 3 listas que solo crecen

def publicar(llave, evento):
    p = zlib.crc32(llave.encode()) % PARTICIONES   # la llave decide la partición
    log[p].append(evento)
    return p, len(log[p]) - 1                       # partición y offset del evento

eventos = [("listing-42", "alta"), ("listing-7", "alta"), ("listing-42", "precio 900"),
           ("listing-99", "alta"), ("listing-42", "vendido"), ("listing-7", "precio 450")]
for llave, que in eventos:
    p, off = publicar(llave, f"{llave}: {que}")
    print(f"{llave:<11} {que:<11} → partición {p}, offset {off}")

# Dos grupos leen la partición de listing-42, cada uno con su propio offset
p42 = zlib.crc32(b"listing-42") % PARTICIONES
confirmado = {"indexador": 0, "sink-al-lake": 0}
for grupo, cuantos in (("indexador", 3), ("sink-al-lake", 1)):
    leidos = log[p42][confirmado[grupo]:confirmado[grupo] + cuantos]
    confirmado[grupo] += len(leidos)                # commit: «ya procesé hasta aquí»
    print(grupo, "leyó", leidos, "· su siguiente offset:", confirmado[grupo])
Salida
listing-42  alta        → partición 2, offset 0
listing-7   alta        → partición 1, offset 0
listing-42  precio 900  → partición 2, offset 1
listing-99  alta        → partición 2, offset 2
listing-42  vendido     → partición 2, offset 3
listing-7   precio 450  → partición 1, offset 1
indexador leyó ['listing-42: alta', 'listing-42: precio 900', 'listing-99: alta'] · su siguiente offset: 3
sink-al-lake leyó ['listing-42: alta'] · su siguiente offset: 1

Al final, los dos grupos leyeron la misma partición sin estorbarse: el indexador va en el offset 3 y el sink, más lento, en el 1.

Fíjate en tres cosas. Los tres eventos de listing-42 cayeron en la misma partición, en orden: alta, precio, vendido. listing-99 cayó en la misma, por casualidad. Y la partición 0 se quedó vacía: con pocas llaves, el reparto puede salir disparejo.

El orden solo vale dentro de una partición. Entre particiones no hay orden: el «precio 450» del listing 7 pudo procesarse antes o después que el «vendido» del 42. Casi nunca hace falta orden global; hace falta orden por entidad: que los eventos de un mismo listing, de una misma tarjeta, lleguen en orden. Para eso se elige la llave (M02): listing_id en C07, card_id en C09.

Un tópico de 3 particiones, P0, P1 y P2, leído por un grupo de consumidores. Arriba, el grupo tiene 2 consumidores: el consumidor 1 lee P0 y P1, y el consumidor 2 lee P2. Abajo, el grupo tiene 4 consumidores: cada partición tiene el suyo, y el consumidor 4 no tiene ninguna partición que leer. Cada color es una llave, y una llave cae siempre en la misma partición. Cada partición la lee un solo consumidor del grupo, así que más consumidores que particiones no ayuda. El orden solo vale dentro de cada partición. 3 PARTICIONES, UN GRUPO DE CONSUMIDORES Con 2 consumidores P0 P1 P2 consumidor 1 consumidor 2 El 1 lee dos particiones; el 2, una. Con 4 consumidores P0 P1 P2 consumidor 1 consumidor 2 consumidor 3 consumidor 4 no tiene qué leer Cada color es una llave: cae siempre en la misma partición. Cada partición, a un solo consumidor.
Un tópico de 3 particiones y un grupo de consumidores. Arriba, con 2 consumidores: uno lee dos particiones y el otro una. Abajo, con 4: cada partición tiene el suyo y el cuarto consumidor no tiene nada que leer.

Grupos de consumidores

¿Cómo se reparte el trabajo de leer? Con un grupo de consumidores (consumer group): varias copias de un mismo consumidor que se reparten las particiones. La regla es simple: cada partición la lee un solo consumidor del grupo. Así se conserva el orden de cada partición, y cada evento lo procesa una sola copia.

Dos consecuencias:

  • Más consumidores que particiones no ayuda. Con 12 particiones y 13 consumidores, uno se queda sin nada. El número de particiones es el techo de paralelismo del grupo.
  • Cada grupo es independiente. El indexador de C07 es un grupo; el sink al lake, otro. Cada grupo tiene sus propios offsets y recibe todos los eventos. Así varios sistemas leen el mismo tópico.

Cuando un consumidor del grupo se cae, o se agrega uno, el broker vuelve a repartir las particiones: es el rebalanceo (rebalance). El que hereda una partición sigue desde su último offset confirmado.

Para ir más hondoCuántas particiones

Las particiones se eligen por el paralelismo que vas a necesitar, no por los bytes. En C09 el tráfico es chico (≈ 6 MB/s), y aun así el tópico lleva 48 particiones: es el número de copias de Flink que se quieren poder correr en paralelo, con margen. Tener muchas tampoco es gratis: cada partición son archivos abiertos y copias que el broker mantiene, y un rebalanceo con cientos de particiones tarda más. Y cambiar el número después cambia el residuo del hash: una llave que caía en la partición 2 puede caer en otra, y por un rato los eventos viejos y los nuevos de ese listing viven en particiones distintas, sin orden entre ellas.

Por qué Kafka no es una buena cola de trabajos

Un consumidor de Kafka no confirma mensaje por mensaje: avanza su offset. Si un evento tarda mucho o es venenoso, todo lo que viene detrás en esa partición espera (head-of-line blocking, bloqueo en la cabeza de la fila). Por eso, para 50 trabajos de OCR por segundo (leer el texto de un documento escaneado), una cola como SQS es más simple y más correcta. Kafka gana cuando varios sistemas leen el mismo flujo o hace falta releerlo (M02).

Un stream de eventos en C07: un tópico, varios grupos · Toca cada caja. Usa «Recorrer» para seguir un evento de C07 desde que el vendedor marca «vendido», un consumidor que se cae y un grupo nuevo que relee desde un offset.
  • Vendedor · marca «vendido»
    Ana vendió sus tenis y lo marca en la app. Desde ahí, C07 promete que la publicación desaparezca de la búsqueda en un minuto.
  • listing-service · el productor
    El servicio del catálogo guarda el cambio en su base y publica el evento listing 42: vendido con llave listing_id. Para no perder ninguno entre la base y el tópico, usa el patrón outbox o CDC.
  • listing-events · tópico · llave listing_id
    Un tópico de Kafka partido en particiones. La llave decide la partición con un hash, así los eventos de un mismo listing quedan en orden. Guarda 7 días: leer no borra nada.
  • Lag por grupo · > 3 min durante 10
    El lag de cada grupo: el último offset escrito menos el confirmado, convertido a tiempo. Con más de 3 minutos sostenidos 10, alguien revisa. Los workers escalan por lag.
  • DLQ · fotos que no bajan
    Un evento cuya foto no se puede bajar o leer va aquí con su motivo, y el worker sigue con el siguiente offset. Así un error nunca bloquea la partición.
  • sink-al-lake · otro grupo, más lento
    Un grupo que guarda cada evento en el data lake para entrenar. Va un rato atrás, con su propio offset, y no estorba a nadie.
  • embed-worker-v7 · grupo del índice actual
    El grupo que mantiene el índice de búsqueda. Cada partición la lee un solo consumidor del grupo. Calcula el embedding de lo nuevo y borra del índice lo vendido.
  • embed-worker-v8 · un grupo nuevo
    Para estrenar un modelo nuevo, un grupo nuevo lee el mismo tópico desde el offset de un snapshot del catálogo. El v7 sigue sirviendo mientras tanto: los dos leen todo, cada uno a su ritmo.
07

Lag del consumidor: cuánto vas atrás

Esencial

Consumer lag: how far behind you are

Una cola o un stream que funciona no hace ruido. Uno que se atrasa, tampoco: no hay errores, solo datos cada vez más viejos. Por eso se mide el atraso.

El lag del consumidor (consumer lag) es cuánto va atrás un consumidor respecto de lo último que se escribió. En mensajes, es el último offset escrito menos el offset confirmado. Más útil es en tiempo: cuántos segundos de eventos le faltan por leer.

Un déficit de 10 % durante 10 minutos (M02)La cuenta
Eventos que llegan
dato
20,000 por segundo
Lo que procesa el grupo12 particiones, un consumidor cada una
12 × ≈ 1,500
18,000 por segundo
Lo que crece el lag
20,000 − 18,000
2,000 por segundo
Lag tras 10 minutos
2,000 × 600 s
1.2 M eventos
Lag en tiempolo atrasado entre lo que llega por segundo
1.2 M ÷ 20,000
≈ 60 s
Agregar un consumidor número 13no hay partición que darle
—
0 por segundo más
Resultado
El grupo va un minuto atrás, y cada minuto se atrasa 6 s más. Ninguna línea de log dice «error».

En una cola como SQS no hay offsets, y el atraso se mide con la edad del mensaje más viejo (en SQS, ApproximateAgeOfOldestMessage). La profundidad sola, cuántos mensajes esperan, engaña: 50 mensajes pueden ser 2 minutos o 2 horas de atraso, según lo que tarde cada uno (M12).

La alerta

Una alerta típica: lag mayor que ≈ 60 s durante 5 minutos. El «durante» importa: un lag que sube un momento después de un despliegue y se recupera solo no debería despertar a nadie. El umbral sale de la promesa del sistema: si C07 promete que un listing nuevo se pueda buscar en 5 minutos o menos, un lag de 3 minutos ya se come la mayor parte del margen.

Escalar con la cola

El lag también sirve para decidir cuántos workers prender. En Kubernetes, el ajuste normal de copias (el HPA de la lección de async) mira la CPU, y la CPU no dice cuántos mensajes esperan. KEDA es un complemento que se instala en Kubernetes y escala según una métrica de afuera: la profundidad de una cola de SQS o el lag de Kafka.

Cuántos workers pide KEDA (M06)La cuenta
Espera que se acepta en la colala promesa
dato
300 s
Lo que tarda cada trabajo
dato
≈ 20 s
Mensajes por workerqueueLength en KEDA
300 ÷ 20
15
Mensajes en la colavisibles más en vuelo
dato
120
Workers que pide
⌈120 ÷ 15⌉
8
Resultado
Con 8 workers, el último de 15 mensajes espera 15 × 20 s ≈ 300 s: justo la promesa.

La regla de M12: escalar por cantidad, alertar por edad. La cantidad reacciona rápido y prende workers; la edad dice cuándo la promesa está en peligro. Y el techo de workers lo pone tu presupuesto o tu cuota de GPUs, no el autoscaler.

Para ir más hondoEl mensaje venenoso que no envejece la cola

M12 trae una trampa de SQS. En una cola estándar, un mensaje que se recibió 3 o más veces sin borrarse se mueve al final de la fila y deja de contar en la edad del mensaje más viejo: la métrica pasa a mirar el siguiente. Una imagen corrupta que tumba al worker una y otra vez no hace sonar la alerta de edad, aunque esté ocupando workers. Por eso la DLQ lleva su propia alerta, y por eso también se vigilan los reintentos por mensaje: en un sistema asíncrono, los mensajes en la DLQ y los reintentos por mensaje son lo que el porcentaje de errores 5xx es en uno síncrono.

El lag de un grupo de consumidoresCalculadora

Un tópico con particiones y un grupo que las lee. Cada partición la lee un solo consumidor del grupo. Arranca con el ejemplo de M02: 20,000 eventos por segundo y 12 particiones de ≈ 1,500.

Consumidores que leen algo12
Consumidores sin particiónesperan por si otro se cae0
Lo que procesa el grupo, por segundo18,000
Lo que crece el lag, por segundo0: el grupo va al día2,000
Lag al final, en eventos1,200,000
Lag al final, en segundoscuántos segundos de eventos le faltan por leer60

Con los valores iniciales, el lag en segundos crece 6 s por minuto: pasa de 60 s a los 10 minutos y, si sigue así, una alerta de «más de 60 s durante 5 minutos» suena a los ≈ 15.

Prueba 14 consumidores con 12 particiones: no cambia nada. Prueba 1,800 eventos por segundo por consumidor: el grupo procesa 21,600 y el lag deja de crecer.

08

Colas a fondo: CDC, outbox y cuál elegir

Extra

Queues in depth: CDC, outbox and which one to pick

La escritura doble

Un servicio guarda un listing en su base de datos y, además, quiere publicar el evento «listing 42 creado» en Kafka. Son dos escrituras en dos sistemas, y cualquiera puede fallar sola. Si la base guarda y Kafka no, el índice nunca se entera del listing. Si Kafka recibe y la base falla, el índice tiene un listing que no existe. Es el mismo trabajo huérfano de la lección de APIs.

Hay dos salidas, y las dos se apoyan en la transacción de la base: un grupo de escrituras que se guarda completo o no se guarda (lo viste en la lección de bases de datos).

El patrón outbox («buzón de salida»): el servicio escribe el listing y el evento en la misma transacción, en una tabla aparte de la base, la outbox. Un proceso separado lee esa tabla y publica cada evento en el broker. Si la publicación falla, se reintenta; si se publica dos veces, el consumidor lo aguanta, porque es at-least-once.

CDC. El change data capture (captura de cambios) va más lejos: no toca el código del servicio. Toda base de datos seria anota cada cambio en su propio registro interno, para no perder nada si se va la luz y para que sus copias se pongan al día (en PostgreSQL es el WAL, write-ahead log; en MySQL, el binlog). Una herramienta de CDC, como Debezium, lee ese registro y publica cada inserción, cambio y borrado como un evento en Kafka (M09). Así, el resto del sistema se entera de lo que cambió en la base sin preguntarle cada rato y sin que el servicio publique nada.

OutboxCDC
Qué se publicaLos eventos que el servicio decideCada cambio de cada fila
Toca el código del servicioSí: escribe en la outboxNo
Se usa paraEventos de negocio: «publicación creada»Copiar una base a otro lado: el lake, un índice

Cuál elegir

SQSPub/SubRabbitMQKafkaKinesis
Qué esCola administrada de AWSCola y stream de GoogleBroker que instalas o rentasLog particionadoLog administrado de AWS
¿Se puede releer?No: confirmar borraSí, si guardas los confirmadosEn sus colas, no; en sus streams, síSí, desde un offsetSí, desde una posición
OrdenSin garantía; FIFO por grupoOpcional, por llavePor cola, con un consumidorPor particiónPor partición (shard)
Retención4 días; hasta 147 días; hasta 31En una cola, hasta que se consume7 días; configurable24 h; hasta 365 días
Lo que operasCasi nadaCasi nadaLas máquinasUn cluster, o pagas el servicioCasi nada

La regla de la Ronda: cola de trabajos para repartir trabajos; stream cuando varios sistemas leen lo mismo o hace falta releer. Y no inventes infraestructura: en M01, un sistema de 5 requests por segundo (≈ 430 mil al día) no justifica Kafka, y C01, con su pico de 500, tampoco necesita un sistema de colas para el camino online.

Dos matices que te pueden preguntar. Pub/Sub hace de cola y de stream, pero no tiene particiones que tú controles ni offsets que puedas regresar a cualquier punto (M13). Y Kafka trae los share groups (KIP-932, «colas para Kafka»; un KIP es una propuesta formal de cambio a Kafka), con ack por mensaje, conteo de entregas y más consumidores que particiones: llegaron a prueba en Kafka 4.0 (2025) y la versión 4.2 (febrero de 2026) los declaró listos para producción. Kafka empieza a funcionar también como cola de trabajos. Aun así, para 50 trabajos por segundo, una cola administrada sigue siendo más simple.

Qué lleva un mensaje

Un mensaje es como un sobre: adentro va el contenido (payload, la carga) y por fuera unos datos para el que lo recibe. Casi todos los brokers guardan, además del contenido, un id del mensaje, la hora en que llegó, cuántas veces se ha entregado y unos atributos que pone el productor, parecidos a los headers de HTTP. En Kafka, cada evento lleva su llave, su valor, sus headers y su hora.

Tres costumbres que la Ronda da por hechas:

  • Un id propio en el contenido, como el job_id o el id del evento. El del broker cambia si el productor reintenta; el tuyo no, y es el que sirve para descartar repetidos.
  • La hora en que pasó, no solo la hora en que llegó. Un pago que tardó 30 s en llegar cuenta en el minuto en que se hizo: lo verás en la lección de tiempo y hashes.
  • Un formato con esquema. C09 escribe sus eventos en Avro, un formato binario con un esquema que dice qué campos lleva cada evento (lo viste en la lección de formatos). Si el productor cambia el formato sin avisar, todos los consumidores se rompen a la vez; con un esquema versionado, se nota antes.

El orden en una cola

Una cola estándar de SQS no promete orden: reparte los mensajes entre muchas máquinas y los entrega más o menos como llegaron. Casi nunca importa: el PDF 8f3a y el 8f3b no dependen uno del otro. Cuando sí importa, se pide orden por entidad, igual que en Kafka: SQS FIFO ordena los mensajes que comparten un MessageGroupId, Pub/Sub los que comparten un ordering key. En SQS FIFO, mientras un mensaje de un grupo está en vuelo, el siguiente de ese grupo no sale hasta que el anterior se confirme o vuelva a la cola; en Pub/Sub, si un mensaje se reentrega, se reentregan también los que venían detrás con su misma llave. Por eso el orden se paga con menos paralelismo, y una cola FIFO aguanta menos que una estándar: sin su modo de alto rendimiento, 300 llamadas por segundo de cada tipo (enviar, recibir, borrar), o 3,000 mensajes si van en lotes de 10, según la documentación de AWS de 2026.

Cuando el trabajo tiene varios pasos

Un documento de C05 pasa por varias etapas: revisar la calidad de la foto, leerla con OCR, extraer los campos, consultar al SAT (el servicio de impuestos de México) y, si algo sale dudoso, esperar a que una persona lo revise. Encadenar cinco colas a mano funciona, pero nadie sabe en qué paso va cada documento ni qué reintentar.

Para eso existen los orquestadores de flujos de trabajo (workflow orchestrators): Step Functions de AWS, Temporal o, para trabajos programados como el reentrenamiento de cada mes, Airflow y Argo Workflows. Guardan el estado de cada trabajo, reintentan cada paso con su propio límite y pueden esperar días a una persona. Por dentro usan colas; por fuera, te dan el trabajo completo como una sola cosa con su historial. La regla de M02: una cola para un paso; un orquestador cuando hay pasos, estados y esperas largas.

Para ir más hondoCelery, Redis y las colas «de mentiras»

En Python es común repartir tareas con Celery, una biblioteca que usa Redis (la base de datos llave-valor en memoria de la lección de bases de datos) o RabbitMQ como broker. Con Redis, el broker vive en la memoria de un servidor: es rápido, pero si Redis se cae sin haber guardado a disco, los mensajes se pierden. Y Celery con Redis tiene su propio visibility timeout de 1 hora: si las tareas confirman al final (acks_late), una que tarda más de una hora se entrega otra vez y corre dos veces. La lección es la de siempre: antes de confiar en una cola, pregunta dónde guarda los mensajes y qué pasa con uno en vuelo cuando el worker se cae.

09

Errores comunes

Esencial

Common mistakes

1. Un visibility timeout más corto que el trabajo

El trabajo tarda 10 minutos y el plazo es de 30 segundos: el mensaje reaparece, otro worker lo toma y en 10 minutos hay 20 copias del mismo trabajo. No hay errores, solo una cuenta de GPU enorme.

Cómo evitarlo: un plazo mayor que el p99 del trabajo, con margen (el doble), y un latido que lo renueve si el trabajo es largo o variable.

2. Suponer que cada mensaje llega una sola vez

Un consumidor que suma 1 por cada mensaje, manda un correo o cobra, procesa los repetidos como si fueran nuevos. Con 5 % de caídas, salen ≈ 60 efectos de más por cada 1,000 trabajos.

Cómo evitarlo: consumidores idempotentes: resultado en una llave fija, upsert, o ids procesados guardados junto con el efecto. Escribe «es at-least-once» en el diseño.

3. Cola sin DLQ

Un mensaje venenoso tumba workers para siempre, o hasta que vence su retención. Mientras, ocupa la capacidad que necesitan los demás.

Cómo evitarlo: un maxReceiveCount (por ejemplo, 5), una DLQ con retención más larga que la cola, y una alerta cuando la DLQ tiene más de 0 mensajes, con una persona a cargo de revisarla.

4. Más consumidores que particiones

El lag de Kafka crece y se agregan consumidores. Si ya hay uno por partición, los nuevos se quedan sin nada que leer.

Cómo evitarlo: elegir el número de particiones por el paralelismo que vas a necesitar, con margen, desde el principio. Para escalar de verdad: consumidores más rápidos o más particiones.

5. Vigilar solo la profundidad de la cola

«Hay 50 mensajes» no dice si son 2 minutos o 2 horas de atraso. Y un mensaje venenoso puede quedarse atorado sin que la profundidad cambie.

Cómo evitarlo: alertar por la edad del mensaje más viejo, o por el lag en segundos, sostenido unos minutos. Escalar por cantidad está bien; alertar, por edad.

6. Usar una cola para algo que alguien está esperando

Meter la request de Ana en una cola agrega latencia, polling y piezas, para un trabajo de milisegundos con un fallback que ya existe. La Ronda lo descarta en C01.

Cómo evitarlo: primero la pregunta «¿alguien espera?». Si sí, síncrono con timeout y fallback; la cola es para lo que puede esperar segundos, minutos u horas.

10

En la vida real

Extra

In real life

C01: la cola está en el backfill, no en el camino de Ana

C01 responde a Ana de forma síncrona: «una cola asíncrona agregaría complejidad sin beneficio para este flujo», dice la Ronda. Pero la misma Ronda dice que la cola sí es la forma correcta para el backfill y para los equipos que mandan lotes.

El backfill clasifica ≈ 200 M de fotos viejas, en ≈ 2,000 pedazos (shards) de ≈ 100,000 fotos. Cada shard puede ser un mensaje en una cola. Con 4 máquinas de 1,000 fotos por segundo, el total tarda ≈ 200 M ÷ 4,000 ≈ 50,000 s ≈ 14 h, y cada shard ≈ 100 s. Las máquinas son spot, más baratas pero reclamables (en AWS, con 2 minutos de aviso): si una desaparece, su shard vuelve a la cola al vencer su plazo (unos minutos, con latido) y otra lo repite. Se pierden, a lo mucho, ≈ 100 s de trabajo por máquina. Como escribir la categoría de una foto es idempotente, repetir un shard no daña nada.

El log de predicciones de C01 no usa un broker: cada línea va a la salida del programa y otro programa la lleva a S3, el object storage de AWS. La respuesta no espera al log. Y el etiquetado es una cola de otro tipo: ≈ 5,000 fotos al mes que esperan a una persona.

C07: búsqueda que se entera en minutos

Los cambios del catálogo entran a un tópico de Kafka con llave listing_id, así los eventos de un mismo listing llegan en orden. Cada versión del índice lee con su propio grupo de consumidores, embed-worker-v7; para estrenar la v8, un grupo nuevo relee desde el offset de un snapshot, la foto fija del catálogo con la que se armó el índice nuevo. Una foto que no se puede bajar va a una DLQ con su motivo: un error nunca bloquea la partición. La alerta: lag de más de 3 minutos durante 10, y los workers escalan por lag.

C09 y C04: el stream chico y la cola que pagina

En C09, cada transacción deja ≈ 3 KB en Kafka entre evento y decisión. En el pico, 2,000 × 3 KB ≈ 6 MB/s, 18 MB/s con 3 copias: Kafka chico, y está bien. Lo eligen porque varios lo leen (Flink, el programa que calcula las features de fraude sobre el stream; el sink al lake; el monitoreo) y porque se puede releer durante 7 días. Flink reprocesa tras una falla (at-least-once) y escribe el valor de cada ventana, no una suma, para que repetir sea inofensivo.

En C04, cada imagen de patología es un mensaje en la cola slide-jobs. Después de 3 intentos va a la DLQ, que alerta con más de 0. Y la alerta de atraso no mira cuántos mensajes hay, sino su edad: con 170 imágenes por hora, 85 en la cola son ≈ 30 minutos de espera.

C05: un documento que nunca se pierde

C05 lee facturas y contratos: ≈ 5,000 documentos al día, ≈ 12,000 páginas, de las que solo ≈ 6,200 necesitan OCR. En el pico son ≈ 0.6 páginas por segundo: cabe en CPU, sin GPUs. Lo difícil no es la capacidad sino la promesa: ningún documento se pierde. Cada documento es un flujo de trabajo con su estado, reintentos por etapa, un estado de falla con su motivo y un paso que espera a una persona cuando un campo sale dudoso. Las consultas al SAT se reintentan, y el aviso al cliente es at-least-once.

C08: el log que entrena al modelo de mañana

C08 recomienda publicaciones en la portada. Cada respuesta deja un registro con las 50 publicaciones que vio el usuario y las features que usó el modelo, ≈ 22 KB. Con ≈ 90 M respuestas al día, 90 M × 22 KB ≈ 2 TB al día por Kafka. Con ese log se entrena el modelo de mañana, así que perderlo es un problema de datos, no de latencia. La respuesta no espera a Kafka: el productor guarda en un buffer en memoria y, si se llena, descarta y cuenta lo descartado. La Ronda avisa que esa pérdida es silenciosa y sesgada: pasa en los picos, cuando más tráfico hay. Por eso tiene su alerta: más de 0.1 % de pérdida, o el consumidor que une cada impresión con lo que el usuario hizo después (clicks, contactos) con más de 15 minutos de lag.

Fuera de ML: la cocina de un restaurante

Las comandas que el mesero cuelga en la ventanilla son una cola: el mesero no espera al cocinero, varios cocineros toman comandas, y una comanda que nadie terminó sigue colgada. El ticket de la caja, en cambio, es un log: la cocina, la contabilidad y el inventario leen el mismo registro, cada uno a su ritmo.

11

Ejercicios

Esencial

Exercises

Resuélvelos con lápiz antes de abrir la solución. Los de picos y lag se pueden comprobar con las calculadoras.

  1. 1Fácil

    Para cada trabajo di si lo harías síncrono, con una cola de trabajos o con un stream de eventos: (a) la categoría de los tenis de Ana; (b) transcribir un audio de una hora; (c) que el índice, el data lake y el monitoreo se enteren de cada venta; (d) mandarle a un vendedor el correo «tu publicación tiene 20 vistas».

    Ver la solución paso a paso
    • (a) Síncrono: Ana está esperando en el formulario.
    • (b) Cola de trabajos: tarda minutos y nadie mira la pantalla. La API responde 202 y un worker lo hace.
    • (c) Stream: tres sistemas leen el mismo evento, cada uno a su ritmo. Con una cola, el primero que confirma lo borra.
    • (d) Cola de trabajos: un trabajo por correo, que se reparte entre workers. Con un consumidor idempotente, para no mandar el correo dos veces.
  2. 2Fácil

    Un worker recibe un mensaje y confirma antes de procesarlo. Se cae a la mitad. ¿Qué garantía de entrega es y qué le pasó al trabajo? ¿Y si confirma después?

    Ver la solución paso a paso
    • Ack antes es at-most-once: el mensaje ya se borró, así que el trabajo se perdió.
    • Ack después es at-least-once: el mensaje vuelve al vencer su plazo y otro worker lo hace. No se pierde, pero puede repetirse si el primero alcanzó a terminar.
  3. 3Fácil

    Un tópico tiene 12 particiones. Un grupo de consumidores tiene 20 copias. ¿Cuántas leen algo? Y si otro grupo lee el mismo tópico, ¿qué eventos recibe?

    Ver la solución paso a paso
    • Cada partición la lee un solo consumidor del grupo: 12 leen, 8 se quedan sin nada.
    • El otro grupo tiene sus propios offsets y recibe todos los eventos del tópico, sin estorbar al primero.
  4. 4Fácil

    En la cola de C04 llegan 170 imágenes por hora y hay 85 esperando. ¿Cuánto espera, más o menos, la que acaba de llegar? ¿Qué ley usaste?

    Ver la solución paso a paso
    • La ley de Little: tiempo = lo que hay adentro ÷ lo que sale por unidad de tiempo. Si en equilibrio sale lo mismo que entra, 85 ÷ 170 por hora = 0.5 h ≈ 30 min.
  5. 5Medio

    Un trabajo de OCR tarda en su p99 4 minutos. La cola tiene el plazo por omisión de SQS, 30 s. ¿Qué pasa? ¿Qué plazo pondrías? Si a veces tarda 40 minutos, ¿qué cambias?

    Ver la solución paso a paso
    • A los 30 s el mensaje reaparece aunque el worker siga trabajando: otro lo toma, y a los 60 s otro más. Hasta 8 copias en 4 minutos (240 ÷ 30).
    • Plazo: el doble del p99, 8 minutos.
    • Si a veces tarda 40, un plazo de 80 minutos haría que un worker muerto tarde 80 en liberar el mensaje. Mejor un latido: plazo de unos minutos, renovado a la mitad mientras el worker vive.
  6. 6Medio

    Los workers de una cola estuvieron caídos 20 minutos mientras entraban 50 trabajos por segundo. ¿Cuántos se juntaron? Si al volver procesan 200 por segundo, ¿en cuánto se vacía la fila? ¿Y si solo procesan 60?

    Ver la solución paso a paso
    • Se juntaron 50 × 1,200 s = 60,000.
    • Mientras se vacía siguen llegando 50: el ritmo neto es 200 − 50 = 150 por segundo. 60,000 ÷ 150 = 400 s ≈ 7 min.
    • Con 60: neto 10 por segundo. 60,000 ÷ 10 = 6,000 s = 100 min. La capacidad de sobra es lo que vacía una fila.
  7. 7Medio

    Con KEDA, queueLength en 15 (cada trabajo tarda ≈ 20 s) y un máximo de 8 workers, llegan de golpe 400 mensajes. ¿Cuántos workers pediría KEDA sin máximo? ¿Cuánto espera el último con 8? ¿Se cumple una promesa de 300 s?

    Ver la solución paso a paso
    • Sin máximo: ⌈400 ÷ 15⌉ = 27 workers.
    • Con 8: a cada worker le tocan 400 ÷ 8 = 50 mensajes, y el último espera ≈ 50 × 20 s = 1,000 s ≈ 17 min.
    • No se cumple la promesa de 300 s. El techo de 8 es una decisión de costo: o se sube, o se acepta el atraso y se avisa con la alerta de edad.
  8. 8Medio

    Un mensaje venenoso vive en una cola con visibility timeout de 15 minutos y maxReceiveCount de 5. ¿Cuánto tarda en llegar a la DLQ si cada intento se cuelga hasta el plazo? ¿Y con un plazo de 3 minutos renovado por latido mientras el worker vive, si el worker se cae al abrir el archivo?

    Ver la solución paso a paso
    • 5 × 15 min = 75 minutos de workers ocupados.
    • Con latido, el worker muerto deja de renovar y el mensaje vuelve a los 3 minutos como mucho: 5 × 3 = 15 minutos. El latido ayuda también aquí.
  9. 9Reto

    El grupo de la sección del lag procesa 18,000 eventos por segundo con 12 particiones, y llegan 20,000. Lleva 10 minutos atrasado (1.2 M eventos). Compara dos arreglos: (a) un consumidor más rápido, de 1,800 por segundo; (b) un tópico de 24 particiones con 24 consumidores de 1,500. ¿En cuánto recupera cada uno el atraso?

    Ver la solución paso a paso
    • (a) 12 × 1,800 = 21,600 por segundo. Sobran 1,600: 1.2 M ÷ 1,600 = 750 s ≈ 12.5 min.
    • (b) 24 × 1,500 = 36,000. Sobran 16,000: 1.2 M ÷ 16,000 = 75 s.
    • (b) recupera 10 veces más rápido, pero cambiar las particiones de un tópico que ya existe mueve qué llave cae en qué partición, y con eso el orden por llave durante el cambio. Por eso se eligen con margen desde el principio.
  10. 10Reto

    El sink de C09 lee pagos de Kafka y suma 1 al contador «pagos de esta tarjeta en la última hora». Después de un rebalanceo, el contador de una tarjeta marca 9 cuando hubo 6 pagos, y el modelo de fraude la bloquea. ¿Qué pasó y cómo lo arreglas?

    Ver la solución paso a paso
    • Un consumidor procesó 3 pagos, se cayó antes de hacer commit de su offset, y el que heredó la partición los releyó: at-least-once. Cada relectura sumó 1 otra vez.
    • Arreglo 1: escribir el valor, no sumar. El sink calcula el conteo de la ventana a partir de los eventos y escribe «6» con un upsert por (tarjeta, ventana). Repetirlo deja 6.
    • Arreglo 2: descartar por id de evento antes de contar, guardando el id junto con el cambio.
    • Lo que no arregla: «confirmar antes». Eso cambia repetidos por huecos, y en fraude un pago perdido es un ataque que no se ve.
12

Lo que sigue

Esencial

What comes next

En la Ronda:

  • M02 · Patrones de inferencia dedica dos secciones a esta lección: «Asíncrona con colas» (el 202, el visibility timeout, la tabla de qué cola usar, el backlog de 60,000) y «Entrega, reintentos e idempotencia» (at-least-once, la DLQ, el orden por llave). Y «Streaming» trae el lag de 60 s.
  • M06 · Orquestación y escalado escala los workers con KEDA por la profundidad de SQS o el lag de Kafka, y explica el SIGTERM que termina el mensaje actual.
  • M12 · Monitoreo alerta por la edad del mensaje más viejo y por la DLQ, y muestra el mensaje venenoso que no envejece la cola.
  • Los casos. C01 (backfill en shards), C04 (cola de imágenes con DLQ), C07 (Kafka por listing y grupos por versión) y C09 (Kafka chico con Flink).

En el Dojo: la estación 12, «Monitoreo y reentrenamiento», junta las fotos nuevas y las etiquetas para el siguiente entrenamiento, el trabajo que puede esperar por excelencia. Cuando abra, ahí lo vas a practicar.

En Sensei: la siguiente lección, «Tiempo, hashes e identificadores», explica la función hash que decide la partición, los ids únicos que sirven para descartar repetidos y por qué un evento que llega tarde cuenta en la hora en que pasó.

13

Palabras que vas a oír

Esencial

Words you will hear

Estos son los conceptos que presentó esta lección. Cada uno tiene su tarjeta con la definición completa.

Cola de mensajesmessage queueUn servicio que guarda mensajes en fila hasta que un consumidor los toma, hace el trabajo y confirma que terminó. Cada mensaje lo procesa un solo consumidor, y al confirmarlo se borra. Ejemplos: SQS, RabbitMQ, Pub/Sub.Más

Separa a quien pide un trabajo (el productor) de quien lo hace (el consumidor): el productor deja el mensaje y sigue, y los consumidores lo toman a su ritmo. El broker guarda cada mensaje en disco, con copias, hasta que alguien lo confirma con un ack. Al entregarlo no lo borra: lo esconde durante el visibility timeout, y si no llega el ack, vuelve a la fila. Por eso la entrega normal es at-least-once. Los mensajes llevan una referencia (un job_id y la llave del archivo), no el archivo. No es lo mismo que una fila en la memoria de un proceso, como la del batcher de Triton o queue.Queue de Python, que se pierde si el proceso se reinicia.

Analogía. Las comandas colgadas en la ventanilla de una cocina: el mesero no espera al cocinero, varios cocineros toman comandas y una comanda que nadie terminó sigue ahí. Se rompe en que un cocinero que se desmaya no devuelve la comanda; en una cola, el mensaje vuelve solo cuando vence su plazo.

Ejemplo. La API recibe un PDF de 200 páginas, mete 200 mensajes (uno por página) y responde 202. Con 20 workers, a ≈ 1 s por página, el documento sale en ≈ 10 s en vez de ≈ 200 s.

Ver en el glosario →

ProductorproducerEl programa que mete mensajes en una cola o eventos en un stream. No espera a que alguien los procese: los deja en el servicio que los guarda (el broker) y sigue con lo suyo.Más

En una cola de trabajos, el productor suele ser la API que acepta el pedido y responde 202. En un stream, es el servicio que publica cada cambio como evento, con una llave que decide su partición. Un productor no sabe cuántos consumidores hay ni si están prendidos: solo habla con el broker. Puede pedir que el broker confirme que guardó el mensaje antes de darlo por hecho, y si no recibe la confirmación, reintenta: por eso también el productor puede generar repetidos.

Ejemplo. En C07, el listing-service es el productor: cuando Ana marca sus tenis como vendidos, publica listing 42: vendido en el tópico listing-events con la llave listing_id.

Ver en el glosario →

ConsumidorconsumerEl programa que saca mensajes de una cola o lee eventos de un stream y hace el trabajo. Casi siempre son varias copias iguales, que se reparten los mensajes.Más

En una cola, los consumidores compiten: cada mensaje se lo lleva uno solo, y al terminar lo confirma con un ack. En un stream, los consumidores se organizan en grupos: cada partición la lee un solo consumidor del grupo, que avanza su offset. Como la entrega normal es at-least-once, un buen consumidor es idempotente: procesar un mensaje dos veces deja el mismo resultado. Un worker (trabajador) que lee de una cola es un consumidor.

Ejemplo. Los workers de OCR de un servicio de documentos: cada uno recibe un mensaje, baja el PDF, escribe results/8f3a.json y solo entonces manda el ack.

Ver en el glosario →

Brokermessage brokerEl servicio que guarda los mensajes entre productores y consumidores. Los recibe, los guarda en disco con copias y los entrega. Ejemplos: SQS, RabbitMQ, Pub/Sub, Kafka.Más

Es lo que hace confiable a una cola: antes de decirle al productor «lo tengo», guarda el mensaje en disco, casi siempre en varias máquinas. Lleva la cuenta de qué mensajes están en vuelo, cuántas veces entregó cada uno y, en un stream, el offset confirmado de cada grupo. Algunos son servicios administrados que no operas (SQS, Pub/Sub, Kinesis); otros los instalas y mantienes tú, o pagas a alguien que lo haga (RabbitMQ, Kafka). Como vive aparte, sobrevive a que se reinicie cualquier productor o consumidor.

Ejemplo. En C09, Kafka guarda cada evento en 3 copias: 6 MB/s de eventos se vuelven 18 MB/s escritos en los brokers.

Ver en el glosario →

Stream de eventosevent stream · event logUn registro de eventos que solo crece: cada evento nuevo se agrega al final y leer no borra nada. Varios sistemas lo leen, cada uno a su ritmo con su propio marcador. Ejemplos: Kafka, Kinesis.Más

Un evento es un registro de algo que ya pasó («listing 42: vendido»). El stream los guarda en orden dentro de cada partición durante su retención (por omisión, 7 días en Kafka y 24 h en Kinesis, que se puede subir hasta 365 días) y después los borra por el principio. Cada grupo de consumidores lleva su offset, así que varios sistemas leen lo mismo sin estorbarse y se puede releer (replay) regresando el offset. Es transporte, no archivo: para reprocesar un año, la fuente de verdad es el data lake. No confundir con el streaming de una respuesta por pedazos sobre una conexión.

Analogía. El ticket largo de una caja registradora: la cocina, la contabilidad y el inventario leen el mismo rollo, cada uno hasta donde va. Se rompe en que el rollo de verdad nunca se borra; el stream sí, por el principio, al pasar la retención.

Ejemplo. En C07, el índice de búsqueda, el sink al lake y el monitoreo leen el mismo tópico listing-events. Para estrenar un modelo, un grupo nuevo relee desde el offset de un snapshot.

Ver en el glosario →

TópicotopicUn stream de eventos con nombre, que agrupa los eventos de un mismo tema: listing-events, txn-events. Los productores escriben en él y los grupos de consumidores leen de él.Más

Un tópico se parte en particiones para escribir y leer en paralelo. Tiene su propia retención y su factor de replicación (cuántas copias guarda el broker). Cada grupo que lo lee lleva un offset por partición. En Pub/Sub la palabra topic se usa para el lugar donde se publica, y cada suscripción recibe su copia.

Ejemplo. C09 tiene dos tópicos: txn-events, con ≈ 1 KB por transacción, y fraud-decisions, con ≈ 2 KB por decisión. Los guarda 7 días: ≈ 630 GB con 3 copias.

Ver en el glosario →

Partición del tópicotopic partitionUno de los pedazos en que se parte un tópico (un stream de eventos con nombre): un registro propio, con sus posiciones numeradas (offsets), que se escribe y se lee en paralelo con los demás. El orden de los eventos solo vale dentro de una partición.Más

El productor pone a cada evento una llave, y su biblioteca de Kafka calcula una función hash de la llave (murmur2 en la biblioteca oficial de Java) y toma el residuo entre el número de particiones: la misma llave cae siempre en la misma partición, y por eso los eventos de una misma entidad quedan en orden. Entre particiones no hay orden. El número de particiones es el techo de paralelismo de un grupo de consumidores, y cambiarlo después mueve las llaves de partición. No confundir con la partición de datos, que parte un dataset guardado en carpetas por fecha.

Ejemplo. En C09 la llave es la tarjeta: todos los pagos de una tarjeta caen en la misma partición y se procesan en orden. El tópico lleva 48 particiones, elegidas por el paralelismo de Flink y no por los bytes.

Ver en el glosario →

OffsetoffsetLa posición de un evento dentro de una partición de un stream de eventos: 0, 1, 2… También es el marcador de cada grupo de consumidores, el siguiente evento que le toca leer.Más

Cada evento recibe su offset al escribirse, y nunca cambia. Un grupo de consumidores guarda en el broker hasta dónde procesó: es el commit del offset. Si un consumidor se cae, el que lo reemplaza sigue desde el último offset confirmado, y lo procesado después de ese commit se vuelve a leer: entrega at-least-once. Regresar el offset a un punto anterior es releer (replay), siempre dentro de la retención. El lag en mensajes es el último offset escrito menos el confirmado.

Analogía. El separador de un libro: cada lector tiene el suyo, y leer no arranca las hojas. Se rompe en que el libro pierde sus primeras páginas con el tiempo (la retención), y un separador que se quedó ahí ya no tiene a dónde volver.

Ejemplo. En una partición con 11 eventos (offsets 0 a 10), el indexador puede ir en el 7 y el sink al lake, más lento, en el 4: el mismo registro, cada uno a su ritmo. Si el sink se cae, el que lo reemplaza sigue desde el 4.

Ver en el glosario →

Grupo de consumidoresconsumer groupVarias copias de un mismo consumidor que se reparten las particiones de un tópico. Cada partición la lee un solo consumidor del grupo; cada grupo recibe todos los eventos con sus propios offsets.Más

Dentro del grupo, un evento lo procesa una sola copia, y el orden de cada partición se conserva. Por eso más consumidores que particiones no ayuda: los de más se quedan sin nada que leer. Cuando un consumidor entra o se cae, el broker vuelve a repartir las particiones (rebalanceo) y el que hereda una sigue desde su último offset confirmado. Grupos distintos no se estorban: así el índice, el data lake y el monitoreo leen el mismo tópico.

Ejemplo. Un tópico de 12 particiones con 13 consumidores en el grupo: 12 leen y 1 espera. A 1,500 eventos por segundo cada uno, el grupo procesa 18,000, tenga 12 o 13.

Ver en el glosario →

At-least-onceat-least-once delivery · al menos una vezLa garantía de entrega en que ningún mensaje se pierde, pero alguno puede llegar más de una vez. Es la normal en colas y streams; obliga a que quien recibe aguante duplicados.Más

Sale de confirmar después de hacer el trabajo: si el consumidor se cae entre terminar y confirmar, o el ack se pierde, el broker no distingue «no lo hizo» de «lo hizo y no avisó», y reentrega. SQS estándar, Pub/Sub, Kafka y los webhooks entregan así. Se combina con un consumidor idempotente (resultado en una llave fija, upsert o ids procesados descartados) para lograr el efecto de exactly-once.

Ejemplo. 1,000 mensajes con un consumidor que se cae el 5 % de las veces antes del ack: 1,060 entregas y 60 efectos de más (56 trabajos hechos dos veces y 2, tres). Descartando por id, salen exactamente 1,000 efectos.

Ver en el glosario →

At-most-onceat-most-once delivery · a lo más una vezLa garantía de entrega en que ningún mensaje llega dos veces, pero alguno se puede perder. Sale de confirmar antes de hacer el trabajo.Más

Si el consumidor manda el ack y después se cae, el mensaje ya se borró y ese trabajo no se hace nunca. Sirve para lo que se puede perder sin daño: una métrica entre millones, una lectura de un sensor que llega cada segundo. Para casi todo lo demás se prefiere at-least-once con consumidores idempotentes, porque perder un trabajo suele ser peor que repetirlo.

Ejemplo. 1,000 mensajes con un consumidor que se cae el 5 % de las veces justo después del ack: ningún repetido, pero 58 trabajos perdidos.

Ver en el glosario →

Exactly-onceexactly-once · exactamente una vezQue cada mensaje tenga su efecto una sola vez: ni perdidos ni repetidos. De punta a punta no se puede garantizar en general; en la práctica es at-least-once (nada se pierde, algo se repite) más un consumidor idempotente, al que procesar dos veces lo mismo no le cambia el resultado.Más

El último ack siempre puede perderse (el problema de los dos generales), así que ningún broker puede prometer entregar cada mensaje una sola vez a un sistema de afuera. Lo que sí se logra es el efecto de una vez: at-least-once con un consumidor idempotente. Hay ayudas dentro de cada sistema: SQS FIFO descarta envíos repetidos durante 5 minutos, y Kafka tiene productores idempotentes y transacciones (desde la versión 0.11, de 2017) que dan exactly-once mientras todo se lea y se escriba dentro de Kafka. En cuanto el efecto sale (un correo, una base), vuelve la regla de la idempotencia.

Ejemplo. El sink de C09 cuenta pagos por tarjeta en ventanas de 1 hora. En vez de sumar 1 por evento, escribe el conteo de la ventana con un upsert por (tarjeta, ventana): si Flink reprocesa, el valor queda igual.

Ver en el glosario →

Visibility timeoutvisibility timeout · tiempo de invisibilidadEl tiempo que un mensaje entregado queda escondido para los demás consumidores. Si el consumidor confirma que terminó antes de que venza, el mensaje se borra; si no, vuelve a la cola para otro.Más

Es cómo una cola sobrevive a un worker que se cae: recibir no borra, solo esconde. En SQS vale 30 s por omisión y llega hasta 12 h; en Pub/Sub se llama ack deadline (10 s por omisión, hasta 600 s). Debe ser mayor que el p99 del tiempo de proceso, con margen (el doble); si es más corto, el mensaje reaparece mientras el primero sigue trabajando y el trabajo se hace varias veces. Para trabajos largos, el worker lo renueva con un latido (en SQS, ChangeMessageVisibility), así uno muerto lo libera pronto y uno vivo lo conserva.

Ejemplo. Un video de 10 minutos con el plazo por omisión de 30 s: el mensaje reaparece cada 30 s y, si hay workers libres, 20 procesan el mismo video antes de que el primero termine.

Ver en el glosario →

Dead-letter queue (DLQ)dead-letter queue · cola de mensajes muertosLa cola a donde van los mensajes que fallaron demasiadas veces, para que dejen de tumbar workers. Alguien los revisa, arregla el problema y los reenvía, o los da por fallidos.Más

Un mensaje venenoso (un archivo corrupto, un dato imposible) falla siempre: sin límite, vuelve y vuelve para siempre. La cola cuenta las entregas de cada mensaje y, al llegar a un máximo (maxReceiveCount en SQS, por ejemplo 5), lo mueve a la DLQ. La DLQ lleva su propia alerta (más de 0 mensajes) y más retención que la cola original. Reenviar los mensajes a la cola original, ya corregido el bug, se llama redrive. En un stream, el consumidor manda el evento malo a una DLQ y sigue con el siguiente offset, para no bloquear la partición.

Ejemplo. Con un visibility timeout de 15 minutos y maxReceiveCount de 5, un mensaje venenoso que cuelga al worker tarda 75 minutos en llegar a la DLQ. En C04, una imagen va a la DLQ después de 3 intentos.

Ver en el glosario →

Lag del consumidorconsumer lagCuánto va atrás un consumidor respecto de lo último que se escribió: en mensajes (la posición del último escrito menos la del último procesado) o, más útil, en segundos. Si crece, los datos envejecen sin ningún error.Más

Es la métrica de salud de un stream. Crece cuando el grupo procesa menos de lo que llega, y en tiempo se calcula como el atraso en eventos entre los eventos que llegan por segundo. En una cola sin offsets, el equivalente es la edad del mensaje más viejo (en SQS, ApproximateAgeOfOldestMessage); la profundidad sola engaña. Se alerta por lag sostenido (por ejemplo, más de 60 s durante 5 minutos) y se puede escalar con él: KEDA prende o apaga workers según el lag o la profundidad de la cola.

Ejemplo. Llegan 20,000 eventos por segundo y el grupo procesa 18,000: en 10 minutos junta 1.2 M eventos, ≈ 60 s de atraso. Un consumidor número 13 no ayuda si hay 12 particiones.

Ver en el glosario →

CDC (change data capture)change data capture · captura de cambiosLeer cada inserción, cambio y borrado de una base de datos desde su registro interno y publicarlo como un evento, sin tocar el código de la aplicación. Ejemplo: Debezium hacia Kafka.Más

Toda base de datos seria anota cada cambio en su propio registro antes de aplicarlo. Una herramienta de CDC lee ese registro y publica los cambios en un stream, así otros sistemas (el data lake, un índice de búsqueda) se enteran sin consultar la base cada rato. Resuelve la escritura doble (guardar en la base y publicar en Kafka, donde una puede fallar sin la otra). Su pariente es el patrón outbox: el servicio escribe el evento en una tabla de la misma base, dentro de la misma transacción, y un proceso aparte lo publica.

Ejemplo. Cada cambio de precio en la tabla de publicaciones de un marketplace sale por CDC como un evento a Kafka, y el índice de búsqueda lo aplica en segundos.

Ver en el glosario →

14

Chequeo

Esencial

Check

Intenta responder en tu cabeza antes de abrir cada respuesta.

  1. 1¿Qué gana un sistema al poner una cola entre el productor y el consumidor?Ver respuesta

    Que el productor no espera: deja el mensaje y sigue. El consumidor trabaja a su ritmo, se reparte entre varios workers y, si se cae, el trabajo no se pierde. La cola absorbe los picos, siempre que en promedio se procese más de lo que llega.

  2. 2¿Por qué un broker no borra un mensaje en cuanto lo entrega?Ver respuesta

    Porque el worker puede caerse a la mitad. Lo esconde durante el visibility timeout; si llega el ack, lo borra, y si no, el mensaje vuelve a la cola para otro worker.

  3. 3¿Por qué «al menos una vez» es la entrega normal, y cómo se logra el efecto de «exactamente una vez»?Ver respuesta

    Porque el broker no distingue «no lo hizo» de «lo hizo y no avisó», y ante la duda reentrega. El efecto de una vez sale de at-least-once con un consumidor idempotente: resultado en una llave fija, upsert, o ids descartados.

  4. 4¿Qué es una DLQ y para qué sirve?Ver respuesta

    La cola a la que van los mensajes que fallaron N veces (maxReceiveCount). Saca al mensaje venenoso del camino para que no tumbe workers para siempre, y tiene su propia alerta.

  5. 5¿En qué se diferencia un stream de eventos de una cola de trabajos?Ver respuesta

    En la cola, confirmar borra el mensaje y cada uno lo procesa un solo worker. En el stream, leer no borra nada: cada grupo lleva su offset, varios sistemas leen todo a su ritmo y se puede releer dentro de la retención.

  6. 6Un tópico tiene 8 particiones y el lag crece. ¿Sirve poner 16 consumidores en el grupo?Ver respuesta

    No: cada partición la lee un solo consumidor del grupo, así que 8 se quedarían sin nada. Hay que hacer más rápido a cada consumidor o tener más particiones.

Sigue · 31Tiempo, hashes e identificadoresDatos y almacenamiento Léelo en la RondaM02 · Patrones de inferenciaLo que acabas de leer, con el ojo del entrevistador. ← 29 · Object storage, data lakes y warehouses