Introducción a Spark: Big Data para gente de Marketing (7)

Un poco de historia sobre Spark

El proyecto Spark tuvo su comienzo entre los muros de la universidad de Berkeley en California en 2009. En su concepción Spark intentaba demostrar la viabilidad de otro proyecto de la misma universidad, Mesos desarrollado por Benjamin Hindman y Matei Zaharia, entre otros.

Mesos intentaba aportar una nueva capa de gestión que permitía compartir recursos en sistemas distribuidos a través de diversos frameworks de manera eficiente sin necesidad de replicar los datos en cada uno de ellos.

En 2013 Spark empezó a convertirse en un proyecto incubado por la fundación de Software Apache (Apache Software Foundation) y a principios de 2014 se convirtió en uno de proyectos prioritarios (top-level) de la organización.

Trabajar con Datos en Streaming

Normalmente los procesos de los que venimos hablando hasta ahora hacen referencia al procesado de los datos por lotes, una traducción del inglés batch processing. En este tipo de procesos se usa como base del análisis una fuente estática de datos, es decir, mientras estamos realizando el análisis no se produce la entrada de nuevos datos que modifiquen de alguna manera el conjunto de datos con el que trabajamos.

Existen fuentes de datos que son capaces de generar grandes volúmenes de datos de manera continua como si de la corriente de un caudaloso rio se tratase, de ahí que a este tipo de datos se les denomine datos en Streaming (streaming data). Hay muchos escenarios en los que vamos a necesitar trabajar con los datos a medida que se generan. Al proceso de trabajar con los datos a medida que los recibimos se le llama stream processing o, en ocasiones, se usa simplemente streaming.

Detrás del procesado de datos en streaming está la ambición de ser capaces de hacer un tratamiento de los datos en tiempo real (real time). Es importante intentar definir qué queremos decir cuando hablamos de tiempo real, el principal parámetro que se usa para definir este concepto se basa en el intervalo de tiempo que transcurre desde la entrada del dato en el sistema hasta el momento en que somos capaces de procesarlo. Pensemos por ejemplo en el mundo de las apuestas online, en este particular sector la velocidad a la que se procesa un resultado de un evento deportivo tiene gran importancia, en este caso la velocidad a la hora de procesar ese dato es crítica para el negocio y necesitará que el proceso se realice lo más cerca posible al momento en que ocurrió el evento.

Por lo tanto, lo que significa procesar los datos en tiempo real va a significar algo diferentes para diferentes usuarios en función del contexto y de las necesidades del negocio. Por ese motivo a un sistema de este tipo se le va a pedir que nos permita ajustar el intervalo de actualización a dichas necesidades del negocio. En ocasiones el intervalo se medirá en segundos y en otras podrá extenderse a varios minutos, dependerá de la naturaleza del negocio. Este es precisamente uno de los principios fundacionales que hay detrás de Spark.

Spark permite fijar el intervalo de tiempo al que el usuario quiere procesar los datos según entran en su modelo de datos. Para ello Spark va a crear micro-lotes de los datos (micro-batch), un concepto que hace referencia a la agrupación del conjunto de datos que se han producido en un determinado intervalo de tiempo definido por el usuario.

Tenemos que tener en cuenta que, para muchos negocios, es muy posible que no sea suficiente sólo con un procesado en streaming. En algunos escenarios va a ser necesario combinar los procesados en micro-batch (micro lotes) con los datos que se han procesado por ejemplo durante los últimos 15 minutos. Un buen marco para el procesado de datos debería permitir combinar esas dos opciones y, como veremos más adelante, la librería Spark Streaming es una de las piezas del ecosistema Spark que nos permitirá, entre otras cosas, hacer precisamente eso.

Procesado In-Memory

Desde un punto de vista de Hardware hay tres componentes que influyen en la velocidad en la que podemos procesar los datos:

  • El procesador de la máquina que realiza los cálculos
  • El almacenamiento, es decir, donde guardamos los datos con los que trabajamos
  • Las tasas de trasferencia en el intercambio de datos entre los dos, el procesador y el almacenamiento.

