Las arquitecturas de procesamiento de flujos evolucionaron del procesamiento de Apache Storm de al menos una vez a las modernas garantías exactamente una vez introducidas por Apache Flink y Spark Structured Streaming. A medida que las empresas migraron de arquitecturas por lotes Lambda a flujos continuos Kappa, la complejidad pasó de transformaciones simples a la gestión de estados distribuidos para agregaciones con ventanas y sesión. La aparición de requisitos de soberanía de datos y restricciones de latencia regionales requirió implementaciones activas-activas sin depender de almacenamiento compartido NFS o SAN, creando nuevos desafíos para la consistencia del estado durante fallos geográficos.
El procesamiento de flujos con estado requiere mantener gigabytes de estado de operador (ventanas con clave, tiendas de sesión) localmente en nodos de procesamiento mientras se ingieren millones de eventos por segundo. Las semánticas exactamente una vez exigen compromisos atómicos a través de tres componentes: seguimiento de desplazamientos de origen, actualizaciones de backend de estado y escrituras en el sumidero. La replicación activa-activa entre regiones sin almacenamiento compartido introduce riesgos de cerebro partido cuando ocurren particiones de red, mientras que el escalado automático requiere migración de estado en vivo sin perder registros en tránsito o violar garantías de tiempo de procesamiento. Soportar múltiples lenguajes (Java, Python, Go) tradicionalmente obliga a una sobrecarga de serialización o a un bloqueo específico de tiempo de ejecución.
La arquitectura emplea un diseño desacoplado con Apache Kafka o Apache Pulsar como el registro unificado, nodos de procesamiento que se ejecutan en Kubernetes con sidecars gRPC agnósticos al lenguaje para soporte poliglota. La gestión del estado utiliza RocksDB integrado con puntos de control incrementales asíncronos a almacenamiento de objetos compatible con S3, coordinado a través de un servicio ligero de coordinación distribuida (etcd o ZooKeeper). Las semánticas exactamente una vez se logran a través del algoritmo de instantánea Chandy-Lamport para el estado y protocolos de compromiso en dos fases (2PC) para sumideros transaccionales (transacciones Kafka o escrituras JDBC idempotentes). La replicación entre regiones utiliza envíos de estado basados en registros a través de Kafka MirrorMaker 2 o Pulsar Geo-Replication, con resolución de conflictos a través de contadores conmutativos basados en CRDT para agregaciones y propiedad primaria versionada para el estado con clave.
La plataforma consta de cuatro capas lógicas: ingestión, procesamiento, gestión de estado y coordinación.
Capa de Ingestión
Los clústeres de Apache Kafka operan en múltiples regiones con MirrorMaker 2 que permite la replicación bidireccional de temas. La idempotencia del productor y los IDs transaccionales garantizan la ingestión exactamente una vez incluso durante fallos del productor entre regiones.
Capa de Procesamiento
Apache Flink o procesadores de flujos similares se ejecutan como StatefulSets de Kubernetes. Cada TaskManager expone un sidecar gRPC que acepta tareas serializadas en Protobuf, permitiendo que las funciones definidas por el usuario (UDFs) en Python y Go se ejecuten dentro de contenedores gRPC mientras que el tiempo de ejecución de Java gestiona el estado y los puntos de control. El JobManager fragmenta la topología entre TaskManagers utilizando hashing consistente sobre las claves de registro.
Gestión del Estado
Los backends de estado de operador utilizan RocksDB con enableIncrementalCheckpointing. Los puntos de control escriben cambios de estado delta a cubetas regionales de S3 asíncronamente cada 15 segundos. Para la consistencia entre regiones, las implementaciones activas-activas utilizan LWW-Element-Set CRDTs para agregaciones monotónicas (contadores, sumas) y afinidad de clave primaria para operaciones no conmutativas. Durante un fallo regional, los TaskManagers en espera hidratan el estado desde S3 utilizando Savepoints.
Garantías Exactamente Una Vez
El sistema implementa exactamente una vez de extremo a extremo a través de:
Una plataforma global de transporte compartido requería cálculos de precios en tiempo real de sobrecarga que agregaban la disponibilidad de conductores y la demanda de viajes por geohash a través de AWS us-east-1 y AWS eu-west-1. La arquitectura anterior utilizaba un clúster Redis con un único primario con retraso en la replicación, causando ventanas de failover de 2 segundos donde los cálculos de precios producían multiplicadores de sobrecarga obsoletos o duplicados durante cortes regionales, resultando en cálculos de tarifas incorrectos y quejas de clientes.
Solución 1: Activo-Pasivo con Almacenamiento Compartido
El equipo consideró montar EFS (NFS compartido) a través de regiones para almacenamiento de estado. Pros: Failover simplificado con semánticas de único escritor, fuerte consistencia. Contras: La latencia de EFS superaba los 100ms para el acceso entre regiones, violando el SLA de procesamiento de 50ms; además, los problemas de consistencia de escritura de NFS causaron corrupción de puntos de control durante particiones de red.
Solución 2: Arquitectura Lambda
Implementando una capa de velocidad con Kafka Streams y una capa por lotes con Spark para correcciones. Pros: Tolerancia a fallos a través de registros inmutables, recuperación simple. Contras: Complejidad operativa para mantener caminos de código duales; las correcciones por lotes llegaban demasiado tarde para una sobrecarga de precios que requería precisión de sub-segundo para equilibrar la oferta y la demanda.
Solución 3: Procesamiento de Flujos Activo-Activo con CRDTs
Desplegando Apache Flink en ambas regiones con estado RocksDB, puntos de control incrementales de S3, y contadores basados en CRDT para conteos de viajes. Pros: Latencia de procesamiento local por debajo de 20ms, resolución automática de conflictos para actualizaciones regionales concurrentes, failover sin tiempo de inactividad. Contras: Requiere refactorización de agregaciones para ser conmutativas (usando G-Counters y PN-Counters), aumentó los costos de almacenamiento para puntos de control regionales duales.
El equipo seleccionó Solución 3 porque el requerimiento comercial de 99.99% de disponibilidad con failover de menos de un segundo no podía tolerar la ventana de 2 segundos de la Solución 1 o la latencia de almacenamiento compartido. Implementaron G-Counters para conteos de conductores y LWW-Registers para los últimos multiplicadores de precios.
Resultado
El sistema logró cálculos de precios de sobrecarga exactamente una vez con latencia p99 de 15ms en ambas regiones. Durante un corte simulado en us-east-1, eu-west-1 continuó procesando sin problemas utilizando estado replicado localmente sin cálculos de tarifas duplicados. El tiempo de recuperación de puntos de control promedió 800ms, muy por debajo del requisito de menos de un segundo.
¿Cómo interactúan la sintonización del intervalo de puntos de control con los mecanismos de presión de flujo en procesadores de flujos con estado?
Muchos candidatos optimizan los intervalos de puntos de control para el tiempo de recuperación sin considerar la propagación de presión de flujo. Cuando las barreras de puntos de control se alinean lentamente debido a la presión de flujo, el algoritmo Chandy-Lamport pausa la ejecución de la tubería, lo que puede causar timeouts en cascada. El enfoque correcto implica alinear los tiempos de espera de puntos de control con los umbrales de presión de flujo, utilizando puntos de control desalineados (donde las barreras superan los buffers) durante cargas altas, y separando las fases de puntos de control sincrónicas y asincrónicas. Los puntos de control incrementales de RocksDB deben ser regulados utilizando configuraciones de RateLimiter para evitar que la compactación de SST abrumen la E/S del disco y agraven la presión de flujo.
¿Cuál es la diferencia fundamental entre la entrega al menos una vez combinada con sumideros idempotentes frente a semánticas verdaderamente exactamente una vez?
Los sumideros idempotentes garantizan que el procesamiento duplicado produzca el mismo estado de salida (por ejemplo, UPSERT en PostgreSQL o HBase), pero exponen estados intermedios durante las reintentos. Si un sumidero escribe registros A, B, luego falla y vuelve a intentar escribir A, B, C, los observadores de abajo ven momentáneamente A, B, A, B, C antes de la deduplicación. La verdadera exactamente una vez (efectivamente una vez) utiliza aislamiento transaccional donde los datos precomprometidos permanecen invisibles hasta que se completa el punto de control. Esto requiere que el sumidero soporte transacciones (por ejemplo, transacciones Kafka con isolation.level=read_committed) o protocolos de compromiso en dos fases. Los candidatos a menudo pasan por alto que la idempotencia resuelve el problema de corrección pero no el problema de consistencia/visibilidad durante la recuperación.
¿Cómo debería manejar la segmentación de tiempo de evento los datos que llegan tarde durante escenarios de fallo entre regiones?
Cuando se produce un fallo de la Región A a la Región B, los registros en tránsito en los buffers de red de la Región A pueden perderse o retrasarse más allá del horizonte del watermark. Los candidatos a menudo sugieren extender los watermarks indefinidamente, lo que rompe las garantías de completitud de la ventana. La arquitectura correcta utiliza Side Outputs (en la terminología de Flink) para la captura de datos tardíos combinada con especificaciones de Allowed Lateness. Durante el failover, el sistema debe hidratar las ventanas desde S3 Savepoints con marcas de tiempo, luego fusionar registros que llegan tarde desde la cola de errores de la región fallida en ventanas subsiguientes o activar controladores específicos de datos tardíos. Además, la generación de marcas de tiempo debe ser idempotente entre regiones; usar tiempo de reloj para las marcas de tiempo causa divergencia durante el failover, por lo que las marcas de tiempo deben derivarse de la extracción de tiempo de evento monotónico a través de ambas regiones activas.