Un endpoint de checkout que redimensiona la imagen de un producto, envía un email de confirmación y actualiza un motor de recomendaciones antes de responder con 200 OK es tan rápido como su paso más lento. Si uno solo hace timeout, toda la petición falla. Una cola de mensajes permite que el endpoint entregue ese trabajo como mensajes y responda de inmediato, mientras workers separados lo recogen a su propio ritmo.
Qué hace realmente una cola de mensajes
Una cola de mensajes se coloca entre dos partes de un sistema que no necesitan estar activas al mismo tiempo. Un producer publica un mensaje que describe un trabajo por hacer. La cola lo guarda. Un consumer lo recoge, lo procesa y lo confirma. Los dos nunca se hablan directamente.
Piensa en un buzón de correo. Quien envía deja la carta y se va; quien la lee la revisa cuando está libre, y si está fuera una hora, las cartas esperan.
De ahí salen tres cosas que una llamada de función directa no puede dar:
- Desacoplamiento. El endpoint de checkout no necesita saber cómo funciona el redimensionado de imágenes, ni que es lento. Publica
{"event": "order_placed", "order_id": 4821}y sigue adelante. - Buffering. Si llegan 500 pedidos en el mismo segundo, la cola absorbe el pico. Los workers los procesan al ritmo que pueden sostener, en vez de tener 500 peticiones bloqueadas a la vez.
- Escalado y fallos independientes. Ejecuta tres workers de redimensionado y uno de email, reinicia uno sin tocar el otro, despliega el servicio de checkout sin volver a desplegar sus workers.
Ejecutar RabbitMQ en Docker
RabbitMQ es el message broker de propósito general más usado y una opción razonable si todavía no tienes una cola. Descarga la imagen management para tener también una UI web junto al broker:
| |
El puerto 5672 es el protocolo AMQP al que se conecta tu aplicación. El puerto 15672 es la UI de gestión: abre http://localhost:15672, entra con app / changeme y puedes ver en directo colas, tasa de mensajes y conexiones. El login por defecto guest/guest solo funciona desde dentro del contenedor, así que define tu propio usuario para todo lo que conectes desde el host.
Si nunca has usado Docker, Qué es Docker y para qué sirve explica imágenes, contenedores y puertos. Para un stack real, ejecuta el broker junto a tu aplicación con Compose — mira Qué Es Docker Compose y Cómo Se Usa:
| |
worker llega al broker por el hostname rabbitmq. Compose pone ambos servicios en la misma red user-defined, así que el nombre del servicio funciona también como hostname resoluble: el mecanismo que explica Qué Es una Red Docker y Cómo Se Usa.
Publicar y consumir un mensaje
pika es el cliente Python estándar para el protocolo de RabbitMQ (AMQP 0-9-1). Instálalo con pip install pika. Un producer que publica una tarea se ve así:
| |
durable=True en queue_declare le dice a RabbitMQ que mantenga la cola en sí tras un reinicio del broker. delivery_mode=pika.DeliveryMode.Persistent le dice que escriba cada mensaje en disco en vez de guardarlo solo en memoria. Sin ambos, un docker restart en el broker borra en silencio todo lo que aún no se había consumido.
El consumer toma mensajes de la misma cola y confirma cada uno solo cuando el trabajo ya está hecho:
| |
Dos ajustes hacen el trabajo de fiabilidad aquí:
basic_qos(prefetch_count=1)evita que RabbitMQ entregue a un worker un segundo mensaje antes de que confirme el primero. Sin esto, un worker ocupado puede quedarse con diez mensajes mientras otro libre no recibe ninguno.basic_ackdespués del trabajo, no antes. Si el proceso falla a mitad del redimensionado, el mensaje nunca se confirmó, así que RabbitMQ lo reentrega a otro worker en vez de perderlo. Dejaauto_ackdesactivado, que ya es el valor por defecto de pika.
Arranca tres copias del script consumer y RabbitMQ reparte la cola entre ellos round-robin. Esa es toda la historia de escalado de este patrón: sin código de coordinación, sin estado compartido entre workers.
Algunos mensajes fallan cada vez que se entregan: un payload mal formado, una llamada a un servicio que ya no existe. La reentrega convierte eso en un bucle infinito por tus consumers. Configura x-dead-letter-exchange en la cola, y los mensajes que rechazas sin volver a encolarlos (basic_nack con requeue=False) van a un exchange aparte donde los revisas a mano.
Cómo enrutan los mensajes los exchanges de RabbitMQ
Los ejemplos anteriores publican con exchange="", el default exchange, que envía un mensaje directamente a la cola indicada en routing_key. Eso cubre la mayoría de casos de cola de tareas. El modelo real de RabbitMQ pone un exchange delante de cada cola, y cambiar el tipo de exchange cambia el comportamiento de enrutado:
| Tipo de exchange | Enruta hacia | Úsalo para |
|---|---|---|
direct (el default exchange es uno) | La cola que coincide exactamente con la routing key | Colas de tareas, un grupo de consumers por cola |
fanout | Todas las colas enlazadas, ignorando la routing key | Difundir un evento a varios consumers independientes |
topic | Las colas cuyo patrón de binding coincide con la routing key (order.*.created) | Difusión selectiva, cuando los consumers quieren un subconjunto de tipos de evento |
Un exchange fanout es como consigues publish/subscribe con RabbitMQ: publicas order_placed una sola vez, y el servicio de email, el de analytics y el de detección de fraude reciben cada uno su propia copia a través de su propia cola, en vez de competir por los mismos mensajes.
Cuándo una cola de mensajes es la herramienta equivocada
- Quien llama necesita la respuesta para devolver la suya. Una cola sirve para “hazlo en algún momento”, no para “calcúlalo ahora”. Si tu endpoint de checkout necesita el coste de envío calculado antes de poder responder, eso es una llamada síncrona, no un job en cola.
- Necesitas orden estricto en toda la cola. RabbitMQ garantiza el orden por cola con un único consumer, pero añade un segundo consumer para más throughput y dos mensajes pueden terminar desordenados. Si la secuencia importa, como en un event log o una máquina de estados, necesitas una herramienta pensada para streams ordenados, como las particiones de Kafka con clave por ID de entidad.
- El job es pequeño, síncrono y todavía no tienes un broker. Un hilo en segundo plano o un job runner en el propio proceso suponen menos superficie operativa que levantar y monitorizar RabbitMQ para una tarea del tamaño de un cron.
- Ya tienes Redis y puedes tolerar perder algún job de vez en cuando. Redis Streams o una librería como BullMQ te dan una cola sin infraestructura nueva, a costa de garantías de entrega más débiles si el broker falla. Revisa primero qué hace ya Redis en tu stack: Cómo Funciona el Caching con Redis y Cómo Usarlo.
Mueve tu primera tarea a una cola
Elige algo que bloquea una petición sin necesidad: el email de confirmación, la miniatura, el ping de analytics. Ejecuta RabbitMQ en Docker, declara una cola durable para esa tarea, muévela. Configura prefetch_count=1 y confirma manualmente desde el primer día, porque añadir fiabilidad después de que un fallo ya se haya comido un mensaje cuesta mucho más que escribir esas dos líneas ahora. Recurre a un exchange fanout o topic solo cuando tengas un segundo consumer que quiera el mismo evento.