SkillGridAprende informática practicando

Mensajería: entregar trabajo sin mantener la línea

Llamadas síncronas frente a colas de mensajes, qué compra de verdad una cola, por qué la entrega exactamente una vez no existe y cómo un consumidor idempotente convierte el al menos una vez en algo que puedes publicar.

16 min · 50 XP

La lección anterior terminó en los reintentos: un llamador que no sabe si su petición se procesó, y una clave de idempotencia que hace seguro repetirla. Esta lección toma esa idea y la sigue fuera de un solo servicio hacia la forma en que los servicios hablan entre sí, porque una cola es el sitio donde el problema del reintento pasa a ser el caso normal en vez del caso límite.

La forma que aprendiste primero es la llamada síncrona: el servicio A manda una petición al servicio B y espera la respuesta. Es fácil de razonar, porque la respuesta llega en el mismo aliento que la pregunta y los errores son obvios. Su problema no es que sea lento. Su problema es que acopla los dos servicios en el tiempo: B tiene que estar vivo, accesible y rápido justo en el momento en que A lo necesita, o A también falla. Pon tres servicios en cadena y multiplica esos momentos. Si cada uno está disponible el 99,9 por ciento del tiempo, la cadena está disponible el 99,7 por ciento, lo cual es multiplicar por diez el tiempo de caída de un sistema cuyas partes son individualmente excelentes.

CustomerOrder serviceMail serviceInvoice serviceplace order 9001send confirmation, pleasemail provider is slow todaywaits 8 s, holds a connectioncreate invoicetimeoutsorry, order failedthe order was saved, the customer was told no

La alternativa es dejar de esperar. El servicio de pedidos escribe el pedido, publica un mensaje corto diciendo que se ha hecho un pedido y devuelve de inmediato. Otra cosa, por separado, recoge el mensaje y envía el correo. Las dos partes ya no necesitan estar vivas en el mismo instante, y el cliente recibe respuesta en decenas de milisegundos. La palabra para el mecanismo es cola o broker, y el cambio importante no es la tecnología sino la dirección de la dependencia: el servicio de pedidos ahora depende de que la cola acepte un mensaje, una operación pequeña, rápida y aburrida, en lugar de que el proveedor de correo tenga éxito, que es lento y está fuera de tu control.

CustomerOrder serviceQueueWorkersplace order 9001order.placed, 9001202 accepted in 40 msdeliver to a free consumerack only after the work is donean unacked message comes back

Merece la pena precisar qué compra de verdad la cola, porque se vende como cura de todo. Compra cuatro cosas. Desacoplamiento: el productor no sabe ni le importa quién consume el mensaje, así que puedes añadir un segundo consumidor sin tocar al productor. Amortiguación: un pico llega como una cola profunda en vez de como un consumidor caído, y la cola se vacía al ritmo que el consumidor pueda. Reintentos: la reentrega es ahora trabajo del broker, así que el productor no implementa backoff. Y durabilidad: un buen broker guarda el mensaje en disco, así que un consumidor que se reinicia no pierde el trabajo que lo esperaba.

SYNCHRONOUS CALL              MESSAGE QUEUE

  caller waits                  caller gets 202 now
  both must be up               consumer may be down
  load hits the callee          queue absorbs the spike
  retry is the caller problem   redelivery is the broker job
  one failure, one error        one failure, a growing depthEl mismo pedido que debe disparar un correo y una factura, organizado de dos maneras.

Ahora la parte que decide cómo escribes el consumidor. Todo sistema de mensajes tiene que responder a una pregunta: ¿qué pasa si el consumidor se cae entre recibir un mensaje y terminar su trabajo? Solo hay tres respuestas honestas. Como mucho una vez significa que el broker envía el mensaje y lo olvida. Si el consumidor se cae a mitad del trabajo, el mensaje se ha ido y el cliente nunca recibe una factura. Al menos una vez significa que el broker conserva el mensaje hasta que el consumidor lo confirma, y lo reentrega si no llega confirmación. El trabajo nunca se pierde y puede hacerse dos veces, porque la propia confirmación puede perderse después de que el trabajo tuviera éxito. Exactamente una vez es lo que todos piden, y no es en absoluto un modo de entrega.

AT MOST ONCE           send, forget, never retry
  risk: a lost payment

