Articles

Temas Kafka consultables con flujos Kafka

En las arquitecturas de procesamiento de datos actuales, Apache Kafka se usa a menudo en la etapa de ingreso. Por lo general, este paso se utiliza para enriquecer y filtrar los datos de los mensajes entrantes; sin embargo, no es posible realizar consultas interactivas hasta una etapa posterior en la canalización de procesamiento de datos. Esto se debe a que, aunque todos los mensajes de un tema de Kafka se conservan de forma predeterminada, todavía no hay ningún mecanismo disponible que permita búsquedas rápidas de un mensaje específico en un tema.

Sin embargo, la posibilidad de consultar nuevos datos en esta etapa temprana de la canalización evitaría los retrasos de las canalizaciones de procesamiento tradicionales que generalmente incluyen pasos de preprocesamiento por lotes de larga duración y daría a los usuarios finales un acceso casi instantáneo a los datos entrantes.

Para crear aplicaciones de procesamiento de datos con Kafka, la biblioteca de flujos de Kafka, que se mantiene como parte del proyecto Kafka, se usa comúnmente para definir transformaciones y análisis de datos. Una característica importante de los flujos de Kafka son los almacenes estatales, que ofrecen una abstracción de un almacén de Clave-Valor local rápido que se puede leer y escribir al procesar mensajes con flujos de Kafka. Estos almacenes de clave-Valor se pueden rellenar continuamente con nuevos mensajes de un tema de Kafka definiendo un procesador de flujo adecuado, de modo que ahora sea posible recuperar rápidamente mensajes del tema subyacente.

Sobre la base de esta funcionalidad de secuencias de Kafka, creamos una API REST unificada que proporciona un único punto de enlace de consulta para un tema de Kafka determinado.

En resumen, la combinación de procesadores de secuencias de Kafka con almacenes de Estado y un servidor HTTP puede convertir cualquier tema de Kafka en un almacén rápido de claves y valores de solo lectura.

Kafka Streams está construido como una biblioteca que se puede incrustar en una aplicación independiente de Java o Scala. Permite a los desarrolladores definir procesadores de flujo que realizan transformaciones o agregaciones de datos en mensajes Kafka, asegurando que cada mensaje de entrada se procese exactamente una vez. El uso de Kafka Streams DSL, que se inspira en la API Java Stream, los procesadores stream y las tiendas de estado se pueden encadenar de forma flexible.

Además, al iniciar varias instancias de una aplicación basada en flujos de Kafka, los procesos forman automáticamente un clúster de procesamiento de alta disponibilidad y equilibrio de carga sin depender de sistemas externos que no sean Kafka.

Para ilustrar la arquitectura de una aplicación Kafka Streams que emplea tiendas estatales, imagine el siguiente escenario: Como operador ferroviario, cada vez que un cliente reserva un viaje en nuestro sitio web, se inserta un nuevo mensaje que consta de la identificación del cliente y una marca de tiempo en un tema de Kafka. Específicamente, uno o más productores de Kafka insertan estos mensajes en un tema con nueve particiones. Debido a que el id de cliente se elige como clave para cada mensaje, los datos que pertenecen a un cliente determinado siempre se insertarán en la misma partición del tema. Implícitamente, los productores de Kafka usan DefaultPartitioner para asignar mensajes a particiones.

Ahora, supongamos que tenemos una aplicación Kafka Streams que lee mensajes de este tema y los conserva en una tienda estatal. Debido a que la clave de cada mensaje solo consiste en el id del cliente, el valor correspondiente en la tienda estatal siempre será la marca de tiempo de la última reserva del cliente. Para lograr el máximo grado de procesamiento en paralelo, podemos iniciar hasta nueve instancias de la aplicación Kafka Streams. En este caso, a cada instancia de aplicación se le asignará exactamente una de las particiones de tema. Para cada partición de entrada, Kafka Streams crea un almacén de estado separado, que a su vez solo contiene los datos de los clientes que pertenecen a esa partición.

La arquitectura de aplicación resultante se ilustra en el siguiente diagrama.

El Kafka Procesadores de Flujo responsable de las Particiones de 4 a 9 se queda en esta ilustración. Las flechas discontinuas indican que los mensajes nuevos en una partición también se propagan a procesadores de flujo adicionales y a sus almacenes de estado, lo que permite una conmutación por error rápida si falla el procesador asignado principalmente.

