13 Taller Spark: el motor sobre el clúster que ya tienes
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
MAPREDUCEy unpart-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í definesdc(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:4040solo existe con el driver arriba.
docker composea secas no vespark-client. Sidc psno 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-labcon el atajodcactivo. - 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.txttodaví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-submiten modo client, masteryarn, desdespark-client. - Ver en la terminal una estimación cercana a 3,14.
- En
:8088, una aplicación de nombre tipo Spark Pi, tipoSPARK, estadoFINISHED / 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 unaSPARK(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
JavaWordCountsobrehdfs://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
:8088otra aplicación tipoSPARK.
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_wco 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 clienty con el hostnamespark-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 psnuevo: 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 oyarn application -list). - Liberar recursos y ver que la segunda pasa a
RUNNINGo 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
-inputde 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:
- Qué aplicación YARN fue (ID, tipo
SPARK, estado). - Quién fue el driver y quiénes los executors (máquinas / servicios).
- Si hubo shuffle o no, y cómo lo sabes.
- Qué habría sido otro job MapReduce si hubieras encadenado dos pasos, y por qué Spark lo cuenta como un DAG de etapas.
- 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.jarEjercicio 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.txtAná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 10Busca 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 ALLAná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.txtSalida 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-00000Si 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-00000Aná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 psVerá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 2000Mientras 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 -listLa 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 tipoSPARK, terminóFINISHED / SUCCEEDED. El driver corrió enspark-clienten modo client (spark.submit.deployMode = clienten la pestaña Environment), y los dos executors ennodemanager1ynodemanager2, 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.