Desmitificando la Semántica Exactamente Una Vez de Kafka: Una Guía Completa
Comprende la semántica exactly-once de Kafka con productores idempotentes, transacciones, consumidores read_committed y commits de offsets.
Desmitificando la semántica de Exactly-Once de Kafka: Una guía completa
La semántica exactly-once de Kafka puede proteger un pipeline de procesamiento de flujos de registros de salida duplicados cuando los productores reintentan, los brokers fallan o una aplicación se reinicia. La garantía es poderosa, pero es más limitada de lo que sugiere la frase: Kafka puede hacer que las escrituras en Kafka y los offsets consumidos sean transaccionales. No puede hacer automáticamente que tu base de datos externa, pasarela de pago o API HTTP sean exactly-once.
Utiliza la semántica exactly-once cuando la salida duplicada sea costosa o difícil de limpiar, como ajustes de inventario, eventos de saldo de cuenta o temas de estado derivados consumidos por otros servicios.
Garantías de entrega en lenguaje sencillo
Las aplicaciones de Kafka suelen hablar de tres modelos de entrega.
- At-most-once: Tu aplicación puede perder registros, pero no debería procesar el mismo registro dos veces. Esto puede ocurrir cuando los offsets se confirman antes de que finalice el procesamiento.
- At-least-once: Tu aplicación no debería perder registros, pero puede procesar un registro más de una vez después de un reintento o reinicio.
- Exactly-once: Un bucle de lectura-procesamiento-escritura de Kafka confirma sus registros de salida y sus offsets consumidos como una sola transacción.
El último punto es clave. La semántica exactly-once es más sólida cuando la aplicación lee de Kafka, escribe los resultados de vuelta en Kafka y confirma los offsets dentro de la misma transacción.
Productores idempotentes
Un productor idempotente evita escrituras duplicadas causadas por reintentos del productor. Kafka asigna al productor un ID y rastrea números de secuencia para cada productor y partición. Si el broker ya aceptó un lote y luego recibe el reintento, puede rechazar el duplicado en lugar de añadirlo de nuevo.
Para los clientes actuales de Kafka, la idempotencia está habilitada por defecto cuando no configuras ajustes de productor conflictivos. Aún puedes configurarla explícitamente:
enable.idempotence=true
acks=all
acks=all significa que el líder espera a todas las réplicas sincronizadas antes de confirmar la escritura. La idempotencia también depende de configuraciones compatibles de reintentos y solicitudes en curso, así que evita sobrescribir los ajustes de fiabilidad del productor a menos que conozcas el efecto en tu versión de cliente.
La idempotencia protege los reintentos del productor, pero no hace que un flujo de trabajo de procesamiento completo sea atómico. Si tu aplicación consume de un tema y produce en otro, necesitas transacciones para vincular la salida y la confirmación del offset.
Transacciones de Kafka
Las transacciones permiten que un grupo de productores agrupe múltiples escrituras en una unidad atómica. El productor necesita un transactional.id estable.
transactional.id=inventory-adjuster-0
enable.idempotence=true
acks=all
Un flujo de transacción típico es:
- Inicializar transacciones cuando la aplicación se inicia.
- Iniciar una transacción.
- Consumir registros del tema de entrada.
- Producir registros de salida.
- Enviar los offsets consumidos a la transacción.
- Confirmar la transacción, o abortarla en caso de fallo.
Si el proceso falla antes de la confirmación, Kafka no expone la salida no confirmada a los consumidores read_committed. Al reiniciar, la aplicación puede leer los mismos registros de entrada de nuevo y producir un resultado confirmado.
Ajustes del consumidor que importan
Los consumidores que leen salida transaccional deben usar:
isolation.level=read_committed
enable.auto.commit=false
read_committed oculta los registros de transacciones abortadas. enable.auto.commit=false evita que el consumidor confirme offsets fuera de la transacción.
El nombre de la propiedad es importante. El ajuste del consumidor de Kafka es enable.auto.commit, no auto.commit.enable.
Para una aplicación manual de consumidor-productor, la confirmación del offset debe ser parte de la transacción del productor. En el cliente Java, eso significa usar las APIs del productor transaccional, incluyendo el envío de offsets a la transacción antes de confirmarla.
Un escenario concreto
Imagina un tema orders y un tema de salida inventory-events. Tu servicio lee un pedido, verifica el SKU y escribe un evento de deducción de inventario.
Sin transacciones, un fallo después de escribir la salida pero antes de confirmar el offset de entrada puede crear una deducción duplicada después del reinicio. Con transacciones, el evento de salida y la confirmación del offset de entrada tienen éxito o fallan juntos. Un reinicio puede volver a leer el pedido, pero solo un evento de inventario confirmado se vuelve visible para los consumidores read_committed posteriores.
Límites a tener en cuenta
La semántica exactly-once de Kafka no cubre efectos secundarios fuera de Kafka a menos que los diseñes para ello. Si el mismo servicio también escribe en PostgreSQL o llama a una API de facturación, ese efecto secundario externo necesita su propia clave de idempotencia, restricción única, estrategia de transacción o patrón de buzón de salida.
Las transacciones también añaden sobrecarga de coordinación. Para la ingesta simple de registros donde los duplicados son aceptables, los productores idempotentes más consumidores at-least-once pueden ser suficientes.
Lista de verificación práctica
Usa un transactional.id estable por instancia de aplicación o tarea. No permitas que dos productores activos usen el mismo ID transaccional al mismo tiempo.
Configura los consumidores de salida transaccional en read_committed. Deshabilita las confirmaciones automáticas de offsets en bucles de procesamiento transaccional.
Mantén las transacciones cortas. Las transacciones grandes pueden aumentar la latencia y hacer que la recuperación sea más lenta.
Trata los sistemas externos por separado. Kafka puede proteger el estado de Kafka, pero tus escrituras en la base de datos aún necesitan un diseño idempotente.
La conclusión útil: la semántica exactly-once no es un interruptor mágico. Son un conjunto de elecciones de productor, consumidor y transacción que funcionan mejor para el procesamiento de flujos de Kafka a Kafka.