Guía

Ingesta de eventos: de streaming a tu lakehouse

Cómo diseñar la ingesta de eventos en tiempo real sin sacrificar consistencia ni gobernanza de datos, y cuándo realmente vale la pena la complejidad extra frente a un batch programado.

Agosto 20267 min de lectura

Un pipeline batch que corre cada hora o cada noche funciona bien mientras nadie necesite saber qué está pasando ahora mismo. El problema aparece cuando el negocio sí lo necesita: detectar una transacción fraudulenta antes de que se liquide, alertar sobre un sensor IoT que se sale de rango, o alimentar un dashboard operativo que un equipo de guardia está mirando en vivo. En esos casos, esperar a la siguiente ventana de batch no es una limitación técnica menor, es directamente no cumplir el requisito. La respuesta no es "hacer el batch más frecuente" —eso solo mueve el problema— sino cambiar el modelo de ingesta a uno pensado para flujos continuos de eventos.

El modelo publish-subscribe: topics, particiones y consumer groups

Las plataformas de streaming de eventos (Kafka, Kinesis y equivalentes) resuelven un problema concreto: desacoplar a quien genera un evento de quien lo consume. Un productor escribe en un topic sin saber quién ni cuántos lo van a leer; los consumidores se suscriben al topic sin saber quién lo escribió. Esto permite que un mismo evento alimente varios sistemas downstream de forma independiente, sin coordinación directa entre ellos.

Internamente, un topic no es un log único: se divide en particiones. Cada partición es un log ordenado e inmutable, y ahí está el matiz importante: el orden solo está garantizado dentro de una partición, no a nivel de topic completo. Si necesitas que todos los eventos de un mismo cliente o dispositivo se procesen en orden, tienes que particionar por esa clave (por ejemplo, customer_id), para que siempre caigan en la misma partición.

La paralelización del consumo se gestiona con consumer groups: varios consumidores se agrupan bajo un mismo identificador y el sistema reparte las particiones entre ellos, de forma que cada partición es leída por un solo consumidor del grupo a la vez. Esto da escalabilidad horizontal casi gratis, pero también fija un límite práctico: no tiene sentido tener más consumidores activos que particiones, los sobrantes se quedan sin trabajo.

Semántica de entrega: at-least-once, exactly-once y consumidores idempotentes

Todo sistema de mensajería tiene que responder una pregunta incómoda: ¿qué pasa si un consumidor procesa un evento y falla antes de confirmar que lo procesó? Las opciones son tres semánticas de entrega:

  • At-most-once: se confirma la recepción antes de procesar. Si algo falla después, el evento se pierde. Rara vez aceptable fuera de métricas no críticas.
  • At-least-once: se confirma después de procesar con éxito. Si falla a mitad de camino, el evento se reintenta, lo que puede producir duplicados. Es el comportamiento por defecto en la mayoría de configuraciones productivas.
  • Exactly-once: cada evento se procesa una única vez, sin pérdidas ni duplicados. Es la semántica más deseable en el papel, pero requiere productores transaccionales, consumidores que participen en esa transacción y coordinación adicional entre el broker y el sink de destino. El costo es latencia y complejidad operativa.

Perseguir exactly-once en toda la cadena de punta a punta suele costar más de lo que resuelve; diseñar consumidores idempotentes sobre at-least-once resuelve el mismo problema con muchísima menos fricción.

Un consumidor idempotente asume que va a recibir duplicados y los neutraliza en la escritura: usa un identificador natural del evento como clave, hace upsert en lugar de insert, o deduplica contra una ventana de eventos recientes antes de aplicar el cambio. Esto traslada la garantía de "exactamente una vez" de la infraestructura de mensajería a la lógica de negocio, que es donde realmente importa que no haya efectos duplicados (una transacción bancaria contada dos veces, una alerta disparada dos veces).

Micro-batch vs procesamiento continuo

Aun dentro de una arquitectura de streaming, hay una decisión de diseño que determina buena parte de la complejidad operativa: procesar en micro-lotes o en modo continuo.

