13  Taller Spark: el motor sobre el clúster que ya tienes

big-data
spark
yarn
taller
Autor/a

Wilder Ramírez Delgado

Fecha de publicación

4 de octubre de 2026

Este taller es la práctica del capítulo anterior, Spark: el motor de procesamiento sobre YARN. Partes del clúster ya montado (HDFS, YARN y spark-client). No se vuelve a construir Docker ni a explicar el NameNode. No es PySpark: no abres Jupyter ni escribes DataFrames. El objetivo es ver el motor: tipo SPARK en el portal, driver y executors, un DAG frente a un job MapReduce.

Nivel: intermedio. Cada ejercicio te da un objetivo, qué debes lograr y un análisis. Tú construyes las órdenes. Al final del taller están las soluciones de referencia: úsalas para comprobar tu trabajo después de intentarlo, no para copiar. La señal de que resolviste un ejercicio no es que “no dé error”, sino que puedes explicar por qué YARN y Spark respondieron como respondieron.

Tu herramienta de referencia: spark-submit --help, yarn application -help, yarn node -help. Saber leerla es parte de lo que se evalúa.

Datos que ya dejó el laboratorio del cliente (no los reinventes):

Dato Valor
Atajo Compose (los dos YAML) dc() { docker compose -f compose.yaml -f compose.spark.yaml "$@"; }
JAR de ejemplos /opt/spark/examples/jars/spark-examples_2.12-3.5.3.jar
SparkPi clase org.apache.spark.examples.SparkPi
Wordcount Java clase org.apache.spark.examples.JavaWordCount
Texto de los laboratorios hdfs://namenode:9000/yarn/entrada/texto.txt
Portal YARN http://localhost:8088
UI del driver http://localhost:4040 mientras el driver vive

Requisitos previos

  • Laboratorio del cliente Spark terminado (spark-client, dc, SparkPi de comprobación).
  • MapReduce (teoría y laboratorio): sabes leer tipo MAPREDUCE y un part-r-*.
  • Teoría de Spark: DAG, evaluación perezosa, driver, executors, aplicación / job / etapa / tarea.

Convención de terminales:

  • 🖥️ HOST — Bash en ~/hdfs-docker-lab. Aquí defines dc (se pierde al cerrar la ventana).
  • 🔥 CLIENTE — órdenes Spark y Hadoop dentro de spark-client (dc exec spark-client ...).
  • 🌐 NAVEGADOR — ResourceManager :8088. La UI Spark :4040 solo existe con el driver arriba.

docker compose a secas no ve spark-client. Si dc ps no lista ese servicio, el atajo no está leyendo el segundo YAML.


Mapa del taller

# Ejercicio Frente Concepto Dificultad
0 Clúster y cliente arriba Infra dc, no rehacer HDFS Básico
1 SparkPi y tipo SPARK Motor Mismo YARN, otro tipo Básico
2 Historia: MAPREDUCE frente a SPARK Motor Dos motores, un portal Básico
3 Wordcount Spark sobre el mismo texto Motor Shuffle en Spark Intermedio
4 Comparar con el wordcount MapReduce Motor Mismo conteo, otro job Intermedio
5 Aplicación, job, etapa, tarea Modelo Jerarquía de la teoría Intermedio
6 Dónde está el driver Arquitectura Modo client Intermedio
7 Dónde están los executors Arquitectura Contenedor YARN ≠ Docker Medio-alto
8 Un Pi más largo y :4040 Observación UI del driver Medio-alto
9 Job en ACCEPTED Recursos 2 GB por NodeManager Medio-alto
10 Matar una aplicación Spark Operación KILLED vs SUCCEEDED Medio-alto
11 SparkPi no lee HDFS Modelo MapReduce π vs SparkPi Intermedio
12 Reto integrador Mixto Explicar un DAG con evidencias Alto

Ejercicio 0 — Clúster y cliente arriba

Objetivo: trabajar sobre el clúster ya montado, sin crear otro proyecto Compose.

Qué debes lograr:

  • Estar en ~/hdfs-docker-lab con el atajo dc activo.
  • Ocho servicios, incluido spark-client, en marcha (o arrancarlos sin recrear volúmenes).
  • Modo seguro OFF, tres DataNodes vivos, dos NodeManagers RUNNING.
  • /yarn/entrada/texto.txt todavía tiene las cuatro líneas de los laboratorios.

Análisis: ¿por qué docker compose ps puede ocultar spark-client aunque el contenedor exista? Si el texto falta, ¿lo subes otra vez desde el cliente o desde el NameNode, y qué cambia?


Parte 1 — El mismo YARN, otro motor