Es posible usar almacenes de estado en memoria o persistentes en la aplicación. Las operaciones en los almacenes de estado en memoria son aún más rápidas en comparación con la variante persistente, que utiliza internamente un almacén RocksDB. Por otro lado, los almacenes de estado persistentes se pueden restaurar más rápido en caso de que una aplicación de transmisiones Kafka haya fallado y necesite reiniciarse. Además, el volumen de datos por almacén no está limitado por la cantidad de memoria principal cuando se utilizan almacenes de estado persistentes.

En nuestro escenario, no es necesario tener un tema de registro de cambios que registre las operaciones de escritura en los almacenes de estado: Todos los datos necesarios para recuperar un almacén de estado se pueden obtener del tema de entrada original.

Agregar un punto final REST a los procesadores de flujo

Con la arquitectura presentada hasta ahora, tenemos nueve tiendas de estado que se pueden usar para recuperar la última fecha de reserva de los clientes que pertenecen a la partición de tema de entrada respectiva.

Ahora, para que esta información sea accesible desde fuera de los procesadores de secuencias de Kafka, necesitamos exponer un punto final de servicio en cada una de las instancias de la aplicación de procesador de secuencias y responder a las solicitudes entrantes del almacén de estado interno administrado por secuencias de Kafka.

Como requisito adicional, no podemos esperar que la aplicación solicitante sepa qué instancia de Kafka Streams es actualmente responsable de procesar los datos de un cliente determinado. En consecuencia, cada extremo de servicio es responsable de redirigir la consulta a la instancia de aplicación correcta si los datos del cliente no están disponibles localmente.

Optamos por implementar el punto final de servicio como una API REST, que tiene la ventaja de ser accesible desde cualquier cliente que admita HTTP, y permite agregar un equilibrador de carga transparente muy fácilmente.

El objeto KafkaStreams, que está disponible en todas las aplicaciones de Kafka Streams, proporciona acceso de solo lectura a todas las tiendas de estado locales y también puede determinar la instancia de aplicación responsable de un id de cliente determinado. Cuando se utiliza este objeto para construir nuestro servicio REST, la arquitectura se ve de la siguiente manera:

HTTP cliente puede enviar solicitudes de búsqueda a los demás extremos de los procesadores de flujo. La flecha discontinua indica cómo se reenvía internamente una solicitud entre los procesadores de flujo, si no se puede responder desde una tienda estatal local.

En resumen, nuestra arquitectura propuesta hace uso de temas Kafka para almacenar de forma fiable los datos de los mensajes en reposo y mantiene una segunda representación de los datos en almacenes estatales para admitir consultas rápidas.

Garantizar la escalabilidad de la aplicación

Para obtener una aplicación escalable, debemos asegurarnos de que la carga de procesamiento esté equilibrada por igual en todas las instancias de la aplicación Kafka Streams. La carga de un procesador de flujo individual depende de la cantidad de datos y consultas que tenga que manejar.

Más específicamente, es importante elegir un esquema de particiones para el tema Kafka, de manera que la carga de mensajes y consultas entrantes esté bien equilibrada en todas las particiones y, en consecuencia, también en todos los procesadores de flujo.

Si se deben usar almacenes de estado en memoria, el número de particiones en el tema debe ser lo suficientemente grande para que cada procesador de flujo pueda mantener el volumen de datos de una partición en la memoria principal. Tenga en cuenta que en caso de conmutación por error, un procesador de flujo podría incluso necesitar mantener dos particiones en la memoria principal.

Para tener en cuenta los fallos del procesador de secuencias, se puede configurar el número de réplicas en espera utilizando la configuración num.standby.replicas en secuencias de Kafka, lo que garantiza que los procesadores de secuencias adicionales también se suscriban a los mensajes de una partición de procesador determinada. En caso de un fallo, esos procesadores pueden hacerse cargo rápidamente de responder consultas, en lugar de comenzar a reconstruir el almacén estatal solo después de que ya se haya producido un fallo.

Finalmente, el número deseado de procesadores de flujo debe ajustarse al hardware disponible. Para cada instancia de aplicación de procesador de flujo, se debe reservar al menos un núcleo de CPU. En sistemas multinúcleo, es posible aumentar el número de subprocesos de flujo por instancia de aplicación, lo que mitiga la sobrecarga incurrida al iniciar una aplicación Java separada por núcleo de CPU.