Apache Spark: qué es y cómo funciona

Apache Spark combina un sistema de computación distribuida a través de clusters de ordenadores con una manera sencilla y elegante de escribir programas. Fue creado en la Universidad de Berkeley en California y es considerado el primer software de código abierto que hace la programación distribuida realmente accesible a los científicos de datos.

Es sencillo entender Spark si lo comparamos con su predecesor, MapReduce, el cual revolucionó la manera de trabajar con grandes conjuntos de datos ofreciendo un modelo relativamente simple para escribir programas que se podían ejecutar paralelamente en cientos y miles de máquinas al mismo tiempo. Gracias a su arquitectura, MapReduce logra prácticamente una relación lineal de escalabilidad, ya que si los datos crecen es posible añadir más máquinas y tardar lo mismo.

Spark mantiene la escalabilidad lineal y la tolerancia a fallos de MapReduce, pero amplía sus bondades gracias a varias funcionalidades: DAG y RDD.

DAG (Directed Acyclic Graph)

DAG (Grafo Acíclico Dirigido) es un grafo dirigido que no tiene ciclos, es decir, para cada nodo del grafo no hay un camino directo que comience y finalice en dicho nodo. Un vértice se conecta a otro, pero nunca a si mismo.

Spark soporta el flujo de datos acíclico. Cada tarea de Spark crea un DAG de etapas de trabajo para que se ejecuten en un determinado cluster. En comparación con MapReduce, el cual crea un DAG con dos estados predefinidos (Map y Reduce), los grafos DAG creados por Spark pueden tener cualquier número de etapas. Spark con DAG es más rápido que MapReduce por el hecho de que no tiene que escribir en disco los resultados obtenidos en las etapas intermedias del grafo. MapReduce, sin embargo, debe escribir en disco los resultados entre las etapas Map y Reduce.

Gracias a una completa API, es posible programar complejos hilos de ejecución paralelos en unas pocas líneas de código.

RDD (Resilient Distributed Dataset)

Apache Spark mejora con respecto a los demás sistemas en cuanto a la computación en memoria. RDD permite a los programadores realizar operaciones sobre grandes cantidades de datos en clusters de una manera rápida y tolerante a fallos. Surge debido a que las herramientas existentes tienen problemas que hacen que se manejen los datos ineficientemente a la hora de ejecutar algoritmos iterativos y procesos de minería de datos. En ambos casos, mantener los datos en memoria puede mejorar el rendimiento considerablemente.

Una vez que los datos han sido leídos como objetos RDD en Spark, pueden realizarse diversas operaciones mediante sus APIs. Los dos tipos de operaciones que se pueden realizar son:

  • Transformaciones: tras aplicar una transformación, obtenemos un nuevo y modificado RDD basado en el original.
  • Acciones: una acción consiste simplemente en aplicar una operación sobre un RDD y obtener un valor como resultado, que dependerá del tipo de operación.

Dado que las tareas de Spark pueden necesitar realizar diversas acciones o transformaciones sobre un conjunto de datos en particular, es altamente recomendable y beneficioso en cuanto a eficiencia el almacenar RDDs en memoria para un rápido acceso a los mismos. Mediante la función cache() se almacenan los datos en memoria para que no sea necesario acceder a ellos en disco.

El almacenamiento de los datos en memoria caché hace que los algoritmos de machine learning ejecutados que realizan varias iteraciones sobre el conjunto de datos de entrenamiento sea más eficiente. Además, se pueden almacenar versiones transformadas de dichos datos.

Modelo de programación

Un programa típico se organiza de la siguiente manera:

1. A partir de una variable de entorno llamada context se crea un objeto RDD leyendo datos de fichero, bases de datos o cualquier otra fuente de información.

2. Una vez creado el RDD inicial se realizan transformaciones para crear más objetos RDD a partir del primero. Dichas transformaciones se expresan en términos de programación funcional y no eliminan el RDD original, sino que crean uno nuevo.

3. Tras realizar las acciones y transformaciones necesarias sobre los datos, los objetos RDD deben converger para crear el RDD final. Este RDD puede ser almacenado.

Un pequeño ejemplo de código en Python que cuenta el número de palabras que contiene un archivo sería el siguiente:

my_RDD = spark.textFile("hdfs://...") words = my_RDD.flatMap(lambda line : line.split(" ")) .map(lambda word : (word, 1)) .reduceByKey(lambda a, b : a + b) words.saveAsTextFile("hdfs://...")

Cuando el programa comienza su ejecución crea un grafo similar al de la figura siguiente en el que los nodos son objetos RDD y las uniones entre ellos son operaciones de transformación. El grafo de la ejecución es un DAG y, cada grafo es una unidad atómica de ejecución. En la figura siguiente, las líneas rojas representan transformación y las verdes operación.

Tipos de transformaciones

Es muy posible que los datos con los que se necesite tratar estén en diferentes objetos RDD, por lo que Spark define dos tipos de operaciones de transformación: narrow transformation y wide transformation.

  • Narrow transformation: se utiliza cuando los datos que se necesitan tratar están en la misma partición del RDD y no es necesario realizar una mezcla de dichos datos para obtenerlos todos. Algunos ejemplos son las funciones filter(), sample(), map() o flatMap().
  • Wide transformation: se utiliza cuando la lógica de la aplicación necesita datos que se encuentran en diferentes particiones de un RDD y es necesario mezclar dichas particiones para agrupar los datos necesarios en un RDD determinado. Ejemplos de wide transformation son: groupByKey() o reduceByKey().

Una representación gráfica de ambos tipos de transformaciones es la que se puede apreciar en la figura siguiente:

En algunos casos es posible realizar un reordenamiento de datos para reducir la cantidad de datos que deben ser mezclados. A continuación se muestra un ejemplo de un JOIN entre dos objetos RDD seguido de una operación de filtrado.

Por ejemplo, dados dos objetos RDD (RDD1 y RDD2), con variables ’a’ y ’b’, se va a realizar una operación de JOIN entre ambos conjuntos de datos para los casos en los que ’a’ sea mayor que 5 y ’b’ sea menor que 10:

SELECT a, b FROM RDD1 JOIN RDD2 WHERE a>5 AND b<10

Esta operación puede realizarse de dos maneras, tal y como se aprecia en la imagen de abajo. La primera opción consiste en una implementación muy simple en la que primero se realiza el JOIN entre los objetos RDD y luego se filtran los datos. Sin embargo, en la segunda opción, primero se realiza el filtrado por separado en ambos RDD y luego se hace el JOIN.

La segunda opción es más eficiente debido a que el filtrado y posterior unión de los datos se hace por separado. Podría decirse la mezcla o barajado de datos es la operación que más coste tiene, por lo que Apache Spark proporciona un mecanismo que genera un plan de ejecución a partir de un DAG que minimiza la cantidad de datos que son mezclados. El plan de ejecución es el siguiente:

1. Primero se analiza el DAG para determinar el orden de las transformaciones.

2. Con el fin de minimizar el mezclado de datos, primero se realizan las transformaciones narrow en cada RDD.

3. Finalmente se realiza la transformación wide a partir de los RDD sobre los que se han realizado las transformaciones narrow.

Conclusiones

Apache Spark es una herramienta útil y eficiente para tareas de procesamiento masivo de datos. Está en constante desarrollo y se actualiza frecuentemente. Además, su documentación es muy completa y la comunidad cada vez se hace más grande.