Ejercicio 1 — SparkPi y tipo SPARK

Objetivo: enviar SparkPi y reconocer el motor en el portal.

Qué debes lograr:

  • Lanzar SparkPi con spark-submit en modo client, master yarn, desde spark-client.
  • Ver en la terminal una estimación cercana a 3,14.
  • En :8088, una aplicación de nombre tipo Spark Pi, tipo SPARK, estado FINISHED / SUCCEEDED.

Análisis: en el laboratorio del cliente ya corriste SparkPi para comprobar que el contenedor hablaba con YARN. ¿Qué pregunta responde ahora, en este taller? ¿Por qué el tipo no es MAPREDUCE si YARN es el mismo?

Ejercicio 2 — Historia: MAPREDUCE frente a SPARK

Objetivo: el portal mezcla motores. Tú los separas.

Qué debes lograr:

  • Listar aplicaciones en todos los estados (ALL).
  • Identificar al menos una MAPREDUCE (π o wordcount de MapReduce) y una SPARK (SparkPi).
  • Anotar Application ID, tipo y estado final de cada una.

Análisis: ¿qué pieza decide el tipo que ves: el ResourceManager, el ApplicationMaster o el JAR que enviaste? Si alguien dice “el job corrió en Hadoop”, ¿cómo contestas con esta tabla?

Ejercicio 3 — Wordcount Spark sobre el mismo texto

Objetivo: un cálculo con shuffle (reagrupar por clave a través de la red), no solo π.

Qué debes lograr:

  • Enviar JavaWordCount sobre hdfs://namenode:9000/yarn/entrada/texto.txt.
  • Recuperar el conteo (este programa imprime en la salida estándar del driver; no tiene por qué dejar part-* en HDFS).
  • Confirmar en :8088 otra aplicación tipo SPARK.

Análisis: ¿por qué este ejemplo sí necesita el archivo de HDFS y SparkPi no? Relaciónalo con las tres fases de MapReduce: ¿dónde está aquí el equivalente de map, shuffle y reduce?

Ejercicio 4 — Comparar con el wordcount MapReduce

Objetivo: el mismo diccionario, dos motores.

Qué debes lograr:

  • Releer un part-r-* de MapReduce que ya tengas (/yarn/salida_wc, /yarn/teoria_mr_wc o la salida de Streaming).
  • Poner lado a lado las seis claves: datos, hadoop, hdfs, mapreduce, spark, yarn.
  • Si una salida MapReduce no existe, no montes un clúster nuevo: relanza el JAR de ejemplos Hadoop desde el cliente que ya usas, a un directorio de salida nuevo.

Análisis: si los números coinciden, ¿qué quedó demostrado y qué no? (pista: coincidir no prueba que Spark “sea MapReduce por dentro”.)


Parte 2 — La jerarquía y quién calcula

Ejercicio 5 — Aplicación, job, etapa, tarea

Objetivo: usar los cuatro términos de la teoría con una corrida real.

Qué debes lograr:

  • Para una aplicación Spark que hayas lanzado, decir: cuál es la aplicación YARN (application_…), cuándo nació el job (qué acción lo disparó), si hubo más de una etapa, y qué es una tarea.
  • Apoyarte en :8088 (detalle de la app) y, si alcanzas a abrirla, en :4040 (Jobs / Stages).

Análisis: SparkPi y el wordcount, ¿cuántos jobs YARN ves (uno o dos) y cuántas aplicaciones? ¿Por qué esa diferencia importa cuando alguien dice “lanzé dos jobs”?

Ejercicio 6 — Dónde está el driver

Objetivo: modo client no es un adorno del YAML.

Qué debes lograr:

  • Explicar, con evidencia, que el driver (quien arma el plan y recoge el resultado) no corre como contenedor YARN de los NodeManagers.
  • Relacionarlo con spark.submit.deployMode / --deploy-mode client y con el hostname spark-client.

Análisis: si el driver estuviera en un contenedor YARN (cluster mode), ¿podrías usar Jupyter con el kernel en spark-client como en el laboratorio del cliente? ¿Por qué sí o por qué no?

Ejercicio 7 — Dónde están los executors

Objetivo: contenedor YARN ≠ contenedor Docker.

Qué debes lograr:

  • Localizar en qué NodeManagers corrieron contenedores de tu aplicación Spark (intento, AM, executors).
  • Comprobar que no apareció un docker compose / dc ps nuevo: siguen los mismos ocho servicios.

Análisis: un executor pide heap + overhead (en el laboratorio: 512 MiB + 384 MiB). ¿Cuántos executors caben a la vez en un NodeManager de 2 GB si también hay que alojar el ApplicationMaster? Muestra la cuenta.


