lección 3
Instalar PySpark: levantar un cluster Spark con Docker
Docker Compose con Spark master + worker + Jupyter. Tu laboratorio de procesamiento distribuido en 5 minutos.
⏱ 40 min
## Tu laboratorio personal de Big Data
Hasta ahora hemos hablado de clusters de 10, 50, 100 nodos. Pero para aprender no necesitas una empresa con 200 servidores. Docker te permite simular un cluster Spark completo en tu portátil: un nodo master que coordina, workers que procesan, y un notebook Jupyter donde escribes tu código PySpark. Todo encapsulado en contenedores que puedes levantar y destruir en segundos.
Este docker-compose.yml es tu entorno de desarrollo para TODA esta skill. Lo levantarás al inicio de cada sesión de práctica y lo pararás cuando termines. Los datasets de ejemplo los montamos como volúmenes para que persistan entre sesiones.
Requisito previo: Docker debe estar instalado y funcionando. Si seguiste la Skill 6 (Docker), ya lo tienes. Si no, instala Docker Desktop desde docker.com — es gratuito para uso educativo.
## Paso 1: Crear la estructura de directorios
Crea una carpeta para todo tu trabajo con PySpark. Dentro tendrás el docker-compose, tus notebooks y los datasets de ejemplo:
1# Crear la estructura del proyecto2mkdir -p pyspark-lab/notebooks3mkdir -p pyspark-lab/data4mkdir -p pyspark-lab/output5cd pyspark-lab67# Verificar que Docker está funcionando8docker --version9docker compose version
Estructura básica: notebooks para tu código, data para datasets, output para resultados
## Paso 2: El docker-compose.yml
Este archivo define tres servicios: spark-master (el coordinador), spark-worker (el que procesa datos) y jupyter (tu interfaz para escribir código). El master y el worker forman el cluster Spark. Jupyter se conecta al master para enviar jobs.
1# docker-compose.yml23services:4 spark-master:5 image: bitnami/spark:3.56 container_name: spark-master7 environment:8 - SPARK_MODE=master9 - SPARK_MASTER_HOST=spark-master10 - SPARK_MASTER_PORT=707711 ports:12 - "8080:8080" # Spark UI del master13 - "7077:7077" # Puerto del cluster14 volumes:15 # MISMA ruta que en jupyter: en un cluster el fichero lo abre el executor,16 # no tu notebook, y la ruta tiene que existir igual en los dos sitios17 - ./data:/home/jovyan/data18 - ./output:/home/jovyan/output1920 spark-worker:21 image: bitnami/spark:3.522 environment:23 - SPARK_MODE=worker24 - SPARK_MASTER_URL=spark://spark-master:707725 - SPARK_WORKER_MEMORY=2G26 - SPARK_WORKER_CORES=227 depends_on:28 - spark-master29 volumes:30 # el worker es quien lee y escribe de verdad: misma ruta, sin excepción31 - ./data:/home/jovyan/data32 - ./output:/home/jovyan/output3334 jupyter:35 image: quay.io/jupyter/pyspark-notebook:spark-3.5.036 container_name: pyspark-jupyter37 environment:38 - SPARK_MASTER=spark://spark-master:707739 ports:40 - "8888:8888" # Jupyter Notebook41 - "4040:4040" # Spark UI del driver (para diagnóstico)42 volumes:43 - ./notebooks:/home/jovyan/work44 - ./data:/home/jovyan/data45 - ./output:/home/jovyan/output46 depends_on:47 - spark-master48 - spark-worker
docker-compose.yml: un cluster Spark completo con Jupyter incluido
Analicemos qué hace cada servicio:
- spark-master: el coordinador del cluster. Escucha en el puerto 7077 para que los workers se conecten. La UI web está en el puerto 8080.
- spark-worker: un nodo trabajador con 2 GB de RAM y 2 cores asignados. Procesa las particiones de datos que el master le asigne.
- jupyter: un notebook con PySpark preinstalado. Se conecta al master para enviar jobs al cluster. Tu interfaz de trabajo diaria.
Fíjate en que ./data se monta en la MISMA ruta (/home/jovyan/data) en los tres servicios. Esto es lo que más quebraderos de cabeza da en un cluster: cuando escribes spark.read.csv("/home/jovyan/data/ventas.csv"), el fichero no lo abre tu notebook, lo abren los executors, que viven dentro del contenedor del worker. Si ahí la ruta no existe, el job no falla al leer la línea: falla más tarde, dentro de las tasks, con un FileNotFoundException que parece no tener nada que ver. Regla: en un cluster, toda ruta local que le pases a Spark tiene que existir igual en todos los nodos.
Si tienes menos de 8 GB de RAM en tu máquina, reduce SPARK_WORKER_MEMORY a 1G. Docker + los tres contenedores necesitan al menos 4-5 GB para funcionar sin problemas. Si tu máquina es potente (16+ GB), puedes añadir un segundo worker copiando el servicio spark-worker.
## Paso 3: Levantar el cluster
1# Levantar todos los servicios en background2docker compose up -d34# Ver el estado de los contenedores5docker compose ps67# Deberías ver los tres servicios con STATUS "Up". Algo así:8# NAME STATUS PORTS9# spark-master Up 2 minutes 0.0.0.0:7077->7077, 0.0.0.0:8080->808010# pyspark-lab-spark-worker-1 Up 2 minutes11# pyspark-jupyter Up 2 minutes 0.0.0.0:4040->4040, 0.0.0.0:8888->888812#13# El worker lleva el prefijo de la carpeta del proyecto (pyspark-lab-) y un -114# al final porque no le hemos puesto container_name: es lo que permite escalarlo15# a varios workers más adelante.1617# Ver los logs del master para confirmar que el worker se conectó18docker compose logs spark-master | grep -i "registered"19# Deberías ver: "Registered worker ... with 2 cores, 2048.0 MiB RAM"
Levantar y verificar que el cluster funciona
## Paso 4: Verificar la Spark UI
Abre tu navegador y ve a http://localhost:8080. Deberías ver la Spark Master UI con información sobre el cluster: workers conectados, memoria disponible, cores totales. Esta UI será tu mejor amiga para diagnosticar problemas (la exploraremos en profundidad en la lección 8).
## Paso 5: Tu primer programa PySpark
Abre Jupyter en http://localhost:8888 (puede pedir un token — míralo en los logs con docker compose logs jupyter). Crea el notebook DENTRO de la carpeta work: es la única que está montada contra tu máquina, así que es la única donde el notebook sobrevive a un docker compose down. Ahí escribe tu primer programa Spark:
1from pyspark.sql import SparkSession23# Crear la sesión Spark conectada al cluster4spark = (5 SparkSession.builder6 .appName("mi-primer-spark")7 .master("spark://spark-master:7077")8 .config("spark.executor.memory", "1g")9 .getOrCreate()10)1112# Verificar la conexión13print(f"Spark version: {spark.version}")14print(f"App name: {spark.sparkContext.appName}")15print(f"Master: {spark.sparkContext.master}")1617# Crear un DataFrame simple para probar18datos = [19 ("Ana", "Madrid", 28),20 ("Carlos", "Barcelona", 34),21 ("Elena", "Sevilla", 25),22 ("David", "Valencia", 31),23]2425df = spark.createDataFrame(datos, ["nombre", "ciudad", "edad"])26df.show()27# +------+---------+----+28# |nombre| ciudad|edad|29# +------+---------+----+30# | Ana| Madrid| 28|31# |Carlos|Barcelona| 34|32# | Elena| Sevilla| 25|33# | David| Valencia| 31|34# +------+---------+----+3536print(f"Número de particiones: {df.rdd.getNumPartitions()}")37print("[OK] ¡Spark funciona correctamente!")
Tu primer programa PySpark: conexión al cluster y DataFrame básico
Si obtienes un error de conexión al master, espera 30 segundos y reintenta. El master tarda un poco en estar listo. También verifica con docker compose ps que todos los contenedores están en estado "running".
## Paso 6: Crear los datasets de práctica
Para las siguientes lecciones necesitamos datos realistas. Vamos a generar un dataset de ventas de un e-commerce ficticio que usaremos en todos los ejercicios:
1import csv2import random3from datetime import datetime, timedelta45# Generar dataset de ventas (1 millón de filas para que sea interesante)6random.seed(42)78productos = ["Laptop", "Monitor", "Teclado", "Mouse", "Auriculares",9 "Webcam", "SSD", "RAM", "Cable USB", "Hub USB"]10# Pesos: los periféricos baratos se venden mucho más que los portátiles11pesos_producto = [2, 5, 15, 20, 8, 3, 5, 5, 27, 10]1213ciudades = ["Madrid", "Barcelona", "Valencia", "Sevilla", "Bilbao",14 "Málaga", "Zaragoza", "Murcia", "Palma", "Las Palmas"]15# Madrid y Barcelona concentran la mitad del mercado16pesos_ciudad = [30, 18, 10, 8, 7, 7, 6, 5, 5, 4]1718categorias = {"Laptop": "Portátiles", "Monitor": "Pantallas",19 "Teclado": "Periféricos", "Mouse": "Periféricos",20 "Auriculares": "Audio", "Webcam": "Video",21 "SSD": "Almacenamiento", "RAM": "Componentes",22 "Cable USB": "Accesorios", "Hub USB": "Accesorios"}2324# Ruta absoluta: es la carpeta montada como volumen, la misma en los tres25# servicios. Con una ruta relativa el fichero acabaría dentro de work/ y Spark26# no lo encontraría.27filas_total = 1_000_0002829with open("/home/jovyan/data/ventas.csv", "w", newline="") as f:30 writer = csv.writer(f)31 writer.writerow(["id", "fecha", "producto", "categoria",32 "ciudad", "cantidad", "precio_unitario", "cliente_id"])3334 fecha_base = datetime(2024, 1, 1)35 ultima_fila = None3637 for i in range(filas_total):38 fecha = fecha_base + timedelta(days=random.randint(0, 364))39 producto = random.choices(productos, weights=pesos_producto)[0]40 ciudad = random.choices(ciudades, weights=pesos_ciudad)[0]4142 # Se construye primero la fila limpia...43 fila = [i + 1, fecha.strftime("%Y-%m-%d"), producto,44 categorias[producto], ciudad,45 random.randint(1, 5),46 round(random.uniform(5, 1500), 2),47 random.randint(1, 50000)]4849 # ...y cada tramo ensucia UN campo. 16.000 filas defectuosas (1,6%)50 # repartidas en cinco defectos distintos más los duplicados.51 if i < 4000:52 fila[1] = "" # fecha vacía53 elif i < 6000:54 fila[1] = "31/13/2024" # fecha imposible55 elif i < 9000:56 fila[5] = "" # cantidad vacía57 elif i < 10000:58 fila[6] = -fila[6] # precio negativo59 elif i < 15000:60 fila[4] = f" {ciudad} " # espacios alrededor61 elif i < 17000:62 # 1.000 parejas de filas idénticas, el id incluido: duplicado63 # exacto, que es lo que hay que deduplicar64 if (i - 15000) % 2 == 1:65 fila = list(ultima_fila)6667 writer.writerow(fila)68 ultima_fila = fila6970print("[OK] Dataset ventas.csv generado: 1,000,000 filas")71print(" Con pesos: Cable USB ~27%, Mouse ~20%, Laptop ~2%")72print(" Con suciedad: 16.000 filas defectuosas (1,6%)")73print(" 4.000 fecha vacía · 2.000 fecha 31/13/2024 · 3.000 cantidad vacía")74print(" 1.000 precio negativo · 5.000 ciudad con espacios")75print(" 1.000 parejas de filas idénticas (2.000 filas, 1.000 de sobra)")
Generamos 1 millón de ventas ficticias para practicar con datos realistas
## Paso 7: Leer el dataset con Spark
1# Leer el CSV con Spark2ventas = (3 spark.read4 .option("header", "true")5 .option("inferSchema", "true")6 .csv("/home/jovyan/data/ventas.csv")7)89# Verificar esquema y primeras filas10ventas.printSchema()11# root12# |-- id: integer13# |-- fecha: string14# |-- producto: string15# |-- categoria: string16# |-- ciudad: string17# |-- cantidad: integer18# |-- precio_unitario: double19# |-- cliente_id: integer2021ventas.show(5)22print(f"Total filas: {ventas.count():,}")23print(f"Particiones: {ventas.rdd.getNumPartitions()}")
Spark lee el CSV, infiere tipos y lo particiona automáticamente
## Comandos útiles del día a día
1# Levantar el cluster2docker compose up -d34# Ver logs en tiempo real5docker compose logs -f67# Parar el cluster (conserva datos)8docker compose stop910# Destruir el cluster (elimina contenedores, conserva volúmenes)11docker compose down1213# Destruir TODO incluyendo volúmenes14docker compose down -v1516# Reiniciar un servicio específico17docker compose restart spark-worker1819# Escalar workers (ej: 3 workers)20docker compose up -d --scale spark-worker=3
Comandos Docker Compose que usarás constantemente
Consejo de senior: añade estos alias a tu .bashrc o .zshrc para no escribir "docker compose" mil veces al día: alias spark-up="docker compose up -d" / alias spark-down="docker compose down" / alias spark-logs="docker compose logs -f"
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...
Regístrate para guardar tu progreso.