Compartir este artículo

La versión corta

Cuando una partición de Kafka queda en silencio, ¿terminó, o solo va lenta? Desde fuera ambas cosas se ven idénticas — y la respuesta determina si puede actuar con seguridad sobre lo que ya tiene. Netflix, Uber y Walmart han publicado cómo lo manejan, y cada uno renuncia a algo: un segundo pipeline, un carril de paralelismo, la corrección bajo reprocesamiento. Ingeniería sólida, en todos los casos. Ninguno le pregunta a Kafka lo único que Kafka ya sabe.

Me topé con esto en un flujo de inventario para una empresa Fortune 50: trece topics de Kafka, tres particiones cada uno, cientos de millones de eventos al día, cada evento de un producto dado requerido en estricto orden de tiempo de evento — leyendo siete días de historial desde cero para entrar directamente al tráfico pico en vivo. El primer diseño puso una ventana de treinta minutos delante de todo, aún necesitaba un segundo job para recoger lo que la ventana perdía, y aun así dejaba salir el 5.5% de los registros en el orden equivocado.

La versión que llegó a producción no espera un reloj. Lee los offsets de Kafka.

Así se hizo — y por qué la idea es más antigua que Kafka.

El sacrificio que todos hacen / §01

No es el problema de un solo equipo. Cuatro organizaciones de ingeniería fuertes, cuatro concesiones razonables:

  • Walmart fija cada par artículo–tienda a una única partición, de modo que solo un consumidor lo actualiza. Ordenamiento mediante claves fijas — sacrifica paralelismo.
  • Uber ejecuta un pipeline de backfill separado para recalcular tras eventos tardíos y fuera de orden, porque la ruta de streaming «puede causar imprecisiones al descartar eventos que llegan después del watermarking». Sacrifica un segundo pipeline.
  • Netflix, al hacer backfill de pipelines de Flink, descubrió que el historial reprocesado producía un orden distinto y un estado descontrolado. Sacrifica corrección y estado.
  • La propia guía de Confluent para una partición inactiva de Kafka: esperarla y estancarse, o ignorarla y aceptar «muchos mensajes tardíos cuando la partición vuelva a estar activa». Sacrifica estancamientos o datos tardíos.

Todas son razonables. El hilo común es que cada una infiere la completitud a partir del tiempo — una ventana, un timeout, una espera — cuando los offsets de las particiones dicen exactamente dónde terminan los datos. Esa es la brecha que esto cierra.

Por qué el flujo está revuelto / §02

Un topic de Kafka se divide en particiones, y cada partición es su propia vía. Dentro de una partición, el orden está garantizado: registro uno, luego registro once, siempre. Entre particiones, nada coordina nada — los consumidores que las leen nunca están en el mismo punto al mismo tiempo. Kafka hace exactamente lo que promete, los consumidores hacen exactamente lo que prometen, y la salida combinada sigue estando revuelta para cualquier cosa que dependa de la secuencia.

No se puede actuar sobre «vendido» antes que «en inventario» para la misma unidad. Así que antes de liberar cualquier cosa, un pipeline tiene que responder una pregunta: ¿tengo todo hasta este momento? Casi todas las respuestas en uso hoy son un temporizador — la ventana, el timeout de inactividad, el job de conciliación. Cada una es una conjetura sobre el silencio.

Una solución antigua: reportar, no esperar / §03

Hay un precedente, y no es un reloj mejor. Los primeros ferrocarriles operaban por intervalos de tiempo: un tren seguidor partía una vez transcurrido un intervalo fijo, así que la vía «debía» estar despejada.

En junio de 1889, un tren se detuvo en una pendiente pronunciada a las afueras de Armagh, en Irlanda. Los vagones traseros fueron desenganchados, rodaron cuesta abajo y chocaron contra el tren que venía detrás. Murieron unas ochenta personas.

El tren seguidor había salido de Armagh una vez transcurridos los veinte minutos requeridos. Habían pasado veinte minutos, así que la vía debía estar despejada. Nadie en Armagh tenía forma de saber que no lo estaba.

Gran Bretaña no respondió con un reloj mejor. El Parlamento dio a la Board of Trade la facultad de exigir el bloqueo absoluto en los ferrocarriles de pasajeros: ningún tren entra en un tramo de vía hasta que la cabina de señales de adelante confirma que el tramo está despejado. El despeje dejó de ser una duración que alguien esperaba y se convirtió en un hecho que alguien reportaba. Un tren detenido y uno que había partido dejaron de verse iguales, porque ya nadie interpretaba nada a partir del silencio.

Ese es todo el movimiento. Leer los offsets de Kafka es la cabina de señales.

No infiera la completitud del silencio. Pregúntele al sistema que lo sabe.

El reloj — operación por intervalos de tiempo
Pasaron 20 minutos → ADELANTE

El despeje es una duración que se espera. Un tren detenido y uno que partió se ven exactamente iguales.

La señal — bloqueo absoluto
La cabina de adelante confirma «tramo despejado» → ADELANTE

El despeje es un hecho que alguien reporta. Ya nadie interpreta nada a partir del silencio.

El mismo movimiento, en streaming
cronómetro = la ventana / timeout de inactividad
tramo de vía = una partición de Kafka
«tramo despejado» = la partición alcanzó su offset final
señal despejada = el watermark avanza

La construcción — tres piezas / §04

El principio es breve: el tiempo avanza solo cuando la evidencia demuestra que puede hacerlo. Materializarlo requirió tres piezas, y la parte difícil no estaba donde la gente espera.

Uno: la cola de temporizadores es el ordenamiento. Cada registro se estaciona en estado por clave bajo su propio tiempo de evento, con un temporizador de tiempo de evento para ese milisegundo exacto. Nada se emite todavía. Cuando el watermark alcanza ese timestamp, el temporizador se dispara y todo lo estacionado ahí sale junto. Flink dispara los temporizadores de una clave en orden ascendente de timestamp, así que nunca ordenamos nada. Es un método y una pieza de estado — la dificultad nunca estuvo aquí.

01 · al llegar

Almacenarlo y luego registrar un disparador para su propio tiempo de evento.

state.put(t, list)
registerEventTimeTimer(t)
nada se emite todavía
02 · al dispararse

El reloj lo alcanza, se lee esa clave, se libera todo lo que hay bajo ella.

onTimer(t)
collect(state.get(t))
B y C salen juntos
MapState⟨Long, List⟩ · por clave SKU
tiempo de eventoregistrostemporizador
10:00:18—disparado, eliminado
10:00:26B, Cvence ahora
10:00:33Den espera
10:00:41E, F, Gen espera

◆ La cola de temporizadores de Flink está ordenada por timestamp: los temporizadores se disparan en orden ascendente. Eso es el ordenamiento.

Dos: el flujo sin tiempo de evento se convierte en el reloj. Uno de los trece topics no tenía tiempo de evento en absoluto — solo publicaba productos válidos para que todo lo demás se enriqueciera con ellos. Así que no tenía nada que ordenar, y podía hacer lo que los demás no podían: emitir watermarks desde el tiempo de procesamiento, siempre el timestamp más reciente posible. El problema: el reloj de un pipeline es el mínimo entre sus entradas, así que un flujo corriendo en «ahora» no cambia nada mientras los flujos de historial aún se están contabilizando. Construí esta pieza primero y tuvo efecto cero — no estaba mal, estaba en el orden equivocado. El flujo de control solo importa una vez que los demás abandonan el mínimo, y un flujo abandona el mínimo cuando se marca como inactivo.

Tres: Kafka decide cuándo un flujo ha terminado — no un timeout. La detección de inactividad integrada de Flink es un temporizador: sin datos durante un tiempo, se marca como inactivo. Pero un timeout no puede distinguir «arrancando» de «terminado», y el arranque es el peor caso — las conexiones se establecen a velocidades distintas, un topic queda en silencio, el timeout se dispara, los demás empujan el reloj más allá de tiempos de evento que aún no ha leído, y su primer registro llega ya tarde. Eso no es un retraso, es una violación. Así que el timeout tenía que desaparecer. Al arrancar, antes de leer un registro, el job le pide a Kafka el último offset de cada partición y lo retiene. Un topic está inactivo solo cuando cada partición ha alcanzado ese número — no la mayoría, cada una. No hay duración que ajustar, porque la compuerta es un hecho proveniente de Kafka.

// once, at job startup
Map<TopicPartition, Long> endOffsets = consumer.endOffsets(partitions);

// as records arrive
if (offset + 1 >= startupEndOffsets.get(partition)) markCaughtUp(partition);

Lo que se ganó / §05

La ventana retenía cada registro hasta cerrarse — treinta minutos solo para leer el historial, otra vez en cada reinicio. Ahora un registro se libera en cuanto el watermark lo supera: en promedio menos de 100 milisegundos, incluyendo los registros retenidos para que no salieran fuera de orden. Un registro que llega en el minuto uno sale en el minuto uno. El ordenamiento siguió el mismo camino: dos capas de inactividad por timeout dejaban salir el 5.5% de los registros fuera de orden; una capa, el 1.5%; la inactividad decidida por los offsets de arranque de Kafka, cero. La ventana desapareció y también el job de barrido — un solo job lee siete días de historial y luego opera en vivo.

AntesDespués
Espera antes de que un registro se libere en ordenHasta 30 minutos (la ventana)< 100 ms en promedio*
Registros fuera del orden de tiempo de evento5.5% (inactividad por timeout)0
Jobs a ejecutarStream + barridoUno

*Latencia medida de extremo a extremo en el cliente, sobre cientos de millones de eventos al día. Ordenamiento medido en la implementación de referencia que reconstruí para este artículo.

Dos capas de inactividad por timeout
5.5%
Una capa de inactividad por timeout
1.5%
Inactividad a partir de los offsets de arranque de Kafka
0

Registros liberados fuera del orden de tiempo de evento. Implementación de referencia reconstruida para este artículo.

Lo que no promete / §06