Parte 3 — Observar y operar

Ejercicio 8 — Un Pi más largo y :4040

Objetivo: la UI Spark no es el portal de YARN.

Qué debes lograr:

  • Lanzar SparkPi con más slices (el entero al final del JAR) para que dure lo suficiente.
  • Mientras corre, abrir http://localhost:4040 y anotar al menos: nombre de la app, número de executors, una etapa o un job.
  • En :8088, el estado durante la corrida (RUNNING) y al terminar (FINISHED).

Análisis: al acabar el spark-submit, :4040 deja de responder. :8088 sigue. ¿Qué proceso servía la 4040 y por qué muere con el driver en modo client?

Ejercicio 9 — Job en ACCEPTED

Objetivo: YARN aceptó la app y aún no le dio contenedores.

Qué debes lograr:

  • Dejar una aplicación Spark viva (un Pi largo, o no cerrar otra app) y lanzar otra Spark a la vez.
  • Capturar el síntoma ACCEPTED (portal o yarn application -list).
  • Liberar recursos y ver que la segunda pasa a RUNNING o que decides no lanzarla.

Análisis: ¿el cuello es HDFS, el cliente o la memoria de los NodeManagers? ¿Qué harías antes de subir spark.executor.memory?

Ejercicio 10 — Matar una aplicación Spark

Objetivo: distinguir KILLED, FAILED y FINISHED / SUCCEEDED.

Qué debes lograr:

  • Lanzar un SparkPi largo.
  • Desde otra terminal, matar esa aplicación YARN por su application_….
  • Dejar evidencia del estado final y de que los contenedores volvieron al pool (yarn node -list / métricas de :8088).

Análisis: ¿matar la aplicación YARN mata el spark-submit del driver? ¿Qué verías en la terminal del cliente?

Ejercicio 11 — SparkPi no lee HDFS

Objetivo: no todo trabajo Spark es “leer un txt y contar”.

Qué debes lograr:

  • Contrastar SparkPi (sin -input de texto) con el wordcount Spark y con el π de MapReduce (QuasiMonteCarlo).
  • Una frase por cada uno: ¿lee HDFS? ¿hay shuffle de palabras? ¿para qué sirve en este libro?

Análisis: “Spark siempre trabaja en memoria sobre archivos de HDFS”. Con estas tres corridas, ¿dónde se rompe esa frase?


Ejercicio 12 — Reto integrador

Diseña una corrida (SparkPi o wordcount) y entrega un párrafo con evidencias, no un diario de comandos, que cubra:

  1. Qué aplicación YARN fue (ID, tipo SPARK, estado).
  2. Quién fue el driver y quiénes los executors (máquinas / servicios).
  3. Si hubo shuffle o no, y cómo lo sabes.
  4. Qué habría sido otro job MapReduce si hubieras encadenado dos pasos, y por qué Spark lo cuenta como un DAG de etapas.
  5. Qué no demostraste (por ejemplo: DataFrames, SQL, que “todo cabe en RAM”).

Diagnóstico

Síntoma Hacia dónde mirar
dc no lista spark-client Atajo sin el segundo YAML; ventana de terminal nueva
Connection refused a 9000 u 8088 Clúster HDFS/YARN no listo; no es un fallo de Spark
ACCEPTED eterno Otra Spark viva; 2 GB por NM; spark.stop / matar apps
:4040 no abre El spark-submit ya terminó (el driver se fue)
Wordcount “Path does not exist” Falta /yarn/entrada/texto.txt (laboratorio del cliente o de YARN)
Tipo MAPREDUCE cuando esperabas Spark Enviaste el JAR de Hadoop, no spark-submit

Listo cuando: tienes al menos una app SPARK de SparkPi y una de wordcount, sabes separarlas de las MAPREDUCE del portal, y puedes decir dónde corrió el driver y dónde los executors.


Soluciones de referencia

Consulta esta sección después de intentar cada ejercicio. Los comandos se ejecutan en 🖥️ HOST, dentro de ~/hdfs-docker-lab, con el atajo dc definido. Los identificadores application_… y las cifras exactas cambian en cada corrida.

Para no repetir las rutas largas, define también estas variables en la misma terminal:

dc() { docker compose -f compose.yaml -f compose.spark.yaml "$@"; }
SUBMIT="/opt/spark/bin/spark-submit --master yarn --deploy-mode client"
JAR=/opt/spark/examples/jars/spark-examples_2.12-3.5.3.jar

Ejercicio 0