AT LEAST ONCE          send, retry until acked
  risk: a duplicate payment
  this is what brokers give you

EXACTLY ONCE           not a delivery mode
  it is at-least-once plus a dedup keyDos garantías de entrega reales y una que en realidad es una combinación de la segunda con lógica de aplicación.

La razón de que el al menos una vez sea inevitable es una ventana que no puedes cerrar. El consumidor recibe el mensaje, hace el trabajo y luego confirma. Si se cae después del trabajo pero antes de la confirmación, el broker no tiene forma de saber que el trabajo ocurrió, porque nunca recibió la confirmación. Reentregar es la única opción segura, ya que la alternativa es descartar una factura en silencio. Puedes estrechar la ventana confirmando después del trabajo en vez de antes, pero no puedes eliminarla, porque la propia confirmación viaja por el mismo camino poco fiable que todo lo demás. Esto no es una limitación de un broker concreto. RabbitMQ, Kafka y SQS se comportan así, y ninguna opción de configuración lo cambia.

Así que la respuesta práctica es hacer idempotente al consumidor, que es la misma clave de idempotencia de la lección anterior, trasladada al lado del consumidor. El consumidor registra el id del mensaje en la misma transacción que el trabajo que realiza. Si el mensaje se reentrega, insertar el id falla por la clave primaria, la transacción se revierte y el segundo intento no hace nada. Es deliberadamente aburrido: una tabla, una restricción de unicidad, una transacción que hace las dos cosas juntas. El orden importa enormemente. Si registras el id y luego haces el trabajo, una caída entre medias pierde el trabajo para siempre. Si haces el trabajo y luego registras el id, una caída entre medias duplica el trabajo. Solo hacer ambas cosas en un paso atómico es correcto, y por eso la tabla tiene que vivir en la misma base de datos que el efecto.

CREATE TABLE processed (
  message_id  text PRIMARY KEY,
  handled_at  timestamptz NOT NULL DEFAULT now()
);

-- one transaction: record the id and do the work together
BEGIN;
INSERT INTO processed (message_id) VALUES ('9f2c-1');
UPDATE invoices SET sent = true WHERE id = 9001;
COMMIT;

-- a redelivery hits the primary key and rolls backLa estructura que convierte una reentrega en nada. Ambas sentencias confirman juntas o ninguna.

Incluso un consumidor idempotente puede fallar, y es importante separar dos clases de fallo. Un fallo transitorio es un proveedor de correo caído: reintenta y probablemente funcione. Un fallo permanente es un mensaje con el cuerpo malformado, o que se refiere a un pedido borrado: reintenta para siempre y nunca funcionará. Reintentar un fallo permanente para siempre es el peor resultado disponible, porque ese único mensaje bloquea la cola detrás de él y detiene también todos los mensajes sanos. La respuesta estándar es un número acotado de reintentos con backoff exponencial, y tras el último intento el mensaje va a una cola de mensajes muertos: una cola aparte de la que nadie consume automáticamente.

MAIN QUEUE             orders.email
RETRY                  backoff 1 s, 10 s, 100 s, 1000 s
MAX DELIVERIES         5
DEAD LETTER QUEUE      orders.email.dlq
ALERT ON               dlq depth rises at allLa configuración que evita que un mensaje envenenado se lleve por delante todo el circuito.

Backpressure es el nombre de lo que pasa cuando las llegadas superan el ritmo que el consumidor puede manejar, y la cola lo hace a la vez más fácil y más peligroso. Más fácil, porque el productor no se bloquea: sigue aceptando pedidos y la cola crece. Más peligroso, por la misma razón: nada empuja hacia atrás, así que el sistema acepta trabajo que no puede terminar, y el único síntoma visible es un número que sube. Hay dos respuestas honestas. Escalar los consumidores, que funciona cuando el pico se pasa rápido o el trabajo es fácil de paralelizar. O rechazar trabajo nuevo, lo que significa que el productor comprueba la profundidad de la cola y por encima de un umbral devuelve una respuesta de ocupado. Rechazar es desagradable y suele ser lo correcto pasado cierto punto, porque aceptar un pedido que entregarás el martes que viene es peor servicio que decir que no ahora.

