1. Apache Spark
1. Apache Spark
1. Apache Spark
Fundamentals
Apache Spark
Big Data Fundamentals
Apache Spark
Índice
Esto es, un framework que permite realizar programación distribuida, sobre un grupo
de máquinas interconectadas que se reparten el trabajo y procesan los datos
simultáneamente, aunque también podemos ejecutar Spark de manera local o en un
solo nodo.
El framework expone unas APIs para Java, Python, Scala y R, además de tener varios
proyectos relacionados, de los que hablaremos más adelante:
1.1. Necesidad
Spark surge de la necesidad de procesar un gran volumen de datos para el cual una
sola máquina no es suficiente.
• Intercambio de información entre nodos del cluster a través del envío por
red
Iteratio Iteratio
…
n1 n2
• Descubrir en qué discos de HDFS se encuentran los datos que necesita una etapa
Spark plantea una alternativa a estos accesos a disco: usar la memoria de los distintos
nodos del cluster:
Haciendo solo dos accesos a disco: al principio para leer la información, y al final de
todo el proceso, para escribir las salidas.
• Incluso habrá tareas que podremos operar de forma local, sin accesos a disco
ni conexiones con otros nodos, acelerando de nuevo.
Este enfoque ha demostrado ser muchísimo más eficiente que MapReduce. En esta
tabla vemos la comparativa entre Hadoop y Spark al realizar una regresión logística.
Salvo la primera iteración, en la que Spark hace la lectura de HDFS y la escritura a
memoria de los datos, las siguientes operaciones son dramáticamente más rápidas en
Spark, al no tener que volver a realizar ninguna acción sobre HDFS hasta la última
iteración, donde escribe los resultados finales.
Como hemos comentado, Apache Spark surge para procesar datos de forma distribuida
entre varias máquinas de un cluster. En Spark, esos datos se encuentran en un formato
llamado Redundant Distributed Dataser: RDD
El usuario debería ser capaz de definir el RDD a partir de cualquiera de estas fuentes,
incluso combinándolas en un único RDD.
Cada partición del RDD sabe cómo recrearse en caso de falla, puesto que tiene el
registro de transformaciones, o un histórico que le permite recrearse desde un
almacenamiento estable u otro RDD.
Por lo tanto, cualquier programa que use Spark tiene garantizada la tolerancia a fallos,
independientemente de la fuente de datos subyacente y del tipo de RDD.
Los RDDs son estructuras de datos inmutables (de solo lectura), por lo que las
transformaciones dan como resultado la creación de nuevos RDDs. La ejecución
diferida permite además optimizar muchísimo los pasos de transformación. Por
ejemplo, si la acción es devolver una única línea, Spark solo computa una partición e
ignora el resto de información.
even_names_rdd = clients_rdd. \
filter(lambda c: c[0] % 2 == 0). \
map(lambda c: c[1])
Filter y map son transformaciones, así que este código, que selecciona los nombres en
posición impar y genera un nuevo RDD, se ejecuta en milisegundos ya que realmente
no ejecuta nada aún (no hay acciones, sólo transformaciones). Sólo registra los pasos
en el plan de ejecución del rdd pero sin llevarlas a cabo. Cada transformación retorna
otro RDD.
even_names_count = clients_rdd. \
filter(lambda c: c[0] % 2 == 0). \
map(lambda c: c[1]). \
count()
print(even_names_count)
• Map (func)
• Filter (func)
• FlatMap (func)
Al igual que en map, pero cada elemento de entrada se puede asignar a 0 o más
elementos de salida (func debe devolver una Sec en lugar de un solo elemento).
• MapPartitions (func)
Similar a map, pero corre por separado en cada partición (bloque) del RDD, por lo que
func debe ser de tipo Iterator < T> = > Iterator < U> cuando se ejecuta en un RDD
(Redundant Distributed Dataset) de tipo T.
• MapPartitionsWithIndex (func)
Similar a mapPartitions, pero también proporciona func con un valor entero que
representa el índice de la partición, así que func debe ser de tipo (Int , Iterator < T>)
= > Iterator < U> cuando se ejecuta en un RDD de tipo T.
Muestra una fracción de los datos, con o sin sustitución, utilizando una semilla de
números aleatorios dado. Siguiendo con las operaciones de transformación, se pueden
destacar las siguientes:
• Union (otherDataset)
• Intersection (otherDataset)
• Distinct ([numTasks])
Devuelve un nuevo conjunto de datos que contiene los elementos distintos del
conjunto de datos de origen.
Cuando se llama sobre un conjunto de pares (K, V), devuelve un conjunto de datos de
pares (K, iterable < V >). Si se está agrupando a fin de realizar una agregación (como
una suma o promedio) sobre cada clave, usando reduceByKey o combineByKey, se
obtendrá mucho mejor rendimiento. Por defecto, el nivel de paralelismo en la salida
depende del número de particiones de la RDD (Redundant Distributed Dataset) padre.
Se puede pasar un argumento numTasks opcionales para establecer un número
diferente de tareas.
• ReduceByKey(func, [numTasks])
Cuando se llama sobre un conjunto de datos de pares (K, V), devuelve un conjunto de
datos de pares (K, V), donde los valores con la misma clave K de cada tupla, se aplica
una operación de reducción (por ejemplo, suma) utilizando la función func, que debe
ser del tipo (V, V) => V. Como en groupByKey, el número de reduciones en las tareas
se puede configurar a través de un segundo argumento opcional.
Cuando se llama a un conjunto de pares (K, V), devuelve un conjunto de pares (K, T),
donde los valores para cada clave se agregan utilizando las funciones dadas de
combinación y un neutral valor cero. Permite un tipo de valor agregado que es
diferente al tipo de valor de entrada, evitando al mismo tiempo las asignaciones
innecesarias. Al igual que en groupByKey, el número de tareas de reducción se pueden
configurar a través de un segundo argumento opcional.
Cuando se llama sobre un conjunto de pares (K, V), donde K establece el orden, se
devuelve un conjunto de pares (K, V) ordenados por claves en orden ascendente o
descendente, según se especifica en el argumento ascendente boolean. • Join
(otherDataset, [numTasks]) Cuando se llama sobre conjuntos de datos de tipo (k, V) y
(K, W), devuelve un conjunto de datos de pares con todos los pares de elementos para
cada tupla (K, (V, W)). Las combinaciones externas son apoyadas a través de
leftOuterJoin, rightOuterJoin y fullOuterJoin.
Cuando se invoca sobre los conjuntos de datos de tipo (K, V) y (K, W), devuelve un
conjunto de datos de pares (K, Iterable < V >, < Iterable W>). Esta operación también
se llama groupWith.
• Cartesian (otherDataset)
Cuando se invoca sobre los conjuntos de datos de los tipos T y U, devuelve un conjunto
de pares (T, U) calculado entre todos los pares de elementos.
• Repartition (numPartitions)
Reordena los datos de la RDD (Redundant Distributed Dataset) al azar para crear, ya
sea más o menos particiones, el equilibrio a través de ellas. Esto siempre baraja todos
los datos en la red.
• RepartitionAndSortWithinPartitions (partitioner)
2.4. Acciones
Una vez que se ha creado un RDD (Redundant Distributed Dataset), las diversas
transformaciones se ejecutan solo cuando se invoca una acción sobre él. El resultado
de una acción puede ser: escribir datos en el sistema de almacenamiento o, devolver
al programa del controlador el resultado final de una serie de transformaciones.
Algunas de las acciones más representativas son las siguientes. Entre paréntesis, los
parámetros de entrada que necesitan (entre corchetes si son opcionales):
• Reduce (func)
Agrega los elementos del conjunto de datos, usando una función que toma dos
argumentos y devuelve uno. Solo se aplica sobre RDDs compuestos por tipos básicos.
• Collect ()
Devuelve todos los elementos del conjunto de datos como una matriz en el programa
del controlador (driver). Se usa mejor en subconjuntos de datos suficientemente
pequeños, ya que puede llenar la memoria del controlador
• Count ()
• CountByValue ()
Devuelve el recuento de cada valor único en este RDD como un mapa local de pares
(valor, recuento).
• First ()
Devuelve una matriz con los primeros n elementos del conjunto de datos.
• SaveAsTextFile (path)
Escribe los elementos del conjunto de datos como un texto. Spark llamará a String en
cada elemento para convertirlo a una línea de texto en el archivo o archivos
resultantes. Al igual que en Hadoop, la carpeta no debe existir, de lo contrario,
obtendremos una excepción durante la ejecución de la acción.
• CountByKey()
Solo disponible en RDDs de tipo (K, V). Devuelve un hashmap de pares (K, Int) con el
recuento de cada clave.
• Foreach (func)
2.5. Caché
Otra de las operaciones clave en un procesado con Spark es el cacheado de los datos:
even_names_rdd = clients_rdd. \
filter(lambda c: c[0] % 2 == 0). \
map(lambda c: c[1])
even_names_rdd.count()
even_names_rdd.foreach(lambda c: print(c))
Clients data
Ejecución 1 Ejecución 2
Map names
count foreach(print)
even_names_rdd = clients_rdd. \
filter(lambda c: c[0] % 2 == 0). \
map(lambda c: c[1]).\
cache()
even_names_rdd.count()
even_names_rdd.foreach(lambda c: print(c))
Clients data
Map names
Ejecución 2 Ejecución 3
count foreach(print)
3. Ejecución de Spark
Pero todas estas transformaciones y acciones sobre RDDs, ¿cómo se ejecutan?
• Nodo Driver: Es el nodo del cluster que se encarga de ejecutar las acciones y
las operaciones no distribuidas del programa. De igual forma, es el encargado
de mandar la ejecución de las operaciones distribuidas (transformaciones) a los
workers cuando corresponda (es decir, cuando aparezca una acción).
En MapReduce, la unidad de computación de más alto nivel es la tarea. Una tarea carga
datos, aplica una función de map, la mezcla, aplica una función de reducción y vuelve
a escribir los datos en un almacenamiento persistente. En Spark, la unidad de
computación de más alto nivel es una aplicación. Una aplicación Spark se puede utilizar
para un único trabajo por lotes, una sesión interactiva con múltiples trabajos o un
servidor de larga duración que satisface las solicitudes continuamente. Un trabajo
Spark puede consistir en más que un solo mapa y reducir. MapReduce inicia un proceso
para cada tarea. Por el contrario, una aplicación Spark puede tener procesos que se
ejecutan en su nombre, incluso cuando no está ejecutando un trabajo. Además, se
pueden ejecutar múltiples tareas dentro del mismo ejecutor. Ambos se combinan para
permitir un tiempo de arranque de la tarea extremadamente rápido, así como el
almacenamiento de datos en la memoria, lo que da como resultado un rendimiento de
órdenes de magnitud mucho mayor que el de MapReduce.
2. Ejecutores
$ pyspark
Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 20:42:06)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
21/06/01 19:30:20 WARN Utils: Your hostname, mac-518969.local resolves
to a loopback address: 127.0.0.1; using 192.168.1.40 instead (on
interface en0)
21/06/01 19:30:20 WARN Utils: Set SPARK_LOCAL_IP if you need to bind
to another address
21/06/01 19:30:21 WARN NativeCodeLoader: Unable to load native-hadoop
library for your platform... using builtin-java classes where
applicable
Using Spark's default log4j profile: org/apache/spark/log4j-
defaults.properties
Setting default log level to "WARN".
spark-submit
--class project/main.py
--master yarn
--deploy-mode client
--py-files project.zip
project/main.py
if __name__ == '__main__':
sc = SparkContext(appName=“test”)
spark = SparkSession(sc)
...
5. Ejemplos
Veamos algunos ejemplos de código de una aplicación de Spark:
5.1. Setup
sc = SparkContext(appName=“test”)
spark = SparkSession(sc)
Seguirán con el acceso a datos. En este caso, un fichero CSV con información de varias
personas
customers_rdd = spark.sparkContext.\
textFile("/Users/xxx/Desktop/customers.csv").\
map(lambda line: line.split(","))
customers_rdd. \
map(lambda row: row[2]). \
reduce(max)
Resultado, (‘72’)
customers_rdd. \
map(lambda row: (row[3], row[2])). \
reduceByKey(max)
('F', '38')
('M', '72')
Permite operar sobre sets de datos como si de tablas sql se tratara, directamente con
código SQL:
clients_df.registerTempTable("clients")
spark. \
sql("select count(name) from clients where id % 2 = 0"). \
show()
clients_df.\
where(clients_df["id"] % 2 == 0).\
agg(count("name")). \
show()
6.2. SparkML
6.3. SparkStreaming
Procesado de datos en tiempo real con Spark (los datos se procesan a medida que van
estando disponibles, en lugar del procesado tradicional en batch, desde ficheros).