Evidentemente el más lento, o el de capacidad más limitada, será el que lastre nuestra capacidad de procesar los datos. Normalmente la latencia en el almacenamiento de los datos, el acceso al disco duro es el principal cuello de botella. Lo que propone Spark es mover esa parte del proceso, que ocurre en el disco duro físico del nodo, a la memoria RAM del equipo, que es capaz de procesar los datos a una velocidad muy superior.

Desde su inicio Spark fue concebido para optimizar el uso de los datos in memory, lo que según algunas fuentes le permite procesar los datos hasta 100 veces más rápido que el sistema de Hadoop con MapReduce. Según un un test realizado por Intel Spark pudo reducir la latencia de almacenamiento de un millón de ciclos de CPU a una media de unos 250 ciclos, un factor de aceleración realmente impresionante.

Modelo de programación de Spark

La flexibilidad a la hora de trabajar con múltiples fuentes de datos diferentes y para conectarse con múltiples destinos donde grabar datos es uno de los motivos por los que Spark ha recibido tanta atención por parte de la comunidad de analistas de datos.

El principal motivo detrás de esta flexibilidad es que Spark trabaja con una abstracción de su modelo de datos llamada Resilient Distributed Dataset (RDD). Un RDD es, básicamente, una colección de datos distribuida a través de los nodos de nuestro cluster al más bajo nivel de abstracción disponible en Spark. El nombre se descompone en los tres elementos clave de un RDD:

  • La parte de Resilient (podría traducirse como resistente o resiliente si se prefiere), hace referencia a la capacidad de rehacerse ante la caída de un nodo dentro del cluster.
  • La parte de Distributed (Distribuido) se refiere a la capacidad de fragmentar los datos en piezas más pequeñas llamadas particiones que, una vez distribuidas a través de los nodos del cluster, pueden ser procesadas en paralelo.
  • Por último Dataset representa el conjunto de datos con el que vamos a trabajar y que en Spark puede venir en varios sabores: JSON, CSV, ficheros de texto o una base de datos sin estructura través de JDBC (Java Database Conectivity), una API que nos da la posibilidad de realizar operaciones sobre bases de datos que permitan el uso de JAVA con independencia del sistema operativo o el tipo de base de datos que haya detrás..

Como se puede observar estas son características que, con diferentes enfoques, hemos encontrado tanto en MapReduce como en YARN, y es que Spark no es sino otro giro de tuerca para la misma necesidad. Sin embargo la capacidad de trabajar con distintos modelos de datos, incluido el HDFS, es una de las señas de identidad de Spark así como una de las diferencias principales con los otros sistemas.

Hay tres caracteristicas que definen los RDDs:

  • Son inmutables, una vez que se crean no se pueden modificar. Esto proporciona las garantías que Spark necesita para procesar los datos.
  • Son distribuibles, ya que las tareas que vamos a realizar se subdividen en tareas más pequeñas que se distribuyen para que se ejecuten a través de los nodos (worker nodes). Un proceso del que se encarga totalmente Spark y que es trasparente para el usuario.
  • Viven in-memory siempre que pueden. Sólo en situaciones excepcionales se guarda parte de la información en el disco.

Transformaciones

Como ya he mencionado las estructuras de datos son inmutables, es decir, una vez creadas no se pueden cambiar, una de las características esenciales de los RDDs. Por lo que después de un proceso de transformación lo que obtendremos será una nuevo RDD diferente como el resultado del proceso de las transformaciomes. En otras palabras una transformación es una función que toma un RDD como imput y genera uno o mas RDDs como salida (output).

Estos RDDs creados se pueden guardar para usar más adelante si se quiere evitando tener que volver a procesar los datos de nuevo, lo que contribuye a optimizar los procesos dentro de Spark.

