Resumen Ejercicios Libro Spark
Resumen Ejercicios Libro Spark
Por:
Paula Iglesias
Junio 2021
Índice
Prólogo .....................................................................................................................................................7
Capitulo 1 .................................................................................................................................................9
Capítulo 2 ...............................................................................................................................................10
Spark’s Directories and Files ..............................................................................................................10
Spark Application Concepts ...............................................................................................................10
SparkSession ......................................................................................................................................10
Spark Jobs ..........................................................................................................................................11
Spark Stages .......................................................................................................................................11
Spark Tasks.........................................................................................................................................12
Transformations and actions .............................................................................................................12
Ejercicios libro ....................................................................................................................................14
1. (Pag 30) ......................................................................................................................................14
2. Contando M&Ms. Pag (39) .......................................................................................................14
Ejercicios extras .................................................................................................................................16
Capitulo 3 - Apache Spark’s Structured APIs .........................................................................................19
Resilient Distributed Datasets (RDD) .................................................................................................19
Intro a DataSet ...................................................................................................................................20
Intro a DataFrame ..............................................................................................................................20
The DataFrame API ............................................................................................................................21
Schemas and Creating DataFrames ...............................................................................................22
Definición de esquemas ............................................................................................................22
Columns and Expressions ..............................................................................................................23
Rows ...............................................................................................................................................24
Common DataFrame Operations ...................................................................................................24
Using DataFrameReader and DataFrameWriter .......................................................................24
Saving a DataFrame as a Parquet file or SQL table....................................................................25
Transformations and actions .....................................................................................................27
Projections and filters. ...............................................................................................................27
Renaming, adding, and dropping columns ................................................................................28
The Dataset API..................................................................................................................................28
Dataset ...........................................................................................................................................29
Typed Objects, Untyped Objects, and Generic Rows ....................................................................29
Creating Datasets...............................................................................................................................30
Dataset Operations ........................................................................................................................30
DataFrame vs Dataset ........................................................................................................................30
The Catalyst Optimizer......................................................................................................................31
Qué es Catalyst ..........................................................................................................................31
Componentes Catalyst ......................................................................................................................32
Trees ..........................................................................................................................................32
Rules...........................................................................................................................................32
Usando Catalyst en Spark SQL ..........................................................................................................32
Conclusiones......................................................................................................................................34
Resumen del tema .............................................................................................................................34
Ejercicios extra ...................................................................................................................................35
Capitulo 4 - Spark SQL and DataFrames: Introduction to Built-in Data Sources ...................................38
Using Spark SQL in Spark Applications ..............................................................................................39
Basic Query Examples ...................................................................................................................39
SQL Tables and Views ...................................................................................................................40
Managed Versus Unmanaged Tables ........................................................................................40
METHOD #1........................................................................................................................................40
METHOD #2........................................................................................................................................40
METHOD #1........................................................................................................................................41
Creating SQL Databases and Tables .............................................................................................41
Creating Views...............................................................................................................................41
Viewing the Metadata ..................................................................................................................42
Almacenamiento en caché de tablas SQL ....................................................................................42
Reading Tables into DataFrames ..................................................................................................43
Data Sources for DataFrames and SQL Tables .............................................................................43
DataFrameReader ......................................................................................................................43
DataFrameWriter .......................................................................................................................45
Parquet ..............................................................................................................................................45
Reading Parquet files into a Spark SQL table.................................................................................46
De DataFrame a Parquet ...............................................................................................................46
JSON ...................................................................................................................................................46
JSON to DataFrame ........................................................................................................................46
JSON to Spark SQL table ................................................................................................................47
DataFrames to JSON ......................................................................................................................47
CSV .....................................................................................................................................................47
CSV to DataFrame ..........................................................................................................................47
CSV to Spark SQL table...................................................................................................................48
DataFrame to CSV ..........................................................................................................................48
Avro ....................................................................................................................................................48
Avro to DataFrame.........................................................................................................................48
Avro to Spark SQL table .................................................................................................................48
DataFrames to Avro .......................................................................................................................49
ORC ....................................................................................................................................................49
Images ................................................................................................................................................49
Binary Files .........................................................................................................................................49
Resumen del tema .............................................................................................................................49
Ejercicios extra ...................................................................................................................................50
Capítulo 5 - Spark SQL and DataFrames: Interacting with External Data Sources ................................50
Spark SQL and Apache Hive ...............................................................................................................50
User-Defined Functions .................................................................................................................50
Spark SQL UDFs ..........................................................................................................................51
Evaluation order and null checking in Spark SQL ......................................................................51
Speeding up and distributing PySpark UDFs with Pandas UDFs ...............................................52
Querying with the Spark SQL Shell, Beeline, and Tableau ................................................................54
Using the Spark SQL Shell ..............................................................................................................54
Crate a table...............................................................................................................................54
Insert data into table .................................................................................................................54
Running a Spark SQL query ........................................................................................................54
Working with Beeline ....................................................................................................................54
Start the Thrift server ................................................................................................................54
Connect to the Thrift server via Beeline ....................................................................................54
Execute a Spark SQL query with Beeline ...................................................................................54
Stop the Thrift server .................................................................................................................54
Working with Tableau ....................................................................................................................54
Start the Thrift server ................................................................................................................54
Start Tableau ..............................................................................................................................54
Stop the Thrift server .................................................................................................................54
External Data Sources ........................................................................................................................54
JDBC and SQL Databases................................................................................................................54
The importance of partitioning .................................................................................................55
Higher-Order Functions in DataFrames and Spark SQL .....................................................................55
Transformando un Array ...................................................................................................................55
Built-in Functions for Complex Data Types .......................................................................................56
High-Order Functions.........................................................................................................................58
Common DataFrames and Spark SQL Operations .............................................................................59
Resumen del tema .............................................................................................................................63
Ejercicios extras .................................................................................................................................63
Capítulo 6 - Spark SQL and Datasets......................................................................................................64
Single API for Java and Scala ..............................................................................................................65
Scala Case Classes and JavaBeans for Datasets .................................................................................65
Working with Datasets ......................................................................................................................66
Creating Sample Data ........................................................................................................................66
Transforming Sample Data ................................................................................................................67
Higher-order functions and functional programming ...............................................................67
Converting DataFrames to Datasets ..........................................................................................70
Memory Management for Datasets and DataFrames .......................................................................70
Dataset Encoders ...............................................................................................................................71
Spark’s Internal Format Versus Java Object Format .................................................................71
Serialization and Deserialization (SerDe)...................................................................................72
Costs of Using Datasets .....................................................................................................................72
Strategies to Mitigate Costs...............................................................................................................73
Resumen ............................................................................................................................................74
Capítulo 7 - Optimizing and Tuning Spark Applications ........................................................................74
Optimización y ajuste de Spark para la eficiencia .............................................................................75
Viewing and Setting Apache Spark Configurations ...........................................................................75
Scaling Spark for Large Workloads ....................................................................................................75
Static versus dynamic resource allocation ................................................................................75
Configuring Spark executors’ memory and the shuffle service.................................................76
Maximizing Spark parallelism ....................................................................................................78
Almacenamiento en caché y persistencia de datos ..........................................................................79
DataFrame.cache().............................................................................................................................79
DataFrame.persist() ...........................................................................................................................80
Using Cache and Persist .....................................................................................................................81
A Family of Spark Joins .....................................................................................................................82
Broadcast Hash Join ...........................................................................................................................82
When to use a broadcast hash join ...........................................................................................83
Shuffle Sort Merge Join .....................................................................................................................84
Optimizing the shuffle sort merge join ......................................................................................86
When to use a shuffle sort merge join ......................................................................................86
Inspecting the Spark UI .....................................................................................................................86
Journey Through the Spark UI Tabs ...................................................................................................86
Prólogo
Soy una chica en prácticas que realizó este curso y fui resumiendo lo más importante. Vas a ver que
hay parrafos que dices pero esto de donde lo ha sacado, y lo que pasa es que en cierto capitulo hacia
un resumen de lo que salia y despues, si en otro capitulo volvia a salir mas informacion del mismo
tema pues volvia al resumen y rellenaba la informacion. Además he completado la información con
foros y videos de los que suelo poner el enlace.
Hay algunos párrafos que igual no estan muy bien escritos porque los traduje con traductor e igual
hay alguna información que falta porque no la vi relevante. Anyways, este resumen no está acabado
y eres libre de rellenar / modificar / borrar información a tu parecer. Que vaya bien :)
Los ejercicios que aparecen en el libro se han probado en las siguientes aplicaciones y en el lenguaje
Scala:
• IntelliJ Community: Descargar IntelliJ IDEA: el IDE de Java eficaz y ergonómico de JetBrains
Hay que descargar la versión community (botón negro). Es una aplicación de escritorio donde puedes
tener más organizados por carpetas los distintos ejercicios:
1 Spark usa los Notebooks como zonas donde escribir los distintos comandos
Figura 2: Vista de un proyecto Spark en IntelliJ
Capitulo 1
Introducción a Spark. No lo resumo
Capítulo 2
Spark’s Directories and Files
Estructura del directorio de Spark:
README.md - Este archivo contiene nuevas instrucciones detalladas sobre cómo utilizar los shells de
Spark, construir Spark desde el código fuente, ejecutar ejemplos autónomos de Spark, examinar los
enlaces a la documentación de Spark y las guías de configuración, y contribuir a Spark.
bin - Este directorio, como su nombre indica, contiene la mayoría de los scripts que emplearás para
interactuar con Spark, incluyendo los shells de Spark (spark-sql, pyspark, spark-shell y sparkR).
Utilizaremos estos shells y ejecutables en este directorio más adelante en este capítulo para enviar
una aplicación Spark independiente utilizando spark-submit, y escribir un script que construye y
empuja imágenes Docker cuando se ejecuta Spark con soporte Kubernetes.
sbin - La mayoría de los scripts de este directorio son de carácter administrativo, para iniciar y detener
los componentes de Spark en el clúster en sus distintos modos de despliegue.
kubernetes - Desde el lanzamiento de Spark 2.4, este directorio contiene archivos Docker para crear
imágenes Docker para su distribución de Spark en un clúster Kubernetes. También contiene un archivo
que proporciona instrucciones sobre cómo construir la distribución de Spark antes de construir sus
imágenes Docker.
data - Este directorio está lleno de archivos *.txt que sirven de entrada para los componentes de
Spark: MLlib, Structured Streaming y GraphX.
examples - Para cualquier desarrollador, dos imperativos que facilitan el camino hacia el aprendizaje
de cualquier nueva plataforma son un montón de ejemplos de código "cómo hacer" y una
documentación completa. Spark proporciona ejemplos para Java, Python, R y Scala, y querrás
emplearlos cuando aprendas el framework. Haremos alusión a algunos de estos ejemplos en este
capítulo y en los siguientes.
Provee un punto de entrada unificado para programar Spark con las APIs estructuradas.
Es el objeto principal o la base a partir de la cual cuelga toda la funcionalidad de Apache Spark. Solo
hay que importar la clase y crear una instancia en el código.
Es similar al SparkContext de los RDD, pero en este caso, para trabajar con SparkSQL, DataFrame y
DataSet se usa la SparkSession.
//Constructor
val spark = SparkSession.builder()
Spark Jobs
Para entender los siguientes apartados mejor te recomiendo este video:
Por cada grupo de trabajo/ conjunto de tareas el Driver divide el trabajo en grupos mas pequeños de
trabajo llamados Jobs
Spark Stages
Cada Job está dividido en Stages que engloban tareas del mismo estilo
Ejemplo de Stages:
Imagen extraida de: (11) Apache Spark internals: stages and tasks - YouTube
Spark Tasks
Cada Stage está dividido en tareas
Todas las transformaciones son evaluadas vagamente. Esto significa que los resultados no son
calculados inmediatamente pero son guardados como lineage.
La evaluación perezosa es la estrategia de Spark para retrasar la ejecución hasta que se invoque una
acción o se "toquen" los datos (se lean o se escriban en el disco)
Mientras que la evaluación perezosa permite que Spark optimice sus consultas al ojear a sus
transformaciones encadenadas, el linaje y la inmutabilidad de los datos proporcionan tolerancia a los
fallos.
Como Spark registra cada transformación en su linaje y los DataFrames son inmutables entre
transformaciones, puede reproducir su estado original simplemente simplemente reproduciendo el
linaje registrado, lo que le da resiliencia en caso de fallos.
Actions
Una vez que los datos se encuentran en el formato que necesitaba para llevar acabo el análisis de
palabras, llegamos al punto de obtener los resultados
Nada en un plan de consulta se ejecuta hasta que se invoca una acción. La acción es lo que
desencadena la ejecución de todas las transformaciones registradas como parte del plan de ejecución
de la consulta.
Código:
// In Scala
import org.apache.spark.sql.functions._
filtered.count()
res5: Long = 20
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions._
/**
*/
object MnMcount {
.builder
.appName("MnMCount")
.getOrCreate()
if (args.length < 1) {
sys.exit(1)
}
// Get the M&M data set filename
.option("header", "true")
.option("inferSchema", "true")
.load(mnmFile)
.groupBy("State", "Color")
.agg(count("Count").alias("Total"))
.orderBy(desc("Total"))
// Show the resulting aggregations for all the states and colors
countMnMDF.show(60)
println()
.groupBy("State", "Color")
.agg(count("Count").alias("Total"))
.orderBy(desc("Total"))
caCountMnMDF.show(10)
spark.stop()
}
MnMcount.main(Array("/FileStore/tables/mnm_dataset_csv-1.txt"))
Ejecutar método:
1a consulta
2a consulta
Ejercicios extras
Aplicar no solo count (para obtener el número de líneas) y show sino probar distintas sobrecargas del
método show (con/sin truncate, indicando/sin indicar num de filas, etc) así como también los
métodos, head, take, first (diferencias entre estos 3?)
import org.apache.spark.sql.functions._
i. Otras operaciones de agregación como el Max con otro tipo de ordenamiento (descendiente).
ii. hacer un ejercicio como el “where” de CA que aparece en el libro pero indicando más opciones de
estados (p.e. NV, TX, CA, CO).
.where(col("State") === "CA" => .where(col("State") === "CA" || col("State") === "NV")
iii. Hacer un ejercicio donde se calculen en una misma operación el Max, Min, Avg, Count. Revisar el
API (documentación) donde encontrarán este ejemplo: ds.agg(max($"age"), avg($"salary"))
ds.groupBy().agg(max($"age"), avg($"salary")) NOTA: $ es un alias de col()
caMax.show
iv. Hacer también ejercicios en SQL creando tmpView
caCountMnMDF.agg(min("Count"),max("Count"),avg("Count"),sum("Count")).createOrReplaceTemp
View("people")
sqlDF.show()
Capitulo 3 - Apache Spark’s Structured APIs
Resilient Distributed Datasets (RDD)
Es una colección de elementos tolerante a fallos que es capaz de operar en paralelo
RDD es la abstracción más básica en Spark. Es lenguaje de bajo nivel. están particionados en los nodos
del cluster. Se suelen crear a partir de un fichero del HDFS, Usan la evaluación perezosa (todas las
transformaciones que hagamos a los RDD se van a ir almacenando en un grafo aciclico dirijido (DAG)
y no se van a resolver hasta que no quede mas remedio)
• Dependencias
En primer lugar, se requiere una lista de dependencias que indique a Spark cómo se construye un RDD
con sus entradas. Cuando sea necesario para reproducir resultados, Spark puede recrear un RDD a
partir de estas dependencias y replicar operaciones en él. Esta característica da a los RDDs resiliencia
En segundo lugar, las particiones proporcionan a Spark la capacidad de dividir el trabajo para
paralelizar el cálculo en las particiones entre los ejecutores. En algunos casos -por ejemplo, la lectura
de HDFS, Spark utilizará la información de localización para enviar el trabajo a los ejecutores cercanos
a los datos. datos. De este modo, se transmiten menos datos por la red.
Y finalmente, un RDD tiene una función de cálculo que produce un Iterator[T] para los datos que se
almacenarán en el RDD
//Ejemplo:
// sc=DpstkContext()
cadenasRDD.collect()
DSL (Domain Specific Languaje) - En la API de Spark se puede usar Java, Python, Spark, R, and SQL
Cuando usar RDD:
Para cambiar entre DataFrames o Datasets y RDDs se utiliza una simple llamada al método de la API,
df.rdd.
Intro a DataSet
Un DataSet es una colección de datos distribuidos que tienen ya una estructura, a diferencia de los
RDD, que son conjuntos de datos desestructurados.
Crear un Dataset:
Intro a DataFrame
Un DataFrame es un DataSet que a la vez está organizado en columnas.
Ejemplos:
Ej1. (Pag 46) Agregar todas las edades de cada nombre, agruparlas por nombre, y luego promediar las
edades
Los DataFrames son inmutables y Spark mantiene un linaje de todas las transformaciones. Puede
añadir o cambiar los nombres y tipos de datos de las columnas, creando nuevos DataFrames mientras
se conservan las versiones anteriores. Una columna con nombre en un DataFrame y su tipo de datos
asociado a Spark asociado pueden ser declarados en el esquema.
Definición de esquemas
Spark te permite definir un esquema de dos formas:
• Definiendolo programandolo
• Usando un Lenguaje de Definicion de Datos (DDL) (Scala, Python, ...), que es mucho más simple
y fácil de leer.
Ejemplo: Definir un esquema programando para un DataFrame con tres columnas nombradas: autor
titulo y pages.
Leyendo de un JSON:
Columns and Expressions
The spark.read.csv() function reads in the CSV file and returns a DataFrame of rows and named
columns with the types dictated in the schema
If you don’t want to specify the schema, Spark can infer schema from a sample at a lesser cost.
El metodo write tiene muchas ociones para escribir en ficheros , pero el principal es el save()
census_df.write.save("hdfs://…/some_parent_dir/another_directory")
Writing DataFrames to HDFS como ficheros Parquet
census_df.write.parquet("hdfs://…/spark_output/parquet_dir")
La otra forma de escribir un DataFrame como ficheros Parquet es usando el Save() y el format()
Parquet is a columnar format that is supported by many other data processing systems:
.option("compression", "gzip")
.mode("overwrite")
Partición
En Spark, los datos de un DataFrame se mantienen en la memoria y si el tamaño de los
datos excede la cantidad de memoria disponible, entonces los datos se derraman en la
unidad de disco local de los nodos trabajadores del clúster. Sin embargo, este espacio de
disco local no es el mismo que el de HDFS. Dado que Spark está diseñado para ejecutarse
en un sistema distribuido de máquinas, los datos de un DataFrame se dividen en piezas
más pequeñas y se distribuyen en la memoria y/o el almacenamiento local de los nodos
trabajadores. En este sentido, un DataFrame de Spark se divide lógicamente, es decir, se
particiona. Así, una partición es una colección atómica de subconjuntos de datos de un
DataFrame que reside en la memoria y/o en el disco local de un nodo trabajador. El
propósito de las particiones del DataFrame es que los datos divididos en trozos más
pequeños puedan tener cálculos ejecutados en paralelo. Esto acelera enormemente los
cálculos.
coalesce utiliza las particiones existentes para minimizar la cantidad de datos que se
barajan.
repartition crea nuevas particiones y hace un barajado completo.
coalesce da como resultado particiones con diferentes cantidades de datos (a veces
particiones que tienen tamaños muy diferentes) y repartition da como resultado
particiones de tamaño más o menos igual.
Introducción:
// In Scala it would be similar
Array(
... ...
val sfFireFile="/databricks-datasets/learning-spark-v2/sf-fire/sf-fire-calls.csv"
Fuente: (15) The Parquet Format and Performance Optimization Opportunities Boudewijn Braams
(Databricks) - YouTube
spark.sql.functions has a set of to/from date/time‐ stamp functions such as to_timestamp() and
to_date() to convert to more usble format:
1.Convert the existing column’s data type from string to a Spark-supported timestamp.
2. Use the new format specified in the format string "MM/dd/yyyy" or "MM/dd/yyyy hh:mm:ss a"
where appropriate.
3. After converting to the new data type, drop() the old column and append the new one specified in
the first argument to the withColumn() method.
Dataframe
Conceptualmente, puedes pensar en un DataFrame en Scala como un alias para una colección de
objetos genéricos, Dataset[Row], donde un Row es un objeto JVM genérico no tipado que puede
contener diferentes tipos de campos. Un Dataset, por el contrario, es una colección de objetos JVM
fuertemente tipados en Scala o una clase en Java. O, como dice la documentación de Dataset, un
Dataset es: una colección fuertemente tipada de objetos específicos del dominio que pueden ser
transformados en paralelo usando operaciones funcionales o relacionales. Cada Dataset [en Scala]
también tiene una vista unitaria llamada DataFrame, que es un Dataset de Row.
Row is a generic object type in Spark, holding a collection of mixed types that can be accessed using
an index.
For example, an Int as one of your fields in a Row will be mapped or converted to IntegerType or
IntegerType() respectively for Scala or Java and Python:
Creating Datasets
As with creating DataFrames from data sources, when creating a Dataset you have to know the
schema. In other words, you need to know the data types. Although with JSON and CSV data it’s
possible to infer the schema, for large data sets this is resource-intensive (expensive). When creating
a Dataset in Scala, the easiest way to specify the schema for the resulting Dataset is to use a case class.
Cuando quieras instanciar tu propio objeto específico del dominio como un Dataset, puedes hacerlo
definiendo una clase case en Scala. Como ejemplo, veamos una colección de lecturas de dispositivos
del Internet de las Cosas (IoT) en un archivo JSON (utilizamos este archivo en el ejemplo de extremo a
extremo más adelante en esta sección).
Dataset Operations
Just as you can perform transformations and actions on DataFrames, so you can with Datasets.
Depending on the kind of operation, the results will vary:
In this query, we used a function as an argument to the Dataset method filter(). This is an overloaded
method with many signatures.
DataFrame vs Dataset
A estas alturas te estarás preguntando por qué y cuándo debes usar DataFrames o Data sets. En
muchos casos, cualquiera de los dos sirve, dependiendo de los lenguajes con los que se trabaje, pero
hay algunas situaciones en las que es preferible uno u otro. He aquí algunos ejemplos:
DataFrame Dataset
Decirle a Spark qué hacer, no cómo hacerlo
Semántica rica, abstracciones de alto nivel y operadores DSL
Si su procesamiento exige expresiones de alto nivel, filtros, mapas,
agregaciones, cálculo de medias o sumas, consultas SQL, acceso a
columnas o uso de operadores relacionales en datos semiestructurados
Si su procesamiento requiere transformaciones relacionales similares a
las consultas SQL
Desea unificar, optimizar el código (Catalyst optimizer) y simplificar las
API en todos los componentes de Spark
Si eres un usuario de R
Si eres un usuario de Python (baja a RDDs si necesitas más control)
Si quieres eficiencia de espacio y velocidad
Si quieres una seguridad de tipos estricta en tiempo de compilación y no
te importa crear múltiples case classes para un Dataset[T] específico
Si desea aprovechar y beneficiarse de la eficiente serialización de
Tungsten (mejor eficiente en memoria) con codificadores
Spark SQL es un módulo de Apache Spark para el procesamiento de datos estructurados. Una de las
grandes diferencias respecto a la Spark API RDD es que sus interfaces proporcionan información
adicional para realizar procesos más eficientes. Esta información además es útil para que Spark SQL
se beneficie internamente del uso de su optimizador Catalyst y mejorar el rendimiento en el
procesamiento de datos.
La mayor abstracción en la API de Spark SQL es el DataFrame. En Spark, un DataFrame es una colección
distribuida de datos organizada en filas con el mismo esquema. Conceptualmente es equivalente a
una tabla en una base de datos relacional. Los DataFrames en Spark tienen las mismas capacidades
que los RDDs, como por ejemplo su inmutabilidad, en memoria, resilientes, computación distribuida.
Además, le aplica una estructura llamada esquema a los datos. La mayor diferencia entre los
DataFrames y los RDDs es que los DataFrames, al tener más información sobre la estructura de los
datos, permite un mayor nivel de abstracción y técnicas de optimización que no serían posibles con
los RDDs. A partir de la versión de Spark 2.0, se fusionaron en una sola API los DataFrames y Datasets,
unificando así sus capacidades en el procesamiento de datos a través de sus librerías.
Qué es Catalyst
Spark SQL fue diseñado con un optimizador llamado Catalyst basado en la programación funcional de
Scala. Sus dos propósitos principales son: primero, añadir nuevas técnicas de optimización para
resolver algunos problemas con “big data” y segundo, permitir a los desarrolladores ampliar y
personalizarnm las funciones del optimizador.
Componentes Catalyst
Los componentes principales del optimizador de Catalyst son los siguientes:
Trees
El tipo de datos principal en Catalyst son los tree. Cada tree está compuesto por nodes, y cada node
tiene un nodetype y cero o más hijos. Estos objetos son inmutables y pueden ser manipulados con
lenguaje funcional.
Rules
Los trees pueden ser manipulados usando reglas, que son funciones de un tree a otro tree. El
método de transformación aplica la función pattern matching recursivamente en todos los nodos del
tree transformando cada patrón al resultado. A continuación se muestra un ejemplo de regla
aplicada a un tree.
El optimizador Catalyst toma una consulta computacional y la convierte en un plan de ejecución. Pasa
por cuatro fases de transformación:
1. Análisis
2. Optimización lógica
3. Planificación física
4. Generación de código
Por ejemplo, considere una de las consultas de nuestro ejemplo de M&Ms en el capítulo 2.
Los dos bloques de código de ejemplo siguientes pasarán por el mismo proceso, terminando
eventualmente con un plan de consulta similar y un bytecode idéntico para su ejecución. Es decir,
independientemente del lenguaje que utilices, tu computación pasa por el mismo camino y el
bytecode resultante es probablemente el mismo:
Plan de Optimizacion/Optimization
Para ver las diferentes etapas por las que pasa el código Python, puedes utilizar el método
count_mnm_df.explain(True) en el DataFrame. O, para ver los diferentes planes diferentes planes
lógicos y físicos, en Scala puedes llamar a df.queryExecution.logical o
df.queryExecution.optimizedPlan. (En el Capítulo 7, hablaremos más sobre ajuste y depuración de
Spark y cómo leer los planes de consulta).
El motor Spark SQL comienza generando un árbol de sintaxis abstracta (AST) para la consulta SQL o
DataFrame. En esta fase inicial, cualquier columna o nombre de tabla se resolverá consultando un
Catálogo interno, una interfaz programática de Spark SQL que contiene una lista de nombres de
columnas, tipos de datos, funciones, tablas, bases de datos, etc. Una vez que se han resueltos con
éxito, la consulta pasa a la siguiente fase.
Como muestra la Figura primera, esta fase comprende dos etapas internas. Aplicando un enfoque de
optimización basado en reglas estándar, el optimizador de Catalyst construirá primero un conjunto de
múltiples planes y, a continuación, utilizando su optimizador basado en costes (CBO), asignará costes
a cada plan. Estos planes se presentan como árboles de operadores (como en la Figura 3-5); pueden
incluir, por ejemplo, el proceso de plegado de constantes, pushdown de predicados, poda de
proyecciones, simplificación de expresiones booleanas, etc. Este plan lógico es la entrada en el plan
físico.
En esta fase, Spark SQL genera un plan físico óptimo para el plan lógico seleccionado, utilizando
operadores físicos que coinciden con los disponibles en el motor de ejecución de Spark. El plan a
ejecutarse se selecciona usando el modelo basado en costes (comparación entre costes de los
modelos).
La fase final de la optimización de la consulta consiste en generar un bytecode Java eficiente para
ejecutar en cada máquina. Dado que Spark SQL puede operar sobre conjuntos de datos cargados en
memoria,
Spark puede utilizar tecnología de compilación de última generación para la generación de código para
acelerar la ejecución. En otras palabras, actúa como un compilador. El proyecto Tungsten, que facilita
la la generación de código en toda la etapa, desempeña un papel importante.
¿Qué es la generación de código en toda la fase? Es una fase de optimización de la consulta física que
reduce toda la consulta a una sola función, eliminando las llamadas a funciones virtuales y empleando
los registros de la CPU para los datos intermedios. La segunda generación del motor Tungsten,
introducido en Spark 2.0, utiliza este enfoque para generar un código RDD compacto para la ejecución
final. Esta estrategia racionalizada mejora significativamente la eficiencia y el rendimiento de la CPU.
Conclusiones
El optimizador Catalyst de Spark SQL mejora la productividad de los desarrolladores y el rendimiento
de las consultas que escriben. Catalyst transforma automáticamente las consultas relacionales para
ejecutarlas más eficientemente usando técnicas como filtrados, índices y asegurando que los joins
de los orígenes de datos se realizan en el orden más eficiente. Además, su diseño permite a la
comunidad de Spark implementar y extender el optimizador con nuevas funcionalidades.
A través de operaciones de datos comunes ilustrativas y ejemplos de código, demostramos que las
APIs de alto nivel DataFrame y Dataset son mucho más expresivas e intuitivas que la API RDD de bajo
nivel. Diseñadas para facilitar el procesamiento de grandes conjuntos de datos. Las APIs
estructuradas proporcionan operadores específicos del dominio para operaciones de datos
comunes, aumentando la claridad y la expresividad de su código.
Exploramos cuándo utilizar los RDD, los DataFrames y los Datasets, en función de los escenarios de
su caso de uso. de los casos de uso.
Y finalmente, echamos un vistazo bajo el capó para ver cómo los principales componentes del motor
Spark SQL -el optimizador Catalyst y el Proyecto Tungsten- soportan las APIs estructuradas de alto
nivel y los operadores DSL. Como has visto, no importa cuál de los lenguajes soportados por Spark se
utilice, una consulta de Spark se somete al mismo viaje de optimización, desde la construcción del
plan lógico y físico hasta el plan final.
Desde la construcción del plan lógico y físico hasta la generación final de código compacto.
Los conceptos y ejemplos de código de este capítulo han sentado las bases para los los dos próximos
capítulos, en los que ilustraremos la interoperabilidad sin fisuras entre DataFrames, Datasets y Spark
SQL.
Ejercicios extra
b. Leer el CSV del ejemplo del cap2 y obtener la estructura del schema dado por defecto.
Resultado:
c. Cuando se define un schema al definir un campo por ejemplo StructField('Delay', FloatType(), True)
¿qué significa el último parámetro Boolean?
dataframe=dataset[Row]
e. Utilizando el mismo ejemplo utilizado en el capítulo para guardar en parquet y guardar los datos en
los formatos:
Parquet
// In Scala to save as a table
fireDF.write.format("parquet").saveAsTable(parquetTable)
otra forma
import org.apache.spark.sql.SparkSession
.builder
.appName("MnMCount")
.getOrCreate()
("Michael ","Rose","","40288","M",4000),
("Robert ","","Williams","42114","M",4000),
("Maria ","Anne","Jones","39192","F",4000),
("Jen","Mary","Brown","","F",-1))
import spark.sqlContext.implicits._
val df = data.toDF(columns:_*)
df.write.format("parquet").save("/tmp/data/example_cap3.parquet")
i. JSON
ii. CSV (dándole otro nombre para evitar sobrescribir el fichero origen)
iii. AVRO
f. Revisar al guardar los ficheros (p.e. json, csv, etc) el número de ficheros creados, revisar su contenido
para comprender (constatar) como se guardan.
JSON
Part0:
i. ¿A qué se debe que hayan más de un fichero?
df.rdd.getNumPartitions()
numberDF.rdd.partitions.size
coalesce uses existing partitions to minimize the amount of data that's shuffled.
coalesce results in partitions with different amounts of data (sometimes partitions that have
much different sizes) and repartition results in roughly equal sized partitions.
.coalesce(1)
.repartition(1)
iv. Llevar a cabo el ejemplo modificando el número de particiones a 1 y revisar de nuevo el/los ficheros
guardados
df.coalesce(1).write.format("csv").save("/Filestore/tables/example_cap3_coalesce.csv")
Estas consultas sql ejecutadas de esta manera devuelven un DataFrame donde puedes realizar más
consultas Spark
De froma similar a la API DataFrame la interfaz te permite consultar datos estructurados en tus
aplicaciones Spark
Para una tabla gestionada (managed or internal), Spark gestiona la estructura de la tabla y tanto los
metadatos como los datos en el almacén de archivos, que puede ser un sistema de archivos local,
HDFS, o un almacén de objetos como Amazon S3 o Azure Blob. Si intenta escribir SQL en esta tabla,
obtendrá un error de que la tabla o la columna no existen.
Si importas un archivo csv para construir una tabla de referencia, automáticamente se construirá
una tabla gestionada...
Con una managed table - un comando sql como DROP TABLE borra los metadatos y los datos
METHOD #1
df_final.write.mode("overwrite").saveAsTable("salesTable_manag1")
METHOD #2
%sql,
o
Con una unmanaged - el mismo comando borraria solo metadatos
METHOD #1
To create an unmanaged (external) table you can simply specify a path before the saveAsTable()
method:
METHOD #2
As similar result can be obtained by specifying the location as part of a SQL query.
%sql,
LOCATION '/FileStore/tables/salesTable_unmanag2' AS
Creating Views
Además de crear tablas, Spark puede crear vistas sobre tablas existentes.
Las vistas pueden ser:
• Globales (visibles para todas las Spark Sessions o para un cluster dado)
• Orientadas a sesiones (visibles para una única SparkSession), y son temporales: desaparecen
después de que termine la aplicación de Spark.
Crear vistas tiene una sintaxis similar a crear tablas en una base de datos . Cuando creas una vista, la
puedes consultar como si fuera una tabla. La diferencia entre una vista y una tabla es que las vistas no
mantienen los datos, las tablas persisten despues de que la aplicación de Spark termine, pero las vistas
desaparecen
En una aplicación Spark, después de crear la SparkSession variable, puedes acceder a todos los
metadatos guardados con metodos como estos:
En Spark 3.0, además de otras opciones, puedes especificar una tabla como LAZY, es decir que sólo
debe ser almacenada en caché cuando se utiliza por primera vez en lugar de inmediatamente
Va mas rapido
Reading Tables into DataFrames
A menudo, los ingenieros de datos construyen pipelines de datos como parte de sus procesos
regulares de ingestión de datos y ETL. Llenan las bases de datos y las tablas de Spark SQL con datos
limpios para para su consumo por parte de las aplicaciones.
Supongamos que tiene una base de datos existente, learn_spark_db, y una tabla, us_delay_flights_tbl,
lista para su uso. En lugar de leer desde un archivo JSON externo, puedes utilizar simplemente SQL
para consultar la tabla y asignar el resultado devuelto a un DataFrame:
Now you have a cleansed DataFrame read from an existing Spark SQL table. You can also read data in
other formats using Spark’s built-in data sources, giving you the flex‐ ibility to interact with various
common file formats.
DataFrameReader.format(args).option("key", "value").schema(args).load()
Este patrón de encadenamiento de métodos es común en Spark, y fácil de leer. Lo vimos en el Capítulo
3 cuando exploramos patrones comunes de análisis de datos.
Ten en cuenta que sólo puedes acceder a un DataFrameReader a través de una instancia de
SparkSession.
Es decir, no puedes crear una instancia de DataFrameReader. Para obtener una instancia del mismo,
utiliza:
SparkSession.read // or SparkSession.readStream
Mientras que read devuelve un handle a DataFrameReader para leer en un DataFrame desde una
fuente de datos estática, readStream devuelve una instancia para leer desde una fuente de streaming.
Los argumentos de cada uno de los métodos públicos de DataFrameReader toman diferentes valores:
Importante, aprendete esas y las siguientes opciones de lectura de datos
.option("delimiter", "\t")
.option("mode", "PERMISSIVE")
.option("compression", "snappy")
.option("emptyValue", 0)
.option("pathGlobFilter", "*.jpg")
.option("recursiveFileLookup", "true")
.option("url", "jdbc:postgresql:[DBSERVER]")
.option("dbtable", "[SCHEMA].[TABLENAME]")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "[USERNAME]")
.option("password", "[PASSWORD]")
Ejemplo de combinación:
DataFrameWriter
DataFrameWriter hace lo contrario de su homólogo: guarda o escribe datos en una fuente de datos
incorporada especificada. A diferencia del DataFrameReader, no se accede a su instancia desde una
SparkSession, sino desde el DataFrame que se desea guardar.
DataFrameWriter.format(args).option(args).bucketBy(args).partitionBy(args)
.save(path) // bucketBy = orderby
DataFrameWriter.format(args).option(args).sortBy(args).saveAsTable(table)
Parquet
Parquet es un formato de archivo columnar de código abierto que ofrece muchas optimizaciones de
E/S (como la compresión, que ahorra espacio de almacenamiento y permite un rápido acceso a las
columnas de datos).
Debido a su eficacia y a estas optimizaciones, le recomendamos que después de haber transformado
y limpiado sus datos, guarde sus DataFrames en el formato Parquet para su consumo posterior.
Parquet files are stored in a directory structure that contains the data files, metadata, a number of
compressed files, and some status files.
To read Parquet files into a DataFrame, you simply specify the format and path:
De DataFrame a Parquet
Recall that Parquet is the default file format. If you don’t include the format() method, the DataFrame
will still be saved as a Parquet file.
JSON
La notación de objetos de JavaScript (JSON) también es un formato de datos muy popular. Saltó a la
fama por ser un formato fácil de leer y de descifrar en comparación con XML. Tiene dos formatos de
representación: el modo de una línea y el modo de varias líneas. Ambos modos son compatibles con
Spark.
En el modo de línea única, cada línea denota un único objeto JSON, mientras que en el modo multilínea
todo el objeto constituye un único objeto JSON. Para leer en este modo, establezca multiLine como
verdadero en el método option()
JSON to DataFrame
JSON to Spark SQL table
DataFrames to JSON
CSV
Tan utilizado como los archivos de texto plano, este formato de archivo de texto común captura cada
dato o campo delimitado por una coma; cada línea con campos separados por comas representa un
registro. Aunque la coma es el separador por defecto, puede utilizar otros delimitadores
para separar los campos en los casos en que las comas formen parte de sus datos. Las hojas de cálculo
más populares pueden generar archivos CSV, por lo que es un formato muy popular entre los analistas
de datos y de negocios.
CSV to DataFrame
CSV to Spark SQL table
DataFrame to CSV
Avro
Se introdujo en Spark 2.4 con una fuente de datos incorporada. Es usado por ejemplo por Apache
Kafka para serialización y deserialización de mensajes. Ofrece muchos beneficios, incluyendo mapeo
directo a JSON, velocidad y eficacia y fijaciones disponibles para muchos lenguajes de programación
Avro to DataFrame
Leer un fichero Avro a un DataFrame usando DataFrameReader es consistente en uso con las otras
fuentes de datos
ORC
Como formato de archivo columnar optimizado adicional, Spark 2.x admite un lector vectorizado de
ORC vectorizado. Dos configuraciones de Spark dictan qué implementación de ORC utilizar.
lee bloques de filas (a menudo 1.024 por bloque) en lugar de una fila cada vez, lo que agiliza las
operaciones y reduce el uso de la CPU para operaciones intensivas como escaneos, filtros,
agregaciones y uniones. y uniones.
Para las tablas Hive ORC SerDe (serialización y deserialización) creadas con el comando SQL SQL USING
HIVE OPTIONS (fileFormat 'ORC'), se utiliza el lector vectorizado cuando el parámetro de configuración
de Spark spark.sql.hive.convertMetastoreOrc está se establece en true.
ORC a DataFrame
DataFrames to ORC
Images
Binary Files
Resumen del tema
Para recapitular, este capítulo exploró la interoperabilidad entre la API de DataFrame y Spark SQL.
En particular, has visto cómo utilizar Spark SQL para:
• Crear tablas gestionadas y no gestionadas utilizando Spark SQL y la API del DataFrame API.
• Leer y escribir en varias fuentes de datos y formatos de archivo incorporados.
• Emplear la interfaz programática spark.sql para emitir consultas SQL sobre datos
estructurados almacenados como Spark SQL.
• Examinar el catálogo de Spark para inspeccionar los metadatos asociados a las tablas y
vistas.
A través de los fragmentos de código en el capítulo y los cuadernos disponibles en el GitHub del
libro, tienes una idea de cómo utilizar DataFrames y Spark SQL. Siguiendo con el siguiente capítulo
explora más a fondo cómo Spark interactúa con las fuentes de datos externas. Verá algunos
ejemplos más detallados de transformaciones y la interoperabilidad entre la API de DataFrame y
Spark SQL
Ejercicios extra
a). Realizar todos los ejercicios propuestos de libro
b) GlobalTempView vs TempView
La global se puede acceder desde todas las SparkSession creadas, sin el global solo con la que la hayas
creado
Spark SQL permite a los programadores de Spark aprovechar las ventajas de un rendimiento más
rápido y programación relacional (por ejemplo, consultas declarativas y almacenamiento optimizado),
así como llamar a bibliotecas de análisis complejas (por ejemplo, aprendizaje automático). Como se
ha comentado en el capítulo anterior, a partir de Apache Spark 2.x, la SparkSession proporciona un
único punto de entrada unificado para manipular datos en Spark
User-Defined Functions
Aunque Apache Spark tiene un sinfín de funciones incorporadas, la flexibilidad de Spark permite a
los ingenieros y científicos de datos definir también sus propias funciones. Estas se conocen como
funciones definidas por el usuario (UDF).
Spark SQL UDFs
El beneficio de crear tu propio PySpark o Scala UDFs es que tu (y otros) podreis hacer uso de ellas
dentro del propio Spark SQL. Por ejemplo, un cientifico de datos puede envolver un modelo ML
dentro de un UDF para que un analista de datos pueda consultar sus predicciones en Spark SQL sin
tener que entender necesariamente el funcionamiento interno del modelo.
Este es un ejemplo simplificado de la creación de una UDF de Spark SQL. Tenga en cuenta que las
UDFs operan por sesión y no se persiguen en el metastore subyacente:
// Se crea la funcion cubo, se registra en udf, luego se crea una vista que tenga id del 1,9 y luego se
junta con cubo
spark.sql("SELECT s FROM test1 WHERE s IS NOT NULL AND strlen(s) > 1")
Por lo tanto, para realizar una correcta comprobación de nulos, se recomienda hacer lo siguiente lo
siguiente:
1. Hacer que la propia UDF sea null-aware y hacer la comprobación de null dentro de la UDF.
2. Utilizar expresiones IF o CASE WHEN para realizar la comprobación de nulos e invocar la UDF en
una rama condicional.
Speeding up and distributing PySpark UDFs with Pandas UDFs
Uno de los problemas vigentes usando PySpark UDFs fue que tenian menor rendimiento que los
UDFs de Scala. Esto fue porque los UDFs de PySpark requerían movimiento de datos entre la JVM y
Python, el cual era muy caro. Para resolver este problema, PandasUDFs (vectorized UDFs) fueron
introducidos como parte de Apache Spark 2.3. Un Panda UDF usa Apache Arrow para transferir
datos y Pandas para trabajar con los datos. Se define una UDF de Pandas utilizando la palabra clave
pandas_udf como el decorador, o para envolver la propia función.
Una vez que los datos están en el formato de Apache Arrow, ya no hay necesidad de
serializar/pickear los datos ya que están en un formato consumible por el proceso de Python. En
lugar de operar sobre entradas individuales fila por fila, se opera sobre una Serie o DataFrame de
Pandas (es decir, ejecución vectorizada).
Desde Apache Spark 3.0 con Python 3.6 y más, los UDFs de Panda fueron divididos en dos categorías
API: Pandas UDFs and Pandas Function APIs
Pandas UDFs
Con Apache Spark 3.0, las UDFs de Pandas infieren el tipo de UDF de Pandas a partir de las pistas de
tipo de Python en las UDFs de Pandas como pandas.Series, pandas.DataFrame, Tuple e Iterator.
Anteriormente era necesario definir y especificar manualmente cada tipo de UDF de Pandas.
Actualmente, los casos soportados de sugerencias de tipos de Python en UDFs de Pandas son Serie a
Serie, Iterador de Serie a Iterador de Serie, Iterador de Multi de series, Iterador de series a Iterador
de series, Iterador de series múltiples a Iterador de series, y Serie a Escalar (un solo valor)
Las APIs de funciones de Pandas permiten aplicar directamente una función local de Python a un
PySpark DataFrame donde tanto la entrada como la salida son instancias de Pandas. Para Spark 3.0,
las APIs de funciones de Pandas soportadas son grouped map, map, cogrouped map.
Con DataFrames:
Plan de ejecución:
A diferencia de una función local, el uso de una UDF vectorizada dará lugar a la ejecución de trabajos
de Spark; la función local anterior es una función de Pandas ejecutada sólo en el controlador de
Spark. Esto se hace más evidente al ver la UI de Spark para una de las etapas de esta función
pandas_udf
Como muchos trabajos de Spark, el trabajo comienza con parallelize() para enviar datos locales
(lotes binarios de Arrow) a los ejecutores y llama a mapPartitions() para convertir los lotes binarios
de Arrow al formato de datos interno de Spark, que puede ser distribuido a los trabajadores de
Spark. Hay una serie de pasos de WholeStageCodegen, que representan un paso fundamental paso
en el rendimiento (gracias a la generación de código WholeStageCodegen del Proyecto Tungsten que
mejora significativamente la eficiencia y el rendimiento de la CPU). Pero es el paso ArrowE valPython
que identifica que (en este caso) se está ejecutando una UDF de Pandas.
Para empezar necesitarás especificar el JDBC driver para tu JDBC fuente de datos y será necesario
qeu esté en el classpath de Spark. Desde el fichero de $SPARK_HOME, empitirá un comando como el
siguiente:
en las opciones de la fuente de datos. La Tabla 5-1 contiene algunas de las propiedades de conexión
propiedades de conexión (insensible a mayúsculas y minúsculas) que soporta Spark.
Transformando un Array
(23) An Introduction to Higher Order Functions in Spark SQL with Herman van Hovell (Databricks) -
YouTube
1 [1,2,3] 1 1
2 [4,5,6] 1 2
1 3 (...)
1 1+1 1 [2,3,4]
1 2+1
1 3+1
Option 2 :
Ejemplo simple:
• Creamos DataFrame
• Aplicar high-order queries
transform()
Produce un array aplicando una función a cada elemento del array de entrada
reduce( )
Reduce los elementos de una rray a un solo valor uniendo los elementos en un buffer B usando la
function <B,T,B> y aplicando una funcion final function<B,R>
• Collection functions
• Datetime
• Math functions
• Miscellaneous functions
• Non-aggregate functions
• Sorting functions
• String functions
• UDF functions
• Window functions
val bar = delays.union(foo) -> Unir dos DataFrames con el mismo esquema juntos
options with join -> inner, cross, outer, full, full_outer, left, left_outer, right, right_outer, left_semi,
and left_anti
default of an inner join between the air portsna and foo DataFrames:
• Windowing
Una función de ventana utiliza los valores de las filas de una ventana (un rango de filas de entrada)
para devolver un conjunto de valores, normalmente en forma de otra fila. Con las funciones de
ventana, es posible operar en un grupo de filas mientras se devuelve un único valor para cada fila de
entrada. En esta sección, mostraremos cómo utilizar la función de ventana dense_rank() pero hay
muchas otras funciones:
dense_rank()
Para entenderlo mejor: (23) Part 1 How to find nth highest salary in sql - YouTube
=
• Modifications
Aunque los propios DataFrames son inmutables, puede modificarlos mediante operaciones para
crear nuevos DataFrames diferentes, con diferentes columnas, por ejemplo. (Recordemos que los
RDDs subyacentes son inmutables, es decir, no pueden ser cambiados - para asegurar que hay
lineaje da datos para las operaciones de Spark)
Dropping columns
Renaming columns
Pivoting
Original:
Sentencia:
Resultado:
Exploramos las funciones incorporadas de Spark para tipos de datos complejos, y dimos algunos
ejemplos de trabajo con funciones de orden superior. Por último, discutimos algunos operadores
relacionales comunes y mostramos cómo realizar una selección de operaciones de DataFrame.
En el próximo capítulo, exploramos cómo trabajar con Datasets, los beneficios de las operaciones
fuertemente tipadas, y cuándo y por qué usarlas.
Ejercicios extras
a. Realizar todos los ejercicios propuestos de libro (excepto los de hive en caso de utilizar spark
instalado en local y en el caso de RDBMS hacer únicamente ejemplo especificado más adelante)
Cons:
ii. Mediante Joins mostrar toda la información de los empleados además de su título y
salario.
Rank: Tiene en cuenta anteriores repetidos, por ejemplo si dos salarios tienen el mismo
puesto, el numero 1 , el siguiente salario va a obtener el rankin 3 porque ya hay 2 antes de él
Aunque hemos nos hemos introducido brevemente en la API Dataset, nos hemos saltado aspectos
de como los Datasets - colecciones distribuidas fuertemente tipadas - son creadas, guardadas y
serializadas y deserializadas en Spark.
En este capítulo, vamos bajo la cubierta para entender los Dataset: exploraremos el trabajar con
Datasets en Java y Scala, como Spark maneja la memoria para acomodar las construcciones de los
Datasets como parte de la API de alto nivel y el coste asociado usando Datasets.
Single API for Java and Scala
Los Datasets ofrecen una unificada y singular API para objetos fuertemente tipados. A lo largo de los
lenuajes soportados por Spark, solo Scala y Java son fuertemente tipados, por lo que solo Python y R
soportan solo la untipada DataFrame API.
Los Datasets son objetos tipados de dominios especificos que pueden ser operados en paralelo
usando programación funcional o los operadores DSLJava con los que estás familiarizados de la API
DataFrame. Gracias a esta singular API, los desarrolladores ya no se arriesgan a quedarse atrás. Por
ejemplo cualquier interfaz futura o cambios de comportamiento para Scala's groupBy(), flatMap(),
map(), o filter() API será la misma para Java también, porque es una interfaz singular que es común a
ambas implementacioes.
Para crear un Dataset[T], donde T es tu objeto tipado en Scala, necesitas una case class que defina el
objeto. Usando nuestro ejemplo de datos dell Capitulo 3, deciamos q teniamos un fichero JSON con
millones de entradas de blog sobre Apache Spark con el siguiente formato:
Para crear un Dataset[Bloggers] distribuimos, debemos primero definir una Scala case class que
defina cada campo individual que comprende un Scala object. Este case class sirve un plano o
esquema para el objeto tipado Bloggers:
Similarmente puedes crear una clase JAvaBean de tipo Bloggers en Java y después usar los
codificadores para crear un Dataset<Bloggers>
Como puedes ver, el crear Datasets en Scala y Java requiere un poco de previsión, como puedes ver
en todas las individuales nombres de columnas y tipos para las filas q estas leyendo. No como los
DataFrames, donde opcionalmente dejas a Spark que infiera el esquema, los Dataset requieren
que definas los tipos de datos antes de tiempo y que tu case class o JavaBean class encaje en tu
esquema.
Los nombres de los campos en la definición de clase de Scala o Java deben encajar en orden en la
fuente de datos. Los nombres de columnas de cada fila en los datos están automáticamente
mapeadas a los nombres correspondientes en la class y los tipos son automaticamente presetvados
Puedes usar una Scala class o JavaBean class existente en los nombres de campos para encajar con
tu input data. Trabajar con la API Dataset es tan facil, conciso y declarativo como trabajar con
DataFrames. Para la mayoría de transformaciones con Dataset's puedes usar los mismos operadores
relacionales que has aprendido en los capítulos previos. Vamos a examinar algunos aspectos con una
muestra de datos.
Ejemplo Case Classes: Un producto tiene un combre, categoría y descripción opcional. Una categoría
tiene un nombre y puede tener una categoría padre. Todos los atributos son read-only
Ahora, que renemos nuestro Dataset generado, vamos a desarrollar algunas de las más comunes
transformaciones que hemos hecho en capítulos anteriores
Algunos ejemplos de estas transformaciones son map(), reduce(), filter(), select() y aggregate().
Como ejemplos de funciones de orden superior, estos métodos pueden tomar lambdas, cierres o
funciones como argumentos y devolver los resultados. Como tales, se prestan bien a la
programación funcional.
Scala es un lenguaje de programación funcional, y más recientemente las lambdas, los argumentos
funcionales y los cierres han sido añadidos a Java también. Probemos un par de funciones de orden
superior en Spark y utilicemos construcciones de programación funcional con el ata de ejemplo que
creamos antes.
En el primer caso utilizamos una expresión lambda, {d.usage > 900}, como argumento del método
filter(), mientras que en el segundo caso definimos una función Scala, def filterWithUsage(u: Usage)
= u.usage > 900. En ambos casos, el método filter() itera sobre cada fila del objeto Usage en el
Dataset distribuido y aplica la expresión o ejecuta la función, devolviendo un nuevo Dataset de tipo
Usage para las filas donde el valor de la expresión o función es verdadero. (Ver la documentación de
Scala para los detalles de la firma del método).
En Java, el argumento de filter() es de tipo FilterFunction<T>. Este puede ser definido bien en línea
de forma anónima o con una función con nombre. Para este ejemplo, definiremos nuestra función
con nombre y la asignaremos a la variable f. Al aplicar esta función en filter() devolverá un nuevo
Dataset con todas las filas para las que nuestra condición de filtro es verdadera.
Not all lambdas or functional arguments must evaluate to Boolean values; they can return computed
values too. Consider this example using the higher-order function map(), where our aim is to find out
the usage cost for each user whose usage value is over a certain threshold so we can offer those
users a special price per minute.
Para utilizar map() en Java, hay que definir una MapFunction<T>. Esta puede ser una clase anónima
o una clase definida que extienda MapFunction<T>. Para este ejemplo la utilizamos en línea, es
decir, en la propia llamada al método:
Aunque hemos calculado valores para el coste de uso, no sabemos a qué usuarios están asociados
los valores calculados. ¿Cómo podemos obtener esta información?
1. Crear una clase Scala case o JavaBean, UsageCost, con un campo adicional columna llamada coste.
Ahora tenemos un conjunto de datos transformado con una nueva columna, coste, calculada por la
función de nuestra transformación map(), junto con todas las demás columnas.
There are a few things to observe about using higher-order functions and Datasets:
Las funciones de orden superior y la programación funcional no son exclusivas de los conjuntos de
datos de Spark; también puedes utilizarlas con los DataFrames.
• Spark 1.0 utilizaba objetos Java basados en RDD para el almacenamiento en memoria, la
serialización y la deserialización, lo que resultaba caro en términos de recursos y lento.
Además, el almacenamiento se asignaba en el heap de Java, por lo que se estaba a merced
de la recolección de basura (GC) de la JVM para grandes conjuntos de datos.
• Spark 1.x introdujo el proyecto Tungsten. Una de sus características más destacadas era un
nuevo formato interno basado en filas para disponer de Datasets y DataFrames en la
memoria fuera de la pila, utilizando offsets y de memoria fuera de la pila, utilizando
desplazamientos y punteros. Spark utiliza un mecanismo eficiente llamado codificador para
serializar y deserializar entre la JVM y su formato interno Tungsten. La asignación de
memoria fuera de la pila significa que Spark se ve menos afectado por la GC.
• Spark 2.x introdujo el motor Tungsten de segunda generación, con generación de código en
etapas completas y diseño de memoria basado en columnas vectoriales. Basado en ideas y
técnicas de los compiladores modernos, esta nueva versión también aprovecha de las
modernas arquitecturas de CPU y caché para un rápido acceso paralelo a los datos con la
acceso paralelo a los datos con el enfoque "instrucción única, datos múltiples" (SIMD).
Dataset Encoders
Los codificadores convierten los datos en la memoria fuera de la pila desde el formato interno
Tungsten de Spark a objetos Java de la JVM. En otras palabras, serializan y deserializan los objetos
Dataset desde formato interno de Spark a objetos JVM, incluyendo tipos de datos primitivos. Por
ejemplo un Encoder[T] convertirá el formato interno de Spark en Dataset[T].
Spark tiene soporte incorporado para generar automáticamente codificadores para tipos primitivos
(por ejemplo, string, integer, long), clases de caso Scala y JavaBeans. En comparación con Java y
Kryo, los codificadores de Spark son mucho más rápidos.
Sin embargo, para Scala, Spark genera automáticamente el bytecode para estos eficientes
convertidores eficientes. Echemos un vistazo al formato interno de Spark basado en filas de
Tungsteno.
Imagine la sobrecarga para crear, por ejemplo, un objeto MyClass(Int, String, String).
En lugar de crear objetos basados en la JVM para Datasets o DataFrames, Spark asigna memoria
Java fuera de la pila para disponer sus datos y emplea codificadores para convertir los datos de la
representación en memoria a un objeto JVM. Por ejemplo, la Figura 6-1 muestra cómo el objeto
JVM MyClass(Int, String, String) sería almacenado internamente.
Cuando los datos se almacenan de esta manera contigua y son accesibles a través de punteros arith
metic y offets, los codificadores pueden serializar o deserializar rápidamente esos datos. ¿Qué
significa esto?
Por ejemplo, si el objeto JVM MyClass de la Figura 6-1 tuviera que ser compartido entre nodos en un
cluster de Spark, el emisor lo serializaría en una matriz de bytes, y el receptor lo deserializaría en un
objeto JVM de tipo MyClass.
La JVM tiene su propio serializador y deserializador Java incorporado, pero es ineficiente porque
(como vimos en la sección anterior) los objetos Java creados por la JVM en la memoria heap están
hinchados. Por lo tanto, el proceso es lento.
Aquí es donde los codificadores de conjuntos de datos vienen al rescate, por algunas razones:
• El formato binario interno de Tungsteno de Spark (ver Figuras 6-1 y 6-2) almacena los
objetos fuera de la memoria del heap de Java, y es compacto, por lo que esos objetos
ocupan menos espacio.
• Los codificadores pueden serializar rápidamente recorriendo la memoria utilizando una
simple aritmética de punteros con direcciones de memoria y desplazamientos (Figura 6-2).
• En el extremo receptor, los codificadores pueden deserializar rápidamente la representación
binaria en la representación interna de Spark. Los codificadores no se ven obstaculizados por
las pausas de recolección de basura de la JVM.
2. La segunda estrategia es encadenar las consultas de tal manera que se minimice la serialización y
deserialización. Encadenar consultas es una práctica común en Spark.
Ilustrémoslo con un ejemplo sencillo. Supongamos que tenemos un Dataset de tipo Persona, donde
Persona se define como una clase de caso Scala:
Examinemos un caso en el que componemos una consulta de forma ineficiente, de tal manera que
que incurrimos involuntariamente en el coste de la serialización y deserialización repetidas:
Como se puede observar en la Figura 6-3, cada vez que pasamos de lambda a DSL (filter($"salario" >
8000)) incurrimos en el coste de serializar y deserializar el objeto Persona objeto JVM
En cambio, la siguiente consulta sólo utiliza DSL y ninguna lambda. Como resultado, es mucho más
eficiente: no se requiere serialización/deserialización para toda la consulta compuesta y
encadenada:
Resumen
En este capítulo, hemos explicado cómo trabajar con Datasets en Java y Scala. Exploramos cómo
Spark gestiona la memoria para acomodar las construcciones de Dataset como parte de parte de su
API unificada y de alto nivel, y hemos considerado algunos de los costes utilizar Datasets y cómo
mitigar esos costes. También mostramos cómo utilizar las construcciones de programación funcional
de Java y las construcciones de programación funcional de Scala en Spark.
Por último, echamos un vistazo a cómo los codificadores serializan y deserializan desde formato
binario Tungsten interno de Spark a objetos JVM.
En el próximo capítulo, veremos cómo optimizar Spark examinando estrategias eficientes de E/S,
optimizando y ajustando las configuraciones de Spark, y qué atributos y señales que hay que buscar
mientras se depuran las aplicaciones de Spark
Para evitar fallos en los trabajos debido a la falta de recursos o la degradación gradual del
rendimiento, hay un puñado de configuraciones de Spark que puede activar o modificar. Estas
configuraciones afectan a tres componentes de Spark: el Spark, el ejecutor y el servicio de barajado
que se ejecuta en el ejecutor (the Spark driver, the executor, and the shuffle service running on the
executor).
La responsabilidad del controlador de Spark es coordinar con el administrador del clúster para lanzar
ejecutores en un clúster y programar las tareas de Spark en ellos. Con grandes cargas de trabajo
puede tener cientos de tareas. En esta sección se explican algunas configuraciones que puedes
ajustar o habilitar para optimizar la utilización de recursos, paralelizar tareas y evitar cuellos de
botella para un gran número de tareas.
Algunas de las ideas de optimización y conocimientos han sido extraídas de empresas de big data
como Facebook que utilizan Spark a escala de terabytes, como elos dijeron a la comunidad Spark en
la Cumbre Spark + AI.1 (e Spark + AI Summit.)
Un caso de uso en el que esto puede ser útil es el streaming, donde el volumen de flujo de datos
puede ser irregular. Otro es el análisis de datos bajo demanda, en el que puede haber un alto
volumen de consultas SQL durante los picos de trabajo. Activar la asignación dinámica de recursos
permite a Spark lograr una mejor utilización de los recursos, liberando ejecutores cuando no se
utilizan y adquiriendo otros nuevos cuando se necesitan.
Además de cuando se trabaja con cargas de trabajo grandes o variables, la asignación dinámica es
también útil en un entorno multi-inquilino, donde Spark puede desplegarse junto a otras
aplicaciones o servicios en YARN Mesos o Kubernetes. Sin embargo, tenga en cuenta que las
demandas cambiantes de recursos de Spark de recursos de Spark puede afectar a otras aplicaciones
que recursos al mismo tiempo
Para habilitar y configurar la asignación dinámica, puede utilizar ajustes como los siguientes.
Tenga en cuenta que los números aquí son arbitrarios; los ajustes apropiados dependerán de la
naturaleza de su carga de trabajo y deben ajustarse en consecuencia. Algunas de estas
configuraciones no pueden establecerse dentro de un REPL de Spark, por lo que tendrás que
establecerlas programáticamente
spark.dynamicAllocation.minExecutors 2
spark.dynamicAllocation.maxExecutors 20
La cantidad de memoria disponible para cada ejecutor está controlada por spark.executor.memory.
Ésta se divide en tres secciones, como se muestra en la Figura 7-2: memoria de ejecución, memoria
de almacenamiento y memoria reservada.
La división por defecto es del 60% para la memoria de ejecución y el 40% para el almacenamiento,
después de permitir 300 MB para la memoria reservada, con el fin de evitar errores OOM. La
documentación de Spark aconseja que esto funcionará para la mayoría de los casos, pero puede
ajustar qué fracción de spark.executor.memory que quieres que utilice cualquiera de las dos
secciones como línea de base. Cuando el almacenamiento
Cuando la memoria de almacenamiento no está siendo utilizada, Spark puede adquirirla para usarla
en la memoria de ejecución para propósitos de ejecución, y viceversa.
La memoria de ejecución se utiliza para los shuffles , uniones, ordenaciones y agregaciones de Spark.
Dado que diferentes consultas pueden requerir diferentes cantidades de memoria, la fracción
(spark.memory.fraction es 0.6 por defecto) de la memoria disponible para dedicar a esto puede ser
difícil de sintonizar, pero es fácil de ajustar. Por el contrario, la memoria de almacenamiento se
utiliza principalmente para almacenar en caché estructuras de datos de usuario y particiones
derivadas de DataFrames.
Durante las operaciones de mapeo y barajado (map and shuffle) , Spark escribe y lee de los archivos
de barajado del disco local, por lo que hay una gran actividad de E/S. Esto puede dar lugar a un
cuello de botella, porque las configuraciones por defecto no son óptimas para trabajos de Spark a
gran escala. Saber qué configuraciones ajustar puede mitigar este riesgo durante esta fase de un
trabajo de Spark.
En la Tabla 7-1, capturamos algunas configuraciones recomendadas para ajustar de manera que los
procesos map, spill, y merge durante estas operaciones no se vean entorpecidos por una ineficiente
E/S y para permitir que estas operaciones empleen la memoria intermedia antes de escribir las
particiones finales aleatorias en el disco. El ajuste del servicio de barajado que se ejecuta en cada
ejecutor puede también puede ayudar a aumentar el rendimiento general para grandes cargas de
trabajo de Spark.
Maximizing Spark parallelism
Gran parte de la eficiencia de Spark se debe a su capacidad para ejecutar múltiples tareas en
paralelo a escala.
Para entender cómo se puede maximizar el paralelismo -es decir, leer y procesar tantos datos en
paralelo como sea posible, tienes que ver cómo Spark lee los datos en la memoria
En el lenguaje de la gestión de datos, una partición es una forma de organizar los datos en un
subconjunto configurable y legible de trozos o bloques de datos contiguos en el disco. Estos
subconjuntos de datos pueden ser leídos o procesados independientemente y en paralelo, si es
necesario, por más más de un hilo en un proceso. Esta independencia es importante porque permite
el paralelismo masivo del procesamiento de datos.
La última proporciona más control sobre cómo y dónde se almacenan los datos, en memoria y en
disco, serializados y no serializados. Ambos contribuyen a mejorar el rendimiento del acceso
frecuente a los DataFrames o a las tablas.
Más info en: (24) Cache vs Persist | Spark Tutorial | Deep Dive - YouTube
DataFrame.cache()
cache() almacenará tantas particiones leídas en memoria en los ejecutores de Spark como la
memoria lo permita (ver Figura 7-2). Mientras que un DataFrame puede ser cacheado
fraccionadamente, las particiones no pueden ser cacheadas fraccionadamente (por ejemplo, si
tienes 8 particiones pero sólo 4,5 particiones pueden caber en la memoria, sólo 4 se almacenarán en
la caché). Sin embargo, si no se almacenan en la caché todas las particiones, cuando quiera acceder
a los datos de nuevo, las particiones que no se han almacenado en la caché tendrán que volver a
calcularse, lo que ralentizará el trabajo de Spark.
Usando cache() el nivel de almacenaje por defecto es MEMORY_ONLY mientras que con persist()
puede haber mas opciones
Cache() = Persist(StorageLevel.MEMORY_ONLY)
La primera count() materializa la caché, mientras que la segunda accede a la caché, lo que resulta en
un tiempo de acceso cerca de 12 veces más rápido para este conjunto de datos.
Cuando se utiliza cache() o persist(), el DataFrame no está completamente cacheado hasta que se
invoca una acción que recorre cada registro (por ejemplo, count()). Si se utiliza una acción como
take(1), sólo se almacenará en la caché porque Catalyst se da cuenta de que no es necesario calcular
todas las particiones sólo para recuperar un registro.
DataFrame.persist()
persist(StorageLevel.LEVEL) es matizado, proporcionando el control sobre cómo sus datos son datos
a través de StorageLevel. La Tabla 7-2 resume los diferentes niveles de almacenamiento. Los datos
en disco siempre se serializan utilizando la serialización Java o Kryo
StorageLevel:
OFF_HEAP Los datos se almacenan fuera de la memoria. La memoria fuera de la pila se utiliza en
Spark para el almacenamiento y la ejecución de consultas;
Veamos el mismo ejemplo que en la sección anterior, pero utilizando el método persist():
No todos los casos de uso exigen la necesidad de almacenar en caché. Algunos escenarios que no
pueden justificar el almacenamiento en caché de sus DataFrames son
Como regla general, debe utilizar la memoria caché con criterio, ya que puede incurrir en costes de
recursos al serializar y deserializar dependiendo del StorageLevel utilizado.
En el centro de estas transformaciones se encuentra la forma en que Spark calcula qué datos
producir, qué claves y datos asociados escribir en el disco, y cómo transferir esas claves y datos a los
nodos como parte de operaciones como groupBy(), join(), agg(), sortBy() y reduceByKey(). Este
movimiento se denomina comúnmente "shuffle".
Spark tiene cinco estrategias de unión distintas por las que intercambia, mueve, ordena, agrupa y
fusiona (exchanges, moves, sorts, groups, and merges) datos entre ejecutores: el broadcast hash
join (BHJ), el shuffle hash join (SHJ) shuffle sort merge join (SMJ), broadcast nested loop join
(BNLJ), y shuffle-andreplicated nested loop join (también conocido como Cartesian product join).
Aquí nos centraremos sólo en dos de (BHJ y SMJ), porque son los más comunes.
Un caso de uso común es cuando se tiene un conjunto común de claves entre dos DataFrames de
datos, uno de los cuales contiene menos información que el otro, y se necesita una vista combinada
de ambos. Por ejemplo, considere un caso sencillo en el que tiene un gran conjunto de datos de
jugadores de fútbol de todo el mundo, playersDF, y un conjunto de datos más pequeño de los clubes
de fútbol en los que juegan clubesDF, y desea unirlos con una clave común
En este código estamos forzando a Spark a hacer un broadcast join, pero se recurrirá a este tipo de
unión por defecto si el tamaño del conjunto de datos más pequeño está por debajo del
spark.sql.autoBroadcastJoinThreshold.
La BHJ es la unión más fácil y rápida que ofrece Spark, ya que no implica ningún shuffle del conjunto
de datos; todos los datos están disponibles localmente para el ejecutor después de una emisión.
Usted sólo tienes que asegurarte de que tienes suficiente memoria tanto en el controlador de Spark
como en el lado del ejecutor para mantener el conjunto de datos más pequeño en la memoria.
En cualquier momento después de la operación, puedes ver en el plan físico qué operación de join se
realizó ejecutando joinedDF.explain(mode)
En Spark 3.0, se puede utilizar joinedDF.explain('mode') para mostrar una salida legible y digerible.
Los modos incluyen 'simple', 'extendido', 'codegen', 'coste' y 'formateado'.
• Cuando cada clave dentro de los conjuntos de datos más pequeños y más grandes se hashee
a la misma partición de Spark
• Cuando un conjunto de datos es mucho más pequeño que el otro (y dentro de la
configuración por defecto de 10 MB, o más si tiene suficiente memoria)
• Cuando sólo se desea realizar un equi-join, para combinar dos conjuntos de datos basados
en las claves coincidentes sin clasificar
• Cuando no le preocupa el uso excesivo del ancho de banda de la red o los errores OOM
porque el conjunto de datos más pequeño se transmitirá a todos los ejecutores de Spark
Como su nombre indica, este esquema de unión tiene dos fases: una fase de ordenación seguida de
una fase de fusión. La fase de ordenación ordena cada conjunto de datos por su clave de unión
deseada; la fase de fusión itera sobre cada clave en la fila de cada conjunto de datos y fusiona las
filas si las dos claves coinciden.
Además, la interfaz de usuario de Spark (de la que hablaremos en la siguiente sección) muestra tres
etapas para todo el trabajo: las operaciones de Intercambio y Ordenación ocurren en la etapa final
seguido de la fusión de los resultados, como se muestra en las Figuras 7-7 y 7-8. El intercambio es
costoso y requiere que las particiones sean barajadas a través de la red entre ejecutores.
Optimizing the shuffle sort merge join
When to use a shuffle sort merge join
Inspecting the Spark UI
Journey Through the Spark UI Tabs