Determinista no significa clarividente. Un evento existe para el pipeline solo cuando aterriza en Kafka. Si un productor retiene un registro y este aterriza después de que el reloj ya superó su tiempo de evento, sale de inmediato, fuera de turno, y el contador de violaciones lo registra. Ningún procesador de streams puede ordenar un evento que aún no puede ver. Dentro de ese límite la garantía es exacta, y el retraso del watermark define qué tan amplio es el límite.

reloj de tiempo de evento · watermark = ahora − retrasoclockRegistro Aaterriza antes de que el reloj alcance su tiempoliberado en ordenRegistro Baterriza después de que el reloj ya superó su tiempofuera de turno · violación +1

Ningún procesador puede ordenar un evento que aún no puede ver. Dentro del límite la garantía es exacta — el retraso del watermark define qué tan amplio es el límite.

La misma pregunta, en todas partes / §07

Una vez que lo ha visto, lo ve en cada sistema que decide qué es verdad a partir de flujos que no llegan juntos.

Un medidor fluvial queda en silencio durante una inundación. ¿El río está estable, o el agua alcanzó el sensor? En 2018, el USGS reportó estaciones de medición dejadas fuera de línea por el huracán Florence — precisamente las que más importaban. «Sin novedades» es la interpretación equivocada de ese silencio.

Un sistema de cámaras. Capture a un atleta con varias cámaras y cada una es su propio flujo, como una partición; no puede fusionar una pose 3D para un instante hasta que cada cámara lo haya reportado. Una cámara silenciosa pudo haber perdido un fotograma o estar observando a alguien inmóvil. En i3d.ai es la misma pregunta, decenas de veces por segundo.

Un agente. Un agente de IA llama a una herramienta y no recibe respuesta. Los agentes son muy buenos tratando el silencio como éxito — no volvió ningún error, así que al siguiente paso a velocidad de máquina. Un agente que actúa sobre la cuenta de un cliente tal como estaba hace una hora es un tren despachado por temporizador. La solución es la que encontraron los ferrocarriles: ningún paso procede sobre el silencio; cada paso procede sobre una confirmación que puede señalar.

Una ciudad en tormenta
Un medidor fluvial deja de transmitir
silencioso: ¿en calma, o desaparecido?

¿El río se mantiene estable — o el agua acaba de alcanzar el sensor?

Un cuerpo en movimiento
Una cámara no envía nada
silencioso: ¿en calma, o desaparecido?

¿Un fotograma perdido — o un atleta perfectamente inmóvil?

Un agente trabajando
Una llamada a una herramienta no devuelve nada
silencioso: ¿en calma, o desaparecido?

¿Tuvo éxito, falló — o sigue ejecutándose?

Ningún paso procede sobre el silencio. Cada paso procede sobre una confirmación que puede señalar.

La ganancia de confiabilidad más barata en streaming no es un motor más rápido ni una ventana más grande. Es dejar de inferir «terminado» a partir del silencio y preguntarle al sistema que ya lo sabe. La ventana espera el peor caso para cada registro; la verificación de offsets espera exactamente lo que la evidencia requiere, y ni un milisegundo más.

La implementación de referencia simplificada — generador, manejador, rastreador de puesta al día — es lo próximo que publicaré.

¿Su pipeline está esperando un reloj, o leyendo el manifiesto?

Jeffrey Patrick Carter · NeoSavant

Base histórica

Las concesiones de Walmart, Uber, Netflix y Confluent están citadas de las fuentes de ingeniería listadas abajo. El precedente del bloqueo absoluto (Regulation of Railways Act 1889) y el sistema de intervalos de tiempo que reemplazó provienen de los registros públicos. Los mecanismos de Flink y Kafka citan los documentos de diseño y el código fuente de los propios proyectos. Las cifras de ordenamiento se midieron en una reconstrucción del job de producción; la cifra de latencia es de extremo a extremo en el cliente del sistema de producción original.

Fuentes

Uber Engineering — Designing a Production-Ready Kappa Architecture for Timely Data Stream Processing

Netflix — Backfill Streaming Data Pipelines in Kappa Architecture (Data + AI Summit 2022)

Confluent — Why Is My Apache Flink Job Not Producing Results?

Apache Flink — FLIP-180: Adjust StreamStatus and Idleness definition · FLINK-35886: Idleness incorrectly detected · Generating Watermarks (DataStream) · InternalTimerServiceImpl (orden de disparo de temporizadores)

Apache Kafka — KIP-32: Add timestamps to Kafka message (CreateTime / LogAppendTime)

Wikipedia — Regulation of Railways Act 1889 (bloqueo absoluto) · Desastre ferroviario de Armagh

USGS — Working to keep vital flood information flowing (huracán Florence, 2018)

Library of Congress — Jack Delano — Indiana Harbor Belt RR switchman with a fusee, 1943 (ítem 2017878076)

¿Te resultó útil? ¡Compártelo!

Jeffrey Patrick Carter

Principal Streaming Engineer, NeoSavant

Jeffrey Patrick Carter is a Principal Streaming Engineer at NeoSavant, with 10+ production Flink and Kafka implementations — including Allstate and i3d.ai.

LinkedIn →