Un cobro que se hace dos veces
Un reintento es enviar de nuevo un mensaje cuando no se sabe si el primero llegó. Es normal en un sistema distribuido, porque la red falla y los nodos tardan.
El riesgo es que el mensaje llegue dos veces. Un consumidor que cobra un pedido por cada mensaje cobraría dos veces. Esta lección enseña a que un repetido no haga daño.
Al final de esta lección
Nota
Puedo diseñar un consumidor que sobrevive a los reintentos sin repetir su trabajo.
Palabras clave
| Palabra | Qué significa |
|---|---|
| Reintento | volver a enviar un mensaje sin saber si llegó |
| Al menos una vez | cada mensaje llega una o más veces, nunca cero |
| Idempotente | que repetirlo da el mismo resultado que hacerlo una vez |
| Duplicado | un mensaje que llega repetido |
| Desplazamiento | el número de un mensaje en su partición, desde 0 (offset en inglés) |
| Confirmar | anotar hasta qué mensaje se ha procesado |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un reintento puede dejar el mensaje repetido
Un productor envía un mensaje y espera la confirmación. Si no llega a tiempo, no sabe si el mensaje se perdió o si solo se perdió la confirmación.
Para no perder datos, reenvía. Si el primero había llegado, ahora hay dos copias. A esto se le llama entrega al menos una vez: nunca cero, a veces dos.
Ejemplo: cómo se duplica un mensaje
| Paso | Qué ocurre | Resultado en el registro |
|---|---|---|
| 1 | El productor envía «cobro c1» | El mensaje queda guardado |
| 2 | La confirmación se pierde por el camino | El productor no la recibe |
| 3 | Pasado el plazo, reenvía «cobro c1» | El mensaje queda guardado otra vez |
| 4 | Un consumidor lee todo el registro | Ve «cobro c1» dos veces |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Crear un topic de una partición
curl -s -X POST -H "Content-Type: application/json" -d '{"name":"avisos","partitions":1,"replicationFactor":1,"configs":{}}' http://kafka-ui:8080/api/clusters/lab/topics -o /dev/null -w "%{http_code}\n"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Enviar un cobro
curl -s -X POST -H "Content-Type: application/json" -d '{"partition":0,"key":"cobro1","value":"cobroana","headers":{},"keySerde":"String","valueSerde":"String"}' http://kafka-ui:8080/api/clusters/lab/topics/avisos/messages -w "%{http_code}\n"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Reenviar el mismo cobro por un reintento
curl -s -X POST -H "Content-Type: application/json" -d '{"partition":0,"key":"cobro1","value":"cobroana","headers":{},"keySerde":"String","valueSerde":"String"}' http://kafka-ui:8080/api/clusters/lab/topics/avisos/messages -w "%{http_code}\n"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Enviar otro cobro distinto
curl -s -X POST -H "Content-Type: application/json" -d '{"partition":0,"key":"cobro2","value":"cobrabea","headers":{},"keySerde":"String","valueSerde":"String"}' http://kafka-ui:8080/api/clusters/lab/topics/avisos/messages -w "%{http_code}\n"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Ver hasta qué desplazamiento llega la partición
curl -s http://kafka-ui:8080/api/clusters/lab/topics/avisos | grep -o '"partition":[0-9]*\|"offsetMax":[0-9]*'Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Un consumidor idempotente ignora lo ya visto
Una operación idempotente da el mismo resultado si se repite. Poner la lámpara en 25 unidades lo es. Sumar 1 al stock no lo es.
Para que un cobro sea idempotente, cada mensaje lleva un identificador único. El consumidor anota los que ya procesó y, si llega uno repetido, lo ignora.
Cobrar una sola vez cada identificador
# Tres mensajes de cobro, y el primero llega dos veces por un reintento.
mensajes = [["c1", 30], ["c1", 30], ["c2", 12]]
vistos = []
total = 0
for mensaje in mensajes:
if mensaje[0] in vistos:
print("Ya visto:", mensaje[0], "- se ignora")
else:
vistos.append(mensaje[0])
total = total + mensaje[1]
print("Cobrado:", mensaje[0], mensaje[1], "euros")
print("Total cobrado:", total, "euros")
# Quita la línea de «vistos» para ver cuánto cobraría un consumidor ingenuo.
Un error muy común
Advertencia
Mucha gente cree que Kafka entrega cada mensaje exactamente una vez. Con reintentos puede entregarlo más de una. La solución más sencilla no es evitar el duplicado, sino escribir el consumidor para que un duplicado no cambie nada.
El orden de confirmar y procesar importa
Un consumidor lee mensajes, los procesa y confirma su desplazamiento. Si se cae, al volver reanuda desde el último desplazamiento confirmado.
Procesar primero y confirmar después puede repetir un mensaje, pero no pierde ninguno. Confirmar primero y procesar después puede perder uno si la caída llega a medias.
Releer la partición desde el principio
curl -s -N "http://kafka-ui:8080/api/clusters/lab/topics/avisos/messages/v2?seekType=BEGINNING&partitions=0&limit=10" | grep -o '"partition":[0-9]*,"offset":[0-9]*\|"value":"[a-z]*"'Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Dónde reanuda tras una caída
# El consumidor lee el desplazamiento 2, lo procesa y se cae. ¿Dónde reanuda?
registro = ["m0", "m1", "m2", "m3"]
confirmado = 2
orden = "procesar y luego confirmar"
if orden == "procesar y luego confirmar":
print("Reanuda en", confirmado, ": repite", registro[confirmado])
else:
print("Reanuda en", confirmado + 1, ": no repite, pero pierde", registro[confirmado])
# Cambia el orden por «confirmar y luego procesar».