El micro-batch acumula eventos durante una ventana corta (segundos, no horas) y los procesa como si fueran un lote pequeño. Es el modelo que usan por defecto la mayoría de motores de streaming estructurado: cada disparo procesa lo que se acumuló desde el disparo anterior. La ventaja es que reutiliza toda la maquinaria de procesamiento batch (planificación, tolerancia a fallos, checkpoints) con cambios mínimos, y es mucho más fácil de razonar y depurar.

El procesamiento continuo, en cambio, procesa cada evento (o grupos muy pequeños) tan pronto llega, sin esperar a cerrar una ventana. La latencia baja de segundos a milisegundos, pero el costo operativo sube: hay que gestionar backpressure de forma mucho más fina, porque si el consumidor no puede seguir el ritmo del productor no hay "siguiente lote" que absorba el atraso, se acumula presión en tiempo real. La respuesta habitual a backpressure es escalar consumidores, aumentar particiones o, en última instancia, aceptar una cola de retención más larga en el topic mientras se recupera el consumo.

DimensiónMicro-batchProcesamiento continuo
Latencia típicaSegundosMilisegundos
Complejidad operativaBaja-mediaAlta
Tolerancia a fallosReintento de todo el loteRequiere checkpointing más granular
Manejo de backpressureSe absorbe entre disparosDebe gestionarse en tiempo real
Costo de infraestructuraModeradoAlto (recursos siempre activos)

Gestión de esquemas: el patrón de schema registry

Los payloads de eventos cambian con el tiempo: se agrega un campo, se renombra otro, cambia un tipo de dato. El problema es que productores y consumidores se despliegan de forma independiente, así que un cambio de esquema en el productor puede romper silenciosamente a un consumidor que no se actualizó al mismo tiempo.

El patrón que resuelve esto es el schema registry: un servicio central que almacena las versiones de cada esquema y valida la compatibilidad de cada cambio nuevo (hacia atrás, hacia adelante, o ambas) antes de permitir que se publique. En lugar de embeber el esquema completo en cada evento, el mensaje serializado (típicamente en Avro o Protobuf) lleva solo una referencia al esquema registrado, y el consumidor la resuelve contra el registry al deserializar.

Esto tiene dos beneficios prácticos: reduce el tamaño de cada evento (no repites el esquema en cada mensaje) y convierte los cambios de contrato en algo que se valida automáticamente en el momento de publicar, no algo que se descubre cuando un consumidor empieza a fallar en producción.

Si tu equipo no tiene aún un schema registry, un paso intermedio razonable es versionar el esquema explícitamente en el propio evento (un campo schema_version) y validar en el consumidor. No es tan robusto, pero es mejor que descubrir el cambio con un parseo roto en producción.

Cuándo usarlo en la práctica

La ingesta de eventos en tiempo real no es gratis: implica operar infraestructura de mensajería, monitorear consumer lag, gestionar particionamiento y absorber la complejidad de exactly-once o idempotencia. Antes de adoptarla, vale la pena verificar tres cosas:

  1. El requisito de latencia es real, no aspiracional. Si la respuesta a "¿qué tan rápido necesitas el dato?" es "en el próximo reporte diario está bien", un batch programado es más simple, más barato y más fácil de depurar. Resérvalo para casos donde segundos u horas de diferencia cambian una decisión de negocio: fraude, monitoreo de infraestructura, alertas operativas.
  2. El volumen y la variabilidad de carga justifican la infraestructura. Un volumen bajo y predecible no necesita particionamiento fino ni consumer groups elásticos; un batch cada 15 minutos puede rendir prácticamente igual con una fracción del esfuerzo operativo.
  3. El equipo puede operar esto de forma sostenida. Un pipeline de streaming mal monitoreado (sin alertas de consumer lag, sin dead-letter queues, sin gestión de esquemas) es peor que un batch simple, porque falla en silencio y de forma más difícil de diagnosticar.

En la práctica, el patrón más común y más razonable es híbrido: se ingiere el evento crudo en modo streaming hacia la capa base del lakehouse (para no perder ni un evento y mantener trazabilidad completa), y las transformaciones más pesadas —agregaciones, joins con dimensiones, modelos de negocio— se resuelven en batch o micro-batch sobre esa base, salvo que el propio consumo final (un dashboard operativo, una alerta) exija latencia de segundos. Así obtienes la garantía de no perder datos que da el streaming, sin pagar el costo de procesamiento continuo en cada capa del pipeline.