El número que hay que vigilar es la edad de la cola, no su profundidad. La profundidad por sí sola dice muy poco: 50 000 mensajes está bien si el consumidor los vacía en diez segundos, y es una emergencia si esos mismos 50 000 tardan cuatro horas. La edad responde a la única pregunta que le importa a un cliente: cuánto esperará un mensaje publicado ahora antes de que alguien lo atienda. Un solo número, la edad del mensaje sin procesar más antiguo, convierte un atraso técnico en una promesa sobre un plazo de entrega.

Una última propiedad, porque cambia cómo diseñas las claves. Una cola no conserva automáticamente el orden en que se publicaron los mensajes, y añadir consumidores lo empeora: con tres workers, dos mensajes sobre el mismo pedido pueden tratarse en cualquier orden o a la vez. El arreglo habitual es enrutar por una clave, de modo que todo lo relativo a un pedido vaya siempre a la misma partición y lo procese en orden un único consumidor. Esa clave es una decisión de diseño con el mismo sabor que una clave de partición: elegir el id de pedido te da orden por pedido, elegir el id de usuario te da orden por usuario y garantías más fuertes entre pedidos relacionados, y no elegir nada significa que todo consumidor debe tolerar mensajes que llegan desordenados.

Práctica

  1. Un servicio de pedidos publica en una cola tras guardar el pedido. El worker de correo se cae durante diez minutos. ¿Qué experimenta el cliente y por qué esto es el diseño funcionando como se pretendía?

    Pistas
    • Nadie está esperando al worker, así que ninguna petición puede fallar por su culpa.
    • El broker conserva el mensaje hasta que alguien lo confirma.
    • El pedido es duradero; la notificación no es el pedido.
  2. Un mensaje envenenado falla en cada intento y bloquea una partición. Ordena las respuestas tal como las aplicaría un equipo cuidadoso.

    1. 1Dejar que el worker reintente un número acotado de veces con backoff exponencial
    2. 2Alertar sobre la profundidad de mensajes muertos para que una persona lo vea de inmediato
    3. 3Mover el mensaje a la cola de mensajes muertos tras el último intento
    4. 4Solo entonces vaciar la cola de mensajes que ya se habían tratado
    5. 5Arreglar la causa y luego reproducir el mensaje desde la cola de mensajes muertos
    6. 6Leer el mensaje que falla e identificar si el fallo es transitorio o permanente
    Pistas
    • Antes de automatizar, entiende por qué este mensaje es distinto de los demás.
    • Reintenta unas cuantas veces con retraso creciente y luego quítalo de en medio.
    • Una cola que nadie vigila es lo mismo que descartar el mensaje.
  3. El handler de abajo recibe el mismo mensaje dos veces, porque la cola entrega al menos una vez. Hazlo idempotente: la segunda entrega no debe enviar un segundo correo ni una segunda factura.

    El código se ejecuta en tu equipo, no en el nuestro: no lo ejecutamos. Comprueba tu solución con cada requisito y marca los cumplidos.

    Pistas
    • Cada mensaje lleva un id; el estado debería recordar qué ids ha tratado.
    • Devuelve un resultado que diga si esta entrega hizo el trabajo o fue una repetición.
    • Una repetición debe seguir informando de éxito en vez de un error.

Preguntas sobre esta lección

¿Es gratis?

Sí. El primer nivel de cada materia es gratis y no necesita cuenta. Los niveles más profundos y los grados superiores son de pago: eso es lo que mantiene vivo el proyecto.

¿Para qué edad es?

Está escrito para adolescentes curiosos a partir de unos 12 años y para adultos que empiezan de cero. No se supone experiencia previa en programación; las lecciones de redes y física no necesitan matemáticas más allá del colegio.

¿Necesito saber programar ya?

No. Las rutas de lógica, física y redes empiezan desde cero. El código lo escribes en la sección de lenguajes, que también arranca desde el principio.

¿Hay que instalar algo?

No. Todo funciona en el navegador: teoría, práctica y las simulaciones interactivas. No hay nada que descargar ni que configurar.

¿Cuánto se tarda en una materia?

Una lección dura entre 10 y 15 minutos. Un grado junior son unas 12 a 20 lecciones, así que son unas semanas de práctica constante, no una tarde.

¿Hay que seguir el orden?

Dentro de un grado los niveles se abren uno tras otro, porque cada uno se apoya en el anterior. Entre materias eres libre: empieza por lógica, física o redes.