cd ~/hdfs-docker-lab
dc ps
dc start        # solo si hay servicios detenidos; no recrea volúmenes
dc exec spark-client /opt/hadoop/bin/hdfs dfsadmin -safemode get
dc exec spark-client /opt/hadoop/bin/hdfs dfsadmin -report | grep "Live datanodes"
dc exec spark-client /opt/hadoop/bin/yarn node -list
dc exec spark-client /opt/hadoop/bin/hdfs dfs -cat /yarn/entrada/texto.txt

Análisis. docker compose a secas lee solo compose.yaml, que describe HDFS y YARN; spark-client está definido en compose.spark.yaml, así que no aparece. Si falta el texto, súbelo desde el cliente (paso 8 del laboratorio del cliente Spark): el resultado en HDFS es el mismo, solo cambia desde qué contenedor se ejecuta el comando.

Ejercicio 1

dc exec spark-client $SUBMIT --class org.apache.spark.examples.SparkPi $JAR 10

Busca Pi is roughly 3.14… entre los mensajes. En :8088, la aplicación se llama Spark Pi, tipo SPARK, FINISHED / SUCCEEDED.

Análisis. En el laboratorio, SparkPi respondía “¿el cliente habla con YARN?”. Aquí responde “¿qué motor usó YARN?”: el mismo ResourceManager registra la aplicación con otro tipo porque la envió otro cliente.

Ejercicio 2

dc exec spark-client /opt/hadoop/bin/yarn application -list -appStates ALL

Análisis. El tipo lo declara el cliente que envía la aplicación: spark-submit la registra como SPARK y el cliente de MapReduce (yarn jar con un JAR de MapReduce) como MAPREDUCE. El ResourceManager solo lo anota. “Corrió en Hadoop” no dice nada: la pregunta útil es qué motor la envió y en qué cola corrió.

Ejercicio 3

dc exec spark-client $SUBMIT --class org.apache.spark.examples.JavaWordCount $JAR \
  hdfs://namenode:9000/yarn/entrada/texto.txt

Salida esperada (el orden de las líneas puede variar, y van mezcladas con mensajes de log):

hadoop: 3
hdfs: 1
yarn: 3
mapreduce: 1
spark: 1
datos: 3

Análisis. SparkPi genera sus números al azar; el wordcount necesita un archivo que leer. En el programa, partir las líneas en palabras y formar pares (palabra, 1) es el map; reduceByKey provoca el shuffle y la suma es el reduce.

Ejercicio 4

dc exec spark-client /opt/hadoop/bin/hdfs dfs -ls /yarn
dc exec spark-client /opt/hadoop/bin/hdfs dfs -cat /yarn/salida_wc/part-r-00000

Si no existe ninguna salida de MapReduce, genera una nueva desde el mismo cliente:

dc exec spark-client /opt/hadoop/bin/yarn jar \
  /opt/hadoop/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.4.1.jar \
  wordcount /yarn/entrada/texto.txt /yarn/taller_spark_mr_wc
dc exec spark-client /opt/hadoop/bin/hdfs dfs -cat /yarn/taller_spark_mr_wc/part-r-00000

Análisis. Que los conteos coincidan demuestra que ambos motores calcularon bien el mismo problema. No demuestra que Spark use MapReduce por dentro: son motores distintos que comparten HDFS, YARN y la idea de agrupar por clave.

Ejercicio 5

Análisis. Cada spark-submit es una aplicación YARN. SparkPi dispara un job (la acción reduce final) con una etapa, porque no hay shuffle: cada tarea suma sus dardos y el driver combina los resultados. El wordcount dispara un job (la acción collect) con dos etapas, separadas por el shuffle de reduceByKey. Una tarea es una etapa aplicada a una partición: SparkPi con 10 crea diez tareas en su única etapa. “Lancé dos jobs” suele querer decir “dos aplicaciones”: en la UI de Spark, cada una tiene su propia lista de jobs.

Ejercicio 6

Mientras corre una aplicación (por ejemplo, la del ejercicio 8), abre http://localhost:4040, pestaña Environment, y busca spark.submit.deployMode = client y spark.driver.host = spark-client. En :8088, el contenedor del ApplicationMaster de una aplicación en modo client no ejecuta tu programa: solo pide executors.

Análisis. En modo cluster el driver correría dentro de un contenedor YARN, en un NodeManager. El kernel de Jupyter necesita el driver en su propio proceso para devolver resultados a las celdas, así que no podrías trabajar interactivamente desde spark-client.

Ejercicio 7

Con una aplicación en marcha:

dc exec spark-client /opt/hadoop/bin/yarn applicationattempt -list <application_id>
dc exec spark-client /opt/hadoop/bin/yarn container -list <appattempt_id>
dc ps