Se dice que las transformaciones son de evaluación perezosa (lazy evaluation), en referencia al hecho de que Spark esperará hasta el último momento para realizar las transformaciones. los sucesivos RDDs que hayamos ido creando con cada transformación no son más que estados intermedios de los datos que no llegan a materializarse a no ser que una acción lo demande, por lo que realmente no estamos creando copias y más copias de los datos a cada paso. Si no aplicamos una acción (más sobre las acciones a continuación) no se materializan las transformaciones (no generan nuevos datos transformados). Esto le permite a Spark poder evaluar cual es el plan de ejecución óptimo antes de ejecutar el proceso a través del Catalyst Optimizer.

El resultado en el output tras realizar las transformaciones siempre será diferente del original, puede ser más pequeño (por ejemplo al realizar un filtrado, o elegir los elementos que son distintos del set de datos), puede ser mayor (por ejemplo al realizar una unión de datos) o del mismo tamaño (por ejemplo al hacer un Map)

Algunos de las Transformaciones más comunes son:

Map(func)

Es la más sencilla de las transformaciones, sencillamente aplica la función que le hayamos pasado como parámetro a cada uno de los elementos del RDD.

Filter(fun)

Como su nombre indica se usa para filtrar los datos del RDD de entrada en base a los parámetros que hayamos definido.

flatMap(func)

Es muy parecida a la función Map pero ofrece algo más de flexibilidad ya que permite usar funciones que devuelvan una secuencia en lugar de un sólo item.

Acciones

Las acciones son operaciones que devuelven un valor al conductor (Driver). Como hemos dicho todas las transformaciones en Spark son perezosas, que quiere decir que Spark recuerda cada transformación que le hayamos aplicado a un RDD y las aplica de la forma más óptima en el momento en que llamamos a una acción. Hasta que llamamos a esa acción Spark espera para diseñar el plan de ejecución que sea más eficiente.

Tenemos la opción de persistir los resultados intermedios de cada transformación de manera que Spark no tenga que volver a computar todo el proceso cada vez que ejecutemos una acción.

Algunas de las acciones más habituales son:

Reduce(fund)

Realiza una agregación del conjunto de datos de entrada, lo que suele ser un resultado de una función Map

Collect()

Devuelve el contenido del RDD sobre el que lo llamamos de vuelta al programa conductor (driver). Habitualmente es un subconjunto de los datos de entrada que hemos transformado y filtrado aplicando alguna de las transformaciones disponibles.

Count

Como se puede suponer devolverá el número total de elementos que hay en un RDD

Take(n)

Se trata de una función muy util que permite echar una ojeada a los datos resultantes del proceso al permitir obtener los primeros n elementos del RDD.

Transformaciones y Acciones en un ejemplo

Probablemente es más sencillo entender este proceso de las transformaciones y las acciones mediante un ejemplo sencillo. Supongamos que tenemos un vector de datos en un RDD: {1,2,3,4,5} y que vamos a aplicar una función \(f(x) = x +1\) , es fácil ver que el RDD resultante de esta transformación sería {2,3,4,5,6}

Ahora vamos a aplicar una acción, concretamente la suma, donde el imput sería el RDD2: {2,3,4,5,6} y el output sería 20 (el resultado de sumar 2+3+4+5+6).

DAG (Directed Acyclic Graph)

En el proceso de las transformaciones a los datos de nuestro RDD se produce en una secuencia de pasos que en Spark se denomina Linage (linaje sería la traducción más literal aunque no se si la más ilustrativa). Se trata de una función determinista, es decir, que siempre que se ejecute sobre los mismos datos se obtendrá el mismo resultado

Se trata de un concepto importante para entender como funciona Spark al realizar un procesos de transformación y la ejecución de una acción sobre un conjunto de datos. El siguiente gráfico representa el flujo de los pasos que se suceden en uno de los procesos más sencillos que podemos usar para explicar DAG.

