¿Te resulta más fácil leer con fondo crema, un tono de color, más contraste o un texto más grande? Elige tus colores y tu texto una sola vez, para todas las páginas.

Lección gratuita

Reintentos e idempotencia en Kafka

Después de esta lección: Puedo diseñar un consumidor que sobrevive a los reintentos sin repetir su trabajo.

Lección de 55 min6 comandosLaboratorio de Kafka: topics, productores, consumidores, particiones y offsets en el navegador

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

PalabraQué significa
Reintentovolver a enviar un mensaje sin saber si llegó
Al menos una vezcada mensaje llega una o más veces, nunca cero
Idempotenteque repetirlo da el mismo resultado que hacerlo una vez
Duplicadoun mensaje que llega repetido
Desplazamientoel número de un mensaje en su partición, desde 0 (offset en inglés)
Confirmaranotar 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

PasoQué ocurreResultado en el registro
1El productor envía «cobro c1»El mensaje queda guardado
2La confirmación se pierde por el caminoEl productor no la recibe
3Pasado el plazo, reenvía «cobro c1»El mensaje queda guardado otra vez
4Un consumidor lee todo el registroVe «cobro c1» dos veces

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Crear un topic de una partición

Comando
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

Comando
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

Comando
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

Comando
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

Comando
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

Código: python
# 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

Comando
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

Código: python
# 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».

Sobre este laboratorio y su código: Laboratorio de Kafka: topics, productores, consumidores, particiones y offsets en el navegador

¿Enseñas a una clase? Asigna este módulo a tus alumnos con guía del profesor, tareas con corrección automática y un laboratorio para cada alumno.