Verás tres contenedores YARN —el ApplicationMaster y dos executors— repartidos entre nodemanager1 y nodemanager2, y los mismos ocho servicios de siempre en dc ps. Para una aplicación terminada, la misma información está en el detalle de la aplicación en :8088.

Análisis. Cada executor pide 512 MiB de heap + 384 MiB de overhead = 896 MiB. YARN redondea cada petición al múltiplo de yarn.scheduler.minimum-allocation-mb (256 MiB) inmediatamente superior: 1024 MiB. Un NodeManager ofrece 2048 MiB, así que caben dos contenedores por nodo: el que aloja al ApplicationMaster (512 + 384 MiB, también redondeado a 1024) solo tiene sitio para un executor más. En todo el clúster caben cuatro contenedores; una aplicación usa tres.

Ejercicio 8

dc exec spark-client $SUBMIT --class org.apache.spark.examples.SparkPi $JAR 2000

Mientras corre, abre http://localhost:4040: nombre Spark Pi, dos executors en Executors y una etapa con 2000 tareas en Stages. En :8088 la aplicación aparece RUNNING y, al terminar, FINISHED.

Análisis. La UI del puerto 4040 la sirve el driver. En modo client el driver es el proceso spark-submit dentro de spark-client: cuando termina, el puerto deja de responder. El portal de YARN lo sirve el ResourceManager, que sigue vivo.

Ejercicio 9

En una terminal, lanza un SparkPi largo (2000 o más). Mientras corre, en otra terminal (define otra vez dc, SUBMIT y JAR):

dc exec spark-client $SUBMIT --class org.apache.spark.examples.SparkPi $JAR 100
dc exec spark-client /opt/hadoop/bin/yarn application -list

La segunda aplicación queda en ACCEPTED. En su detalle en :8088 el diagnóstico suele decir que se superó el límite de recursos para ApplicationMasters de la cola (Queue’s AM resource limit exceeded). Cuando la primera termina, la segunda pasa a RUNNING.

Análisis. El cuello no es HDFS ni el cliente: es la capacidad de YARN. La cola default reserva solo una fracción del clúster para ApplicationMasters y la primera aplicación ya ocupa tres de los cuatro contenedores posibles. Antes de subir spark.executor.memory, revisa qué otras aplicaciones siguen vivas: subir la memoria hace que quepan menos contenedores.

Ejercicio 10

dc exec spark-client /opt/hadoop/bin/yarn application -list
dc exec spark-client /opt/hadoop/bin/yarn application -kill <application_id>
dc exec spark-client /opt/hadoop/bin/yarn application -status <application_id>

El estado final es KILLED. En :8088, las métricas del clúster vuelven a mostrar la memoria libre.

Análisis. Sí: al morir el ApplicationMaster, el driver pierde a sus executors y el spark-submit termina con un error que indica que la aplicación ya terminó o fue cancelada. KILLED significa que alguien la canceló; FAILED, que falló por sí misma; FINISHED / SUCCEEDED, que terminó bien.

Ejercicio 11

Corrida ¿Lee datos de HDFS? ¿Shuffle de palabras? Para qué sirve en este libro
SparkPi No; genera números al azar No Probar que spark-submit habla con YARN y observar el motor sin datos
Wordcount Spark Sí, el texto de cuatro líneas Sí, en reduceByKey Ver un DAG de dos etapas con shuffle
π de MapReduce (QuasiMonteCarlo) No lee un texto; usa HDFS para sus archivos de trabajo No hay palabras; un reduce combina los resultados Ver las fases map y reduce del motor clásico

Análisis. La frase falla dos veces: SparkPi no lee archivos de HDFS, y el wordcount, aunque cabe en memoria, necesita shuffle, que pasa por disco y red.

Ejercicio 12

Un ejemplo de párrafo con el wordcount:

La aplicación application_…_0012, de tipo SPARK, terminó FINISHED / SUCCEEDED. El driver corrió en spark-client en modo client (spark.submit.deployMode = client en la pestaña Environment), y los dos executors en nodemanager1 y nodemanager2, como contenedores YARN. Hubo shuffle: la UI muestra dos etapas, y la segunda leyó los datos que escribió la primera (Shuffle Read en Stages). En MapReduce, añadir un segundo paso —por ejemplo, ordenar las palabras por frecuencia— exigiría otro trabajo que leyera de HDFS la salida del primero; Spark lo añade como una etapa más del mismo DAG. No demostré nada sobre DataFrames ni SQL, ni que los datos quepan siempre en memoria.

Siguiente: PySpark (PySpark: la API de Spark en Python y Taller de PySpark). El motor ya lo viste; allá escribes la API en Python.