Veamos la secuencia de lo que representa este sencillo gráfico:

  1. Primero se produce una operación para leer una entrada de datos en HDFS (textFile)
  2. En segundo lugar se divide cada frase por palabras (flatMap)
  3. Luego se realiza una operación que recogería en modo de par clave/valor las palabras y el conteo, por ejemplo {palabra, 1} cuenta que “palabra” ha aparecido una vez.
  4. Finalmente la acción ReduceByKey sencillamente suma todas las veces que ha aparecido cada palabra. Por ejemplo si “palabra” hubiera aparecido tres veces en el paso anterior tendríamos {palabra,1}, {palabra,1} y {palabra,1} y en este paso se reduciría a {palabra,3}.
  5. En el gráfico los puntos que marcan el inicio de cada flecha indican la creación de un RDD intermedio después de cada transformación.

El término DAG, Directed Acyclic Graph, hace referencia a varios conceptos de la representación de nodos en un gráfico. En primer lugar decimos que es Dirigido (Directed) porque las flechas (edges) que unen los nodos tienen una dirección concreta. El término Acíclico (acyclic) indica que si seguimos cada flecha en el gráfico no hay posibilidad de vuelta atrás a los nodos anteriores. Podríamos decir que el gráfico muestra las dependencias de padres a hijos entre las etapas del proceso..

Si en un momento dado uno de los nodos del cluster fallase, no tendríamos más que recurrir al linaje, o más bien a la etapa del linaje en que se produjo el fallo del nodo, para recalcular todos los pasos de transformación desde los datos originales y regeneraríamos esa parte de los datos que habíamos perdido. Esta capacidad de regeneración y recuperación del estado de los datos es lo que explica la resistencia (“resilient”) a los fallos de los RDD.

Además, Spark nos permite también hacer persistente (en disco o en memoria principal) un RDD concreto para no tener que volver a computarlo cuando se necesite de nuevo, lo que nos da mucha flexibilidad en aquellos procesos en los que haya que reutilizar “componentes” en otras transformaciones.

Particiones

Para ser capaz de aprovechar los recursos de las máquinas que forman parte de nuestro cluster Spark fracciona los datos en partes más pequeñas denominadas particiones. Cuando Spark quiere realizar algún tipo de computo trabajará en cada partición en los nodos correspondientes en paralelo, a excepción de que vaya a realizar una operación de shuffle, en la que tendrán que compartir datos de distintas particiones necesariamente.

Al más bajo nivel de abstracción toda operación de datos en Spark se reducen a la utilización de los RDDs. Sin embargo, la complejidad de trabajar a este nivel ha hecho que en las últimas versiones de Spark se hayan introducido dos nuevas APIs: Los Datasets y los DataFrames

DataFrame

El API del DataFrame es muy similar a un RDD puesto que representa una colección de datos distribuidos como el RDD. El prinicipal motivo de la creación de los DataFrame era facilitar a los desarrolladores expresar lo que querían conseguir y dejar la optimización de más bajo nivel al propio Spark. Además los DataFrames permitían extender el uso de Spark a otro tipo de usuarios menos técnicos que no estaban habituados a trabajar con Java o Scala.

Un DataFrame es, sencillamente, una tabla de datos con filas y columnas. Se denomina Esquema (schema) a una lista de las columnas y a los tipos de columnas que son. Una analogía sería una hoja de cálculo con distintas columnas cada una de ellas con su respectivo nombre (o tipo). La principal diferencia con una hoja de cálculo convencional es que esta está alojada en una ubicación física de nuestro disco duro mientras que un DataFrame puede estar distribuido entre miles de nodos. Esta característica es la que nos permite exceder los limites del almacenamiento de una sóla máquina o aprovechar la capacidad de procesar los datos en paralelo en un sistema distribuido.

 

Hasta aqui esta primera y densa parte sobre Spark, en breve publicaré la segunda parte de esta serie.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *