Plataforma de procesamiento de eventos

Índice

  • Introducción
  • Arquitecturas Event-driven
  • Data Flow vs Data Store (eventos vs registros)

1. Introducción

En este artículo quiero hablar de cosas básicas en el diseño de arquitecturas basadas en eventos que sean capaces de servir los datos en tiempo real y evitar las duplicidades del dato.

En el año 2020 todavía se siguen utilizando en las grandes compañías el intercambio de ficheros en horario nocturno para la carga de datos en aplicaciones y tenerlos disponibles a primera hora de la mañana. Esta práctica viene heredada de sistemas legacy que no permiten intercambiar datos de forma más reactiva. Los casos de uso en los que el dato es relevante durante un breve periodo de tiempo, o para aquellos sistemas que necesiten los datos actualizados intradía tenemos que valorar el uso de las arquitecturas basadas en eventos.

Esta filosofía cambia el concepto de cómo diseñamos casos de uso, ya que estos se tienen que diseñar utilizando servicios básicos, desacoplados y reutilizables para ganar agilidad en la puesta en producción. En estos escenarios la transaccionalidad se pierde; más adelante veremos el porqué.

El sistema de almacenamiento escogido para los datos es importante para centralizar su consumo, dependiendo de la utilidad del dato podremos escoger sistemas más orientados a la transaccionalidad, o por el contrario más orientados al relacional. Tenemos que evitar la carga del mismo dato en diferentes sistemas para mejorar su trazabilidad y servir la misma versión a todas las aplicaciones. El consumo normalmente lo diseñamos utilizando una arquitectura de microservicios orientada a eventos, de esta manera dejaríamos en la capa de servicios la lógica de negocio necesaria para su consumo.

2. Arquitecturas event-driven

Las arquitecturas event-driven se basan en patrones de diseño de sistemas distribuidos y asíncronos; en la actualidad son muy populares porque permiten diseñar aplicaciones con una capacidad de escalado muy alta. Son arquitecturas bastante adaptables que se pueden utilizar tanto con aplicaciones complejas como con sencillas. Uno de los principios que guían el diseño es el de dividir el procesamiento y el acceso a otros sistemas en componentes sencillos y desacoplados que reciben y procesan eventos de forma asíncrona.

Los beneficios que podemos destacar son:

  • Escalabilidad elástica horizontal
  • Flexibilidad arquitectura
  • Microservicios event-driven
  • Ciclos de vida separados
  • Industrializar despliegues

Las arquitecturas basadas en eventos están divididas de dos topologías: la mediadora y la broker. La mediadora se suele utilizar cuando se necesita orquestar múltiples pasos para un mismo evento y la de broker cuando se encadenan eventos sin un mediador central. Debido a que las características de las arquitecturas y las estrategias de implementación difieren entre estas dos topologías, es importante comprender cada una para saber cuál es la más adecuada para su situación particular.

La topología de mediador es útil para el procesamiento de eventos que tienen múltiples pasos y requieren cierto nivel de orquestación para procesar el evento. Está formado principalmente por cuatro componentes:

  • colas de eventos
  • un mediador de eventos
  • canales de eventos
  • procesadores de eventos

La topología de broker difiere de la del mediador en que no existe un orquestador de eventos central; más bien, el flujo de mensajes se distribuye a través de los componentes del procesador de eventos en una cadena de forma secuencial. Los dos componentes principales son:

  • broker: colas, topic o una combinación de ambos
  • procesadores de eventos

Cuando trabajamos con arquitecturas guiadas por eventos, dado su caracter distribuido y asíncrono tenemos que entender que los patrones son complejos de implementar. Problemas típicos a los que nos tendremos que enfrentar son asegurar la disponibilidad de procesos remotos, timeouts, balanceos, reprocesos de datos, reconexiones del intermediador, etc.

Cuando diseñamos sistemas bajo este paradigma nos olvidamos de lo que eran las transacciones atómicas para un proceso de negocio. Esto se debe a que los componentes de procesamiento de eventos están bastante desacoplados y distribuidos y es muy difícil mantener una unidad de trabajo transaccional entre ellos.

Por esta razón, cuando diseñemos una aplicación teniendo en cuenta este patrón, habrá que pensar en que eventos pueden ejecutarse de forma independiente y cuales no; si existen eventos que no pueden dividir su procesamiento en dos unidades diferentes de procesamiento, quizás este no sea el patrón más adecuado para resolver el problema.

Uno de los aspectos más difíciles al utilizar este patrón es la de crear, mantener y gobernar los contratos de los componentes de procesamiento de eventos. Habría que resaltar la importancia de utilizar un formato de datos estándar y establecer una política de versiones de contratos desde el principio. Los contratos no se refieren a interfaces, sino a la entrada de los procesadores de eventos.

3. Dataflow vs datastore

Por la propia naturaleza de los datos, algunos de ellos o alguna acción derivada de ellos, solo es relevante durante un instante en el tiempo, y es ahí cuando debemos de capturarlo, procesarlo, aplicarle la lógica de negocio y desencadenar una acción. En las arquitecturas orientadas a eventos disponemos de las herramientas adecuadas para procesar los datos mientras se encuentran en movimiento. Algunos ejemplos:

  • Cancelar una transacción antes de que se produzca un fraude
  • Enviar una promoción cuando un cliente entre en una tienda
  • Mantenimiento predictivo, reemplazo de piezas antes de que se rompan
  • Información al cliente de transportes, retrasos, actualizaciones, cupones..

El evento que se encuentra en movimiento pueden interactuar con otros sistemas informacionales para enriquecerse y que vaya viajando por las distintas unidades de procesamiento. El viaje de un evento puede terminar en algún repositorio de datos y dependiendo de su naturaleza, será servido en una capa de consumo rápido o pasará a una capa de datos en reposo para consultas posteriores.

A continuación algunas de las características que debe de tener una plataforma de análisis de datos en streaming:

Para alimentar cualquier plataforma en streaming los datos tienen que ser suministrados en forma de eventos para su procesamiento en tiempo real, estos eventos también pueden ser producidos en batch. En el pasado todos los servicios estaban centralizados en sistemas de almacenamiento de datos en reposo lo que hacía imposible crear servicios ágiles y flexibles.

Un sistema central escalable y distribuido para dar un soporte al viaje de los eventos que se originan en distintas fuentes y se mueven a distintos destinos es fundamental. Una infraestructura distribuida, escalable, y construida sobre una arquitectura diseñada para que el que tiempo de inactividad sea 0, manejando los fallos de los distintos nodos y redes, y con la capacidad de mantener actualizado el software.

En la práctica los procesos de negocio pueden necesitar almacenar estados, a menudo esta necesidad se debe implementar con eventos y cambios de estado, no con llamadas a procedimientos remotos y patrones de petición/respuesta. Para ayudarnos podemos utilizar patrones como CQRS o Event Sourcing.

Después de todo lo escrito no hay que reinventar la rueda creando una plataforma de intercambio de mensajes utilizando algún sistema de mensajería tradicional, utiliza Kafka y tendrás gran parte del trabajo hecho.

Spark Streaming

Spark Streaming es una extensión de la API core de Spark que ofrece procesamiento de datos en streaming de manera escalable, alto rendimiento y tolerancia a fallos. Los datos pueden ser ingestados de diferentes fuentes como Kafka, Flume, Kinesis o sockets TCP, etc.

Los datos ingestados pueden ser procesados utilizando algoritmos complejos expresados como funciones de alto nivel como map, reduce o join. Finalmente los datos procesados se envían al sistema de ficheros, base de datos o dashboards. También se pueden aplicar algoritmos de machine learning y de grafos sobre los streams de datos.

Internamente Spark Streaming trabaja recibiendo streams de datos en vivo y los divide en batches o lotes, que son procesados por el motor de Spark para generar un stream de salida.

streaming-flowImg: spark.apache.org

Spark Streaming proporciona una abstracción de alto nivel llamada DStream, que representa un flujo continuo de streams de datos. Un DStream se puede crear ya sea a partir de flujos de datos de entrada o fuentes como Kafka, Flume y Kinesis. Internamente la representación de un DStream es una secuencia de RDDs.

Ejemplo práctico con Scala.

import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3

// Create a local StreamingContext with two working thread and batch interval of 1 second.
// The master requires 2 cores to prevent from a starvation scenario.

val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(1))

// Create a DStream that will connect to hostname:port, like localhost:9999
val lines = ssc.socketTextStream("localhost", 9999)

// Split each line into words
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)

// Print the first ten elements of each RDD generated in this DStream to the console
wordCounts.print()

ssc.start() // Start the computation
ssc.awaitTermination() // Wait for the computation to terminate+
$ nc -lk 9999
$ ./bin/run-example streaming.NetworkWordCount localhost 9999

StreamingContext: hay que inicializar este objeto como punto de entrada a toda la funcionalidad de Spark Streaming. Se puede crear pasándole una nueva configuración o utilizando un objeto SparkContext previamente creado.

  • Cuando se ha iniciado un contexto no se puede actualizar.
  • Cuando se para un contexto no se puede reiniciar.
  • Solo un StreamingContext puede estar activo en una JVM al mismo tiempo.
val conf = new SparkConf().setAppName(appName).setMaster(master)
val ssc = new StreamingContext(conf, Seconds(1))

Dstreams (Discretized Streams): es la abstracción básica proporcionada por Spark Streaming. Representa un stream continuo de datos, ya sea el flujo de entrada recibido desde una fuente, o el flujo de datos procesados de salida. La representación de un DStream es una secuencia de RDDs ordenados en el tiempo, cada uno de ellos guardando datos para un intervalo concreto.

Cualquier operación realizada sobre un DStream se traduce en una operación sobre cada uno de los RDDs que lo forman.

streaming-dstream-ops
Img: spark.apache.org

Las transformaciones que se pueden aplicar sobre un DStream son prácticamente las mismas que se pueden aplicar sobre un RDD, aunque existen tres funciones a las que vamos a prestar más atención.

UpdateStateByKey: esta operación permite mantener un estado arbitrario mientras se actualiza continuamente con nueva información. Para poder utilizarlo hay que:

  1. Definir el estado. Puede ser un tipo de dato arbitrario.
  2. Definir la función de actualización de estado. Especificar en una función como actualizar el estado utilizando el estado anterior y los nuevos valores de un stream de entrada.

En cada batch Spark aplicará la función de actualización de estado para todas las claves existentes, independientemente de si se tienen nuevos datos o no. Si la función devuelve none entonces se eliminará el par de clave-valor.

Ejemplo de una función de actualización de estado.

def updateFunction(newValues: Seq[Int], runningCount: Option[Int]): Option[Int] = {
    val newCount = ...  // add the new values with the previous running count to get the new count
    Some(newCount)
}

Y se aplica sobre un DStream que contiene el número de palabras.

val runningCounts = pairs.updateStateByKey[Int](updateFunction _)

La función de actualización se llamará para cada palabra, con los nuevos valores y los anteriores. Requiere un directorio de checkpoint.

Transform: son operaciones que permiten aplicar funciones de RDD en RDD sobre un DStream. También puede utilizarse para aplicar funciones definidas por el usuario y que no están definidas en la API DStream.

Window: las operaciones de ventana actúan sobre los datos de una duración concreta. Si necesitamos disponer de los datos de los último diez intervalos de tiempo para realizar algún cálculo podemos utilizar alguna de las operaciones de ventana.

streaming-dstream-window

Checkpoint

Una aplicación de streaming tiene que estar operativa 24/7 y por eso debe ser tolerante a fallos que no sean propios de la aplicación. Por eso Spark Streaming necesita verificar la suficiente información a un sistema de almacenamiento con tolerancia a fallos de tal manera que pueda recuperar los datos perdidos.

Fuentes

Apache Spark Streaming