
Sesión 1 — Entorno Hadoop con contenedores¶
En esta primera sesión se pone en marcha el clúster Hadoop que se empleará durante todo el curso. El clúster ya está construido y publicado como imágenes de contenedor: aquí no se instala Hadoop, se arranca, se entiende y se comprueba un entorno distribuido pequeño con HDFS, YARN y MapReduce.
Este notebook mezcla explicación conceptual (celdas markdown) con órdenes que se ejecutan de verdad contra el clúster (celdas de código). No es necesario memorizar las órdenes; sí es importante entender en qué máquina se ejecuta cada una, con qué usuario y sobre qué sistema de ficheros actúa, porque esa distinción se repetirá en toda la asignatura.
Cómo se evalúa el trabajo de esta sesión. Cada sesión práctica termina con una sección «Evidencias para la siguiente sesión» que indica qué preparar. En una sesión posterior tendrás una reunión individual breve (unos minutos) con el profesor en la que deberás mostrar en vivo, en tu propio ordenador, las evidencias indicadas allí (el clúster en marcha, las órdenes ejecutadas, las interfaces web) y entregar una memoria breve (una o dos páginas) que explique lo realizado. Este esquema —evidencias en pantalla más memoria escrita, revisadas en una reunión breve— se repite en el resto de sesiones del curso.
Requisitos previos en Windows: WSL2 y Docker Desktop¶
Esta sesión ejecuta el kernel de Jupyter en tu propio equipo — a partir de S2
el kernel se ejecutará dentro del propio clúster, pero aquí todavía no existe
clúster que lo aloje. Si usas Windows, !docker no funciona solo con
WSL2 instalado: hace falta además Docker Desktop, correctamente
configurado. Sigue estos pasos antes de empezar:
Instala WSL2. Abre PowerShell como administrador y ejecuta:
wsl --install
Esto instala WSL2 y una distribución Ubuntu por defecto. Reinicia el equipo si te lo pide.
Instala Docker Desktop para Windows desde docker.com y, durante o después de la instalación, comprueba en Settings → General que "Use the WSL 2 based engine" está activado.
Activa la integración WSL para tu distribución. En Docker Desktop, ve a Settings → Resources → WSL Integration y activa el interruptor junto al nombre de tu distribución (por ejemplo,
Ubuntu). Este paso es el que más se olvida: sin él,dockerno se encuentra dentro de WSL2 aunque Docker Desktop esté instalado y en ejecución.Arranca Docker Desktop y espera a que quede en marcha (no basta con tenerlo instalado; el motor debe estar realmente arrancado).
Instala Visual Studio Code con las extensiones WSL, Python y Jupyter. Abre el proyecto desde dentro de WSL2 —por ejemplo, ejecutando
code .desde una terminal de Ubuntu, o con "Connect to WSL" desde la paleta de comandos—, no como una carpeta nativa de Windows. Es preferible clonar el repositorio dentro del sistema de ficheros de WSL2 (p. ej.~/tcdm-public) en lugar de/mnt/c/...: además de evitar problemas de integración, el rendimiento de E/S es mucho mejor.El kernel del notebook será un Python de WSL2 (no uno de Windows) y lo prepara Visual Studio Code. Al abrir
s1/s1.ipynb, pulsa Select Kernel → Python Environments → Create Python Environment → Venv: crea un entorno virtual.venven la carpeta abierta, instala en élipykernel(acepta si lo pide al ejecutar la primera celda) y lo deja elegido como kernel de la sesión.Instala
makey el soporte de entornos virtuales de Python en Ubuntu de WSL2.makese usa a partir de S2 (make -C entorno ...desde la terminal) y, sinpython3-venv, Visual Studio Code no puede crear el entorno del paso 6:sudo apt update && sudo apt install -y make python3-venv
Comprueba antes de continuar, en una terminal de WSL2:
docker --version docker compose version docker info make --version python3 --version
Si algún comando falla, revisa los pasos 2–4 (o el 7, si faltan
makeopython3) antes de seguir: el resto del notebook asume que estas órdenes ya funcionan desde la propia terminal de WSL2. La última muestra la versión de Python con la que Visual Studio Code creará el entorno del paso 6; anótala si tienes que consultar un problema con el kernel.
En Linux y macOS no hacen falta WSL2 ni la integración anterior: basta con tener Docker (Docker Engine en Linux, o Docker Desktop en macOS) instalado y en ejecución, y
makeen el equipo (en Linux, paquetemake; en macOS viene con las herramientas de línea de órdenes de Xcode,xcode-select --install). Sí se necesitan, igual que en Windows, Visual Studio Code con las extensiones Python y Jupyter y un Python en el equipo, con el que Visual Studio Code crea el entorno del kernel de esta sesión (en Debian y Ubuntu hace falta el paquetepython3-venv). A partir de S2 el kernel y todas las bibliotecas vienen ya instalados dentro del contenedornamenode.
Obtener el material de las sesiones¶
El material se distribuye en el repositorio público
dsevilla/tcdm-public, rama
26-27. En una terminal de tu equipo (en Windows, la terminal de Ubuntu de
WSL2):

git clone --depth 1 --branch 26-27 https://github.com/dsevilla/tcdm-public.git ~/tcdm-public
cd ~/tcdm-public
La animación abrevia la orden de clonado: la completa es la del bloque
anterior, que fija la rama 26-27 y el directorio de destino. Su última
orden, code . &, abre la carpeta en Visual Studio Code y devuelve el
control a la terminal.
A este directorio (~/tcdm-public) lo llamaremos raíz de la distribución.
Contiene un directorio por sesión (s1/, s2/, ...) y entorno/, con los
ficheros Docker Compose y el Makefile del laboratorio. entorno/ crece a
medida que se publican sesiones: cada una trae sólo lo que necesita. Antes de
empezar una sesión nueva, actualiza la copia desde la raíz:
git pull
Diapositivas de la sesión¶
Las diapositivas se generan con el paquete jupyter-notebook-slide.
El alias %%diapositiva permite mantener en español los tipos usados en la sesión y produce la misma salida HTML en Jupyter, Colab y la referencia HTML publicada.
En cada sesión encontrarás varios tipos de diapositivas que te ayudarán a seguir el hilo:
- Sección (
titulo): abre la sesión y resume qué vamos a ver y qué haremos. - A continuación (
avance): anuncia el contenido de la parte siguiente, para saber en qué punto del programa estamos. - Recapitulación (
resumen): condensa la parte anterior en puntos clave para recordarlos y revisarlos después. - Pregunta guía (
pregunta): plantea cuestiones que orientan lo que viene a continuación; conviene intentar responderlas antes de seguir. - Evaluación de la sesión (
evaluacion): recuerda cómo se evalúa el trabajo de esta sesión.
La celda siguiente instala y configura ese paquete: ejecútala una vez por
sesión de kernel, antes que cualquier celda %%diapositiva. Si no, esas
celdas fallan con UsageError: Cell magic %%diapositiva not found. Las
diapositivas acompañan la explicación; el trabajo práctico no depende de
ellas.
%pip install -q "jupyter-notebook-slide @ git+https://github.com/dsevilla/jupyter-notebook-slide.git"
%load_ext notebook_slide
import notebook_slide as jnbs
# Colores para TCDM.
jnbs.configure(
background="#eaf2f8",
foreground="#1f2933",
border="#c9d6e1",
heading="#0c304d",
subheading="#0c304d",
link="#174f7a",
code_background="#eaf2f8",
code_foreground="#0c304d",
quote_background="#ffffff",
font_family="Atkinson Hyperlegible, Inter, Aptos, Segoe UI, Helvetica, Arial, sans-serif",
font_url="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&display=swap",
)
jnbs.register_slide_type("avance", "A continuación", "#174f7a")
jnbs.register_slide_type("resumen", "Recapitulación", "#a54467")
jnbs.register_slide_type("pregunta", "Pregunta guía", "#0c304d")
jnbs.register_slide_type("evaluacion", "Evaluación de la sesión", "#b3701a")
jnbs.register_slide_type(
"titulo",
"Sección",
"#174f7a",
layout="title",
background="linear-gradient(135deg, #0c304d, #174f7a 62%, #a54467)",
foreground="#ffffff",
border="transparent",
heading="#ffffff",
subheading="#dceaf4",
)
jnbs.register_alias("diapositiva")
Hadoop, HDFS, YARN y MapReduce¶
Apache Hadoop es una plataforma de software libre para almacenar y procesar grandes conjuntos de datos mediante un clúster de ordenadores. Su diseño parte de una idea sencilla: cuando el volumen de información deja de caber o de procesarse cómodamente en una sola máquina, se reparten los datos y el trabajo entre varios nodos. El sistema debe coordinar esos nodos, aprovechar sus recursos y seguir funcionando cuando alguno deja de estar disponible.
En esta sesión intervienen tres piezas principales:
- HDFS proporciona almacenamiento distribuido;
- YARN negocia y asigna los recursos de cómputo del clúster;
- MapReduce proporciona un modelo y un motor de procesamiento distribuido que se ejecuta utilizando los recursos concedidos por YARN.
Hadoop puede acceder también a otros sistemas de almacenamiento, como el sistema local o almacenes de objetos compatibles con S3. En esta primera parte del curso se utiliza HDFS porque permite estudiar de forma explícita la división en bloques, la replicación y la relación entre almacenamiento y procesamiento. HDFS alojará tanto un data lake de ficheros Parquet como un data warehouse gobernado por un catálogo. Más adelante se repetirá parte del diseño sobre un almacenamiento de objetos compatible con Amazon S3 (sesión 3) para comparar ambos soportes.
HDFS como sistema de ficheros distribuido¶
HDFS, Hadoop Distributed File System, está diseñado para almacenar ficheros grandes en un conjunto de máquinas relativamente convencionales. Un fichero se divide en bloques y esos bloques se distribuyen entre los DataNodes. El cliente sigue viendo un único nombre de fichero: la división y la localización física de cada bloque quedan ocultas tras la interfaz de HDFS.
El sistema favorece el acceso secuencial con un ancho de banda elevado. No intenta comportarse como un disco local de baja latencia:
- funciona bien con ficheros grandes y lecturas completas o extensas;
- permite que varios nodos lean bloques diferentes en paralelo;
- obtiene tolerancia a fallos manteniendo réplicas de los bloques;
- tiene una latencia mayor que un sistema de ficheros local;
- es poco eficiente cuando se almacenan cantidades enormes de ficheros muy pequeños, porque cada uno requiere metadatos en el NameNode;
- está orientado a escribir una vez y leer muchas veces;
- no ofrece el mismo modelo de actualización aleatoria ni de múltiples escritores concurrentes que un sistema de ficheros POSIX.
Los conceptos esenciales de HDFS son:
- NameNode: mantiene el espacio de nombres, los permisos y la relación entre ficheros, bloques y DataNodes. Coordina el acceso, pero no guarda el contenido ordinario de todos los ficheros de usuario.
- DataNode: almacena bloques en su sistema de ficheros local y atiende las lecturas y escrituras solicitadas por los clientes.
- Bloque HDFS: unidad lógica en la que se divide un fichero. En este laboratorio se utilizan bloques de 64 MiB para que sea más fácil observar su distribución con datos de tamaño moderado.
- Replicación: mantenimiento de varias copias de un bloque en DataNodes diferentes. El laboratorio solicita tres réplicas porque dispone de tres DataNodes.
- Espacio de nombres: jerarquía de directorios y ficheros que presenta HDFS. Se parece visualmente a un sistema Unix, pero no es la jerarquía local de ninguno de los contenedores.
Una instalación de producción necesita además una estrategia de alta disponibilidad para el NameNode (NameNodes activo/standby, JournalNodes, etc.). El laboratorio local utiliza un único NameNode y no simula esa parte de una instalación de producción.
YARN como gestor de recursos¶
YARN, Yet Another Resource Negotiator, separa la gestión de los recursos del clúster del motor que procesa los datos. Gracias a esa separación, distintas aplicaciones (MapReduce, y más adelante Spark) pueden solicitar CPU y memoria sin que cada una implemente su propio planificador global.
- el ResourceManager mantiene la visión global de los recursos y decide dónde se pueden asignar contenedores de ejecución;
- cada NodeManager anuncia la CPU y la memoria disponibles en su nodo, inicia los contenedores que le asigna YARN y supervisa su ejecución;
- cada aplicación utiliza un ApplicationMaster, que negocia recursos para sus tareas y sigue su progreso.
Un contenedor YARN no es necesariamente un contenedor Docker: en la terminología de YARN es una asignación de recursos (memoria y vcores) en un NodeManager. En este laboratorio esos contenedores YARN se ejecutan dentro de los contenedores Docker que representan los nodos del clúster.
MapReduce como modelo de procesamiento¶
MapReduce organiza un cálculo distribuido alrededor de dos tipos de función. Las tareas map leen fragmentos de la entrada y producen pares clave-valor intermedios. Hadoop agrupa y transfiere esos pares durante la fase de shuffle. Las tareas reduce reciben los valores asociados a cada clave y generan el resultado final.
MapReduce no es el único motor que puede trabajar sobre Hadoop: Spark también lee HDFS y solicita recursos a YARN, y se usará más adelante en la asignatura (sesión 4). WordCount se ejecuta aquí con MapReduce porque permite observar con pocas órdenes todo el recorrido de una aplicación distribuida:
lusercopia varios ficheros de entrada a HDFS;- el NameNode registra sus nombres, bloques y ubicaciones;
- los DataNodes almacenan las réplicas de esos bloques;
- el cliente envía WordCount al ResourceManager;
- el ApplicationMaster solicita contenedores a YARN;
- los NodeManagers ejecutan las tareas map y reduce;
- el resultado vuelve a escribirse en HDFS.
Seguir este flujo permite distinguir entre dónde están los datos (responsabilidad de HDFS) y dónde se ejecutan las tareas (decisión de YARN).
Referencias¶
Para profundizar en estos temas, se recomienda:
- White, Tom (2015). Hadoop: The Definitive Guide (4ª edición). O'Reilly Media. — La referencia clásica y completa sobre Hadoop, HDFS, YARN y MapReduce.
- Miner, Donald & Shook, Adam (2018). MapReduce Design Patterns (2ª edición). O'Reilly Media. — Patrones y mejores prácticas para escribir aplicaciones MapReduce eficientes.
- Lin, Jimmy & Dyer, Chris (2010). Data-Intensive Text Processing with MapReduce. Morgan & Claypool. — Enfoque práctico en procesamiento de texto a gran escala.
Por qué existe Big Data y por qué apareció MapReduce¶
Big Data (Wikipedia): se refiere al estudio y a las aplicaciones de conjuntos de datos tan grandes y complejos que las aplicaciones tradicionales de procesamiento de datos resultan inadecuadas para tratarlos.
La definición señala tres retos que siguen vigentes: el volumen de datos que hay que almacenar, la velocidad con la que se generan y se necesitan procesar, y la variedad de formatos —texto, sensores, registros de aplicación, vídeo— que conviven en un mismo sistema. Ninguno de los tres se resuelve comprando un ordenador más grande: en algún momento un único disco, una única memoria o un único procesador dejan de ser suficientes, y hay que repartir los datos y el trabajo entre varias máquinas.
Ese cambio de escala llegó con una motivación muy concreta. En 2003 y 2004, Google publicó dos artículos —The Google File System (Ghemawat, Gobioff y Leung) y MapReduce: Simplified Data Processing on Large Clusters (Dean y Ghemawat)— que describían cómo almacenar y procesar datos sobre miles de ordenadores convencionales, asumiendo que algunos de ellos fallarían mientras el trabajo estuviera en marcha. Hacia 2005, Doug Cutting y Mike Cafarella se basaron en esas ideas para crear Apache Hadoop, el proyecto de código abierto que reimplementa GFS como HDFS y ese modelo de programación como MapReduce. YARN se añadió después, en Hadoop 2, para separar la gestión de recursos del clúster del motor de procesamiento concreto que los utiliza; por eso hoy MapReduce es una de varias aplicaciones que pueden pedir recursos a YARN, y Spark —que se usará en una sesión posterior— es otra.
Esa historia explica varias decisiones de diseño que se van a comprobar en esta misma sesión: por qué HDFS divide los ficheros en bloques grandes y los replica en vez de confiar en un único disco, por qué el cómputo se envía a donde están los datos en lugar de mover los datos al cómputo, y por qué el sistema está pensado para seguir funcionando —no para bloquearse— cuando un nodo deja de responder.
Retos tecnológicos de Big Data¶
La gestión de Big Data requiere abordar múltiples desafíos tecnológicos en diferentes áreas:
Búsqueda y refinamiento¶
Para localizar y limpiar datos de múltiples fuentes dispersas y heterogéneas:
- Lucene
- Solr
- Elasticsearch
- OpenSearch — fork abierto de Elasticsearch tras el cambio de licencia
- Meilisearch — motor de búsqueda ligero y moderno
- OpenRefine
Serialización¶
Transformar datos entre distintos sistemas y formatos de almacenamiento:
- JSON
- BSON
- Apache Avro
- Google Protocol Buffers
- Apache Arrow — formato columnar en memoria, base de Pandas, Polars y DuckDB
- gRPC — RPC moderno construido sobre Protocol Buffers
Sistemas de almacenamiento¶
Distribuir el almacenamiento entre múltiples servidores para máximo rendimiento:
- HDFS
- Amazon S3
- MinIO — almacenamiento de objetos compatible con S3, autoalojable
- RustFS — almacenamiento de objetos compatible con S3 escrito en Rust; es el que usa el laboratorio en la sesión 3
- Google Cloud Storage
- Azure Data Lake Storage
Servidores¶
Infraestructura en la nube para escalar recursos según demanda:
- Amazon EC2
- Google Cloud Platform
- Azure
- OpenShift
- Tanzu
- Kubernetes — orquestación de contenedores, estándar de facto para desplegar clústeres
- Docker — la tecnología de contenedores usada en este propio laboratorio
Procesamiento¶
Plataformas y herramientas para procesar datos distribuidos:
- Hadoop
- Hive
- Spark
- Flink
- Kafka — la plataforma de streaming de eventos más extendida
- Airflow
- Dagster — orquestador de datos moderno, alternativa a Airflow
- dbt — transformación de datos como código dentro del almacén analítico
- Beam
- Dask
- Trino — motor de consultas SQL distribuido sobre múltiples fuentes
- DuckDB — motor analítico embebido, muy popular para análisis local
- Polars — DataFrames de alto rendimiento, alternativa moderna a pandas
- Kinesis
Bases de datos¶
Almacenamiento de datos estructurados y semiestructurados a gran escala:
- HBase
- Cassandra
- MongoDB
- Amazon DynamoDB
- Google BigTable
- Parquet
- Apache Iceberg — formato de tabla abierto sobre almacenamiento de objetos
- Delta Lake — formato de tabla con transacciones ACID, alternativa a Iceberg
- ClickHouse — base de datos analítica columnar de muy alto rendimiento
- Snowflake — almacén de datos en la nube totalmente gestionado
- BigQuery — almacén de datos serverless de Google Cloud
Análisis y BI¶
Análisis estadístico y visualización de datos:
- R
- Greenplum
- Splunk
- Tableau
- PowerBI
- Apache Superset — BI de código abierto
- Metabase — BI de código abierto sencillo de desplegar
Procesamiento de lenguaje natural¶
Análisis de contenidos textuales generados por usuarios:
- Natural Language Toolkit
- spaCy — librería de NLP de producción, más rápida que NLTK
- Hugging Face Transformers — modelos de lenguaje preentrenados y pipelines de NLP
Machine Learning y Deep Learning¶
Automatización de decisiones basadas en datos:
- Weka
- SciKit-learn
- PyTorch
- TensorFlow
- Keras
- Hugging Face — repositorio y herramientas para modelos preentrenados
- XGBoost — gradient boosting de referencia para datos tabulares
- LightGBM — gradient boosting rápido, alternativa a XGBoost
- JAX — computación numérica de alto rendimiento para investigación en ML
Visualización¶
Representación gráfica de datos para facilitar su comprensión:
- R
- D3.js
- Looker Studio — antes Google Data Studio
- Observable — cuadernos interactivos para visualización con D3
Panorama de tecnologías Big Data¶
El ecosistema de Big Data es amplio y en continua evolución. La siguiente imagen muestra el panorama de 2021 con las principales categorías y soluciones disponibles:

Procesamiento del Big Data: por qué Hadoop existe¶
Cuando los datos dejan de caber en una máquina, la solución obvia es usar varias máquinas. Pero hay un dilema económico:
Opción A: máquinas caras y confiables. Si compras servidores enterprise de alta calidad, son relativamente fiables, pero muy caros. Procesar petabytes de datos exigiría una inversión gigantesca en hardware.
Opción B: máquinas baratas que fallan. Comprar cientos o miles de máquinas commodity (baratas, convencionales) es mucho más económico. El problema es que fallan continuamente. Con miles de máquinas, algo está fallando casi todo el tiempo.
La respuesta de Hadoop: usar la opción B, pero asumir y gestionar automáticamente los fallos en el software. No es que Hadoop tolere los fallos a pesar de usar máquinas baratas; es que la tolerancia a fallos automática es lo que hace económicamente viable usar máquinas baratas en primer lugar.
Para que esto funcione, necesitamos:
- Escalabilidad con cientos o miles de máquinas y decenas de miles de discos
- Equipos y redes de bajo coste (aunque poco fiables y con elevadas latencias)
- Tolerancia a fallos automática — no como un lujo, sino como requisito fundamental
- Facilidad de programación — el usuario no debe pensar en fallos; el sistema los gestiona
Problemas en la ejecución y cómo los resuelve Hadoop¶
La realidad de usar máquinas baratas plantea tres desafíos que Hadoop debe resolver:
Desafío 1: Los nodos baratos fallan constantemente¶
El problema:
- Tiempo medio entre fallos (MTBF) para 1 nodo: ~3 años
- Tiempo medio entre fallos para 1.000 nodos: ~1 día
Con 10.000 nodos, el modelo proporcional sugiere que algo falla aproximadamente cada 2,6 horas. Sin tolerancia a fallos automática, el trabajo nunca terminaría.
Cómo lo resuelve Hadoop:
- HDFS replica cada bloque en varios nodos (típicamente 3). Cuando un nodo falla, los datos siguen disponibles en otros.
- MapReduce y YARN relanzan automáticamente tareas fallidas en otros nodos.
- El usuario no necesita intervenir: el sistema detecta el fallo, reasigna el trabajo y continúa.
Desafío 2: Las redes baratas tienen elevada latencia¶
El problema:
- Una red de centro de datos enterprise es cara y rápida.
- Una red que conecta cientos de máquinas baratas tiene mucha latencia.
- Mover petabytes por la red es lentísimo.
Cómo lo resuelve Hadoop:
- Llevar la computación a los datos, no los datos a la computación.
- MapReduce lanza tareas map en los nodos donde están los bloques de entrada.
- El resultado local se procesa antes de transferirse por la red.
- Resultado: minimiza el movimiento de datos entre máquinas.
Desafío 3: Programar un sistema distribuido es difícil¶
El problema:
- Sin abstracciones, los usuarios tendrían que escribir código para:
- Particionar datos entre nodos
- Serializar y deserializar
- Detectar y recuperar fallos
- Sincronizar trabajo distribuido
- Esto es tan complejo que la mayoría de errores sería no por lógica de negocio, sino por gestión distribuida.
Cómo lo resuelve Hadoop:
- MapReduce oculta la complejidad distribuida tras un modelo de programación simple.
- El usuario escribe dos funciones:
map()yreduce().map()procesa pares clave-valor y emite pares intermedios.reduce()agrupa valores por clave.
- El sistema Hadoop:
- Distribuye el trabajo automáticamente
- Gestiona serialización y transferencia de datos
- Detecta y recupera fallos sin intervención del usuario
- Coordina tareas map y reduce
Conclusión: La combinación de HDFS (almacenamiento tolerante a fallos), MapReduce (modelo de programación simple) y YARN (coordinación de recursos) hace posible que un usuario escriba map() y reduce() sin pensar en cientos de máquinas que fallan. Eso es lo que hace a Hadoop revolucionario.
Objetivos de la sesión¶
Al finalizar esta sesión deberías poder:
- describir la arquitectura de Hadoop y sus componentes principales;
- explicar la configuración de un clúster Hadoop en contenedores Docker;
- iniciar, detener y regenerar el clúster con Docker Compose;
- distinguir el sistema de ficheros local de cada contenedor del espacio de nombres distribuido de HDFS;
- relacionar NameNode y DataNode con el almacenamiento de datos;
- relacionar ResourceManager y NodeManager con la ejecución de trabajos;
- consultar el estado de los tres nodos de trabajo;
- crear, copiar, mover, leer y borrar ficheros en HDFS como
luser; - interpretar el factor de replicación y la ubicación de los bloques;
- enviar un trabajo MapReduce a YARN y consultar su resultado;
- recuperar un clúster limpio después de una prueba fallida.
El entorno debe quedar operativo al terminar la sesión porque se reutilizará en sesiones posteriores con Parquet/Arrow, Spark, Trino y tablas Iceberg.
Antes de empezar¶
Se necesita Docker Engine o Docker Desktop con Docker Compose v2, iniciado y
con capacidad para descargar las imágenes publicadas del curso. Para esta
sesión basta con asignar al menos 8 GiB de memoria a Docker y detener otros
contenedores que no sean necesarios. Los límites del Compose (4 GiB para
namenode y 3 GiB por DataNode) son techos, no reservas: el clúster de S1
usa bastante menos. Las sesiones con Spark, a partir de S4, se acercan más a
esos límites y conviene subir la memoria de Docker hacia 14 GiB si el equipo
lo permite.
Las celdas de este notebook suponen que Jupyter usa como directorio de
trabajo el propio directorio del notebook (s1/), que es hermano de
entorno/ en la distribución de sesiones. Por eso las rutas del entorno
se escriben como ../entorno/....
Dónde se ejecuta cada orden¶
En esta sesión el kernel de Jupyter se ejecuta en tu equipo, fuera del clúster. Casi todas las celdas tienen esta forma:
!docker exec namenode su - luser -c 'hdfs dfs -ls /'
│ │ │ │ └─ orden que se ejecuta en el contenedor
│ │ │ └─ como el usuario luser, con su entorno de login
│ │ │ (PATH con hdfs, yarn, java...)
│ │ └─ dentro del contenedor namenode
│ └─ el cliente Docker de tu equipo entra en un contenedor
└─ Jupyter pasa el resto de la línea a una shell de tu equipo
Por tanto hay tres niveles:
| Nivel | Ejemplo | Dónde se ejecuta |
|---|---|---|
| Shell de tu equipo | !docker compose -f ../entorno/compose-hadoop-cluster.yml ps |
En tu equipo; sólo necesita el cliente Docker |
| Contenedor | !docker exec namenode ... |
Dentro de namenode (o del datanodeN que se indique) |
| Usuario del contenedor | su - hdadmin -c '...' o su - luser -c '...' |
hdadmin administra los demonios; luser trabaja con los datos |
hdfs, yarn o java no están instalados en tu equipo: sólo existen
dentro de los contenedores. Por eso !hdfs dfs -ls / a secas falla en esta
sesión. A partir de S2 el kernel se ejecutará dentro de namenode y la misma
orden se escribirá sin docker exec.
Comprobar el cliente Docker¶
La salida debe identificar una versión de Docker. Esto sólo confirma que existe el cliente; todavía no demuestra que el motor de contenedores esté en ejecución.
!docker --version
Comprobar Docker Compose¶
El clúster está descrito como un conjunto de servicios coordinados que se
gestionan con el complemento Compose v2, invocado como parte de la orden
docker. Se espera una salida similar a Docker Compose version v....
!docker compose version
Comprobar el motor de contenedores¶
Esta orden contacta con el daemon de Docker y muestra información del equipo y de los recursos que administra. Si aparece un error como Cannot connect to the Docker daemon, hay que iniciar Docker Desktop o el servicio Docker antes de continuar: repetir las celdas de Hadoop no soluciona un daemon que no está disponible.
!docker info
El clúster que se va a utilizar¶
El laboratorio simula un clúster de cuatro ordenadores mediante cuatro contenedores que comparten una red Docker, tienen nombres DNS estables dentro de ella y pueden comunicarse como si fueran hosts separados.
| Contenedor | Servicios | Responsabilidad principal |
|---|---|---|
namenode |
NameNode, ResourceManager y Timeline Server | Metadatos de HDFS, coordinación de recursos de cómputo e histórico de aplicaciones YARN |
datanode1 |
DataNode y NodeManager | Almacena bloques y ejecuta tareas |
datanode2 |
DataNode y NodeManager | Almacena bloques y ejecuta tareas |
datanode3 |
DataNode y NodeManager | Almacena bloques y ejecuta tareas |
Abrir SVG editable · Descargar PNG
El NameNode no contiene una copia completa de los ficheros de usuario: conserva el espacio de nombres, los permisos y la relación entre ficheros, bloques y DataNodes; los DataNodes son quienes almacenan el contenido de los bloques. De forma análoga, el ResourceManager decide dónde se pueden ejecutar aplicaciones, mientras que los NodeManagers ofrecen recursos y ponen en marcha sus tareas.
El Timeline Server recibe y conserva información sobre las aplicaciones YARN
para poder consultarlas después de que terminen. En este laboratorio comparte
el contenedor del NameNode y del ResourceManager, aunque se publica con el
nombre DNS timelineserver para que los clientes usen una dirección
coherente con una instalación en la que fuera un host separado.
Recursos asignados¶
| Contenedor | Límite Docker | Recursos anunciados a YARN |
|---|---|---|
namenode |
2 vCPU y 4096 MiB | No ejecuta NodeManager |
datanode1 |
2 vCPU y 3072 MiB | 2 vcores y 2560 MiB |
datanode2 |
2 vCPU y 3072 MiB | 2 vcores y 2560 MiB |
datanode3 |
2 vCPU y 3072 MiB | 2 vcores y 2560 MiB |
El límite de memoria de Docker y la memoria anunciada a YARN no son la
misma cosa. Cada DataNode puede consumir como máximo 3072 MiB, pero el
NodeManager sólo ofrece 2560 MiB a los contenedores de las aplicaciones; los
512 MiB restantes permiten ejecutar el propio DataNode, el NodeManager y
otros procesos auxiliares. namenode dispone de 4096 MiB porque en
sesiones posteriores alojará también Jupyter y el driver de Spark. Los dos
vcores de cada DataNode permiten que YARN ubique más adelante un
ApplicationMaster y un executor de Spark sin dejar el nodo sin capacidad para
otras tareas.
Imágenes del curso¶
Docker Compose descarga dos imágenes ya preparadas:
| Imagen | Contenedores que la utilizan |
|---|---|
dsevilla/namenode-image:26-27 |
namenode |
dsevilla/datanode-image:26-27 |
Los tres datanodeN |
Un mismo contenedor DataNode ejecuta también un NodeManager porque en Hadoop
resulta habitual acercar el procesamiento a los datos. Ambas imágenes
incorporan Hadoop 3.5.0 y Java 17; los demonios se ejecutan como hdadmin y
los ejercicios como luser. Un poco más abajo se explica cómo se construyen
estas imágenes, aunque no es necesario reconstruirlas para seguir la
sesión.
Identidades y seguridad del laboratorio¶
HDFS utiliza autenticación simple: cada cliente se presenta con su propia
identidad. Cuando se ejecuta su - luser, Hadoop realiza las operaciones
como luser; con su - hdadmin, como el usuario administrativo. La
autenticación simple permite centrarse en HDFS y YARN, pero no demuestra
criptográficamente quién es el cliente: este entorno sólo es apropiado para
docencia local, sus puertos publicados por Compose están limitados a
127.0.0.1 y el clúster no debe exponerse a una red externa o a Internet.
Directorios HDFS iniciales¶
Durante la construcción de la imagen del NameNode se formatea HDFS y se crean, entre otros, estos directorios:
/user/hdadmin
/user/luser
/tmp
/tmp/hadoop-yarn/staging
/tmp/logs
/datalake
/datalake/raw
/datalake/raw/tpcds
/datalake/silver
/datalake/gold
/warehouse
/user/luser es el directorio de trabajo del usuario de las sesiones. Los
directorios temporales tienen sticky bit, de forma parecida a /tmp en un
sistema Unix.
/datalake y /warehouse pertenecen a luser:hadoop con permisos 770.
A partir de la sesión 2, Jupyter se ejecutará en namenode, y desde allí
se lanzarán como luser los clientes Python y los trabajos Spark. Esos
clientes leerán HDFS por HTTP (WebHDFS) y recibirán redirecciones hacia
datanode1, datanode2 y datanode3: esos nombres serán resolubles porque
todos los contenedores están conectados a la misma red Docker
hadoop-cluster.
Cómo se construyen las imágenes del curso¶
El alumnado utiliza imágenes ya publicadas; esta sección resume cómo se incorporan los componentes y la configuración que se observan durante la sesión — no es necesario reconstruirlas para seguir el resto del notebook.
Tres imágenes relacionadas¶
La construcción separa los elementos comunes de la configuración de cada tipo de nodo:
| Imagen | Contenido añadido | Imagen de partida |
|---|---|---|
dsevilla/hadoop-base:26-27 |
Ubuntu, Java, Hadoop, utilidades y usuarios | ubuntu:26.04 |
dsevilla/namenode-image:26-27 |
Configuración de NameNode y ResourceManager, HDFS inicial | hadoop-base:26-27 |
dsevilla/datanode-image:26-27 |
Configuración de DataNode y NodeManager | hadoop-base:26-27 |
Esta separación evita repetir la descarga e instalación de Hadoop en las dos imágenes de servicio, y distingue qué decisiones pertenecen a todo cliente Hadoop y cuáles son propias del nodo que guarda metadatos o del nodo que almacena bloques.
Receta de la imagen base¶
FROM ubuntu:26.04
ARG HADOOP_VERSION=3.5.0
ENV HADOOP_VERSION=${HADOOP_VERSION} \
JAVA_HOME=/usr/lib/jvm/java-17-openjdk \
HADOOP_HOME=/opt/bd/hadoop \
HADOOP_CONF_DIR=/opt/bd/hadoop/etc/hadoop
ARG permite elegir la versión durante la construcción; ENV deja
disponibles dentro de la imagen las rutas que usarán scripts y clientes.
$HADOOP_HOME no apunta directamente a un directorio con el número de
versión, sino al enlace /opt/bd/hadoop, de modo que los comandos y la
configuración no cambian de ruta al actualizar Hadoop.
La imagen instala Java 17 sin entorno gráfico, Python, pip, entornos
virtuales, Maven y varias herramientas de terminal, además de tini (proceso
inicial del contenedor, reenvía señales y recoge procesos terminados). El
paquete python3 es el intérprete de Ubuntu 26.04, incluido para utilidades
y sesiones posteriores; el código Python nuevo del curso se escribe para
Python 3.13, y no debe deducirse la versión objetivo del lenguaje a partir de
que este cliente esté instalado en la imagen Hadoop. El patrón de instalación
y limpieza es:
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install --no-install-recommends -y \
ca-certificates curl tini openjdk-17-jdk-headless \
python3 python3-venv python3-pip maven \
libarchive-tools git make iputils-ping
apt-get clean
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* /tmp/* /var/tmp/*
El Dockerfile sigue añadiendo otros paquetes, hadoop y configurando los usuarios luser y hdadmin.
Dockerfiles de las imágenes de servicio¶
ARG BASE_IMAGE=dsevilla/hadoop-base:26-27
FROM ${BASE_IMAGE}
# Durante la construcción se escribe la configuración específica del NameNode.
COPY namenode.sh /tmp/
RUN sh /tmp/namenode.sh && \
rm -f /tmp/namenode.sh
# Entorno Python del curso (Jupyter, PySpark y bibliotecas de S2-S8).
COPY requirements-namenode.txt install-python-environment.sh /tmp/
RUN sh /tmp/install-python-environment.sh && \
rm -f /tmp/install-python-environment.sh /tmp/requirements-namenode.txt
EXPOSE 9870 9871 8088 8000-10000
CMD ["/inicio.sh"]
El Dockerfile del DataNode es equivalente, pero escribe la configuración de
los DataNodes y del NodeManager y publica sus puertos característicos
(9864 9865 9866 9867 50000-50200 8000-10000). En ambos casos la
configuración se escribe durante la construcción, no cuando el alumnado
ejecuta docker compose up; la diferencia entre ambas imágenes está
principalmente en hdfs-site.xml, en yarn-site.xml, en el contenido de
/inicio.sh y en el entorno Python del curso, que sólo lleva el NameNode.
Entorno Python del curso (sólo en el NameNode)¶
A partir de S2, Jupyter y el driver de Spark se ejecutan dentro de
namenode, así que su imagen incluye el entorno Python que usarán las
sesiones. Ubuntu marca el python3 del sistema como gestionado
externamente (PEP 668): un pip install sobre él falla con el error
externally-managed-environment. Por eso se crea un entorno virtual aparte,
/opt/tcdm/venv, y se instalan en él Jupyter Lab, PySpark y las bibliotecas
de S2-S8:
python3 -m venv /opt/tcdm/venv
/opt/tcdm/venv/bin/python -m pip install --no-cache-dir \
-r /tmp/requirements-namenode.txt
chown -R luser:hadoop /opt/tcdm
cat > /etc/profile.d/tcdm-python.sh <<'EOF'
export PATH="/opt/tcdm/venv/bin:$PATH"
EOF
/etc/profile.d/tcdm-python.sh pone /opt/tcdm/venv/bin al principio del
PATH de los shells de login: tras su - luser, python, python3, pip,
jupyter y spark-submit son los de este entorno. Será el intérprete del
kernel de Jupyter desde S2 y, por tanto, el lugar donde acaba todo
%pip install de un notebook. luser es su propietario, de modo que puede
instalar paquetes sin privilegios de administrador.
Los DataNodes no llevan este entorno: cuando Spark envía trabajo a YARN,
spark-submit distribuye a los ejecutores pyspark.zip, py4j y los .jar
de Spark, y éstos sólo necesitan el python3 de la imagen base.
Usuarios y entorno de ejecución¶
groupadd --system hadoop
useradd --system --create-home --gid hadoop \
--home-dir /opt/bd --shell /bin/bash hdadmin
useradd --system --create-home --gid hadoop \
--home-dir /home/luser --shell /bin/bash luser
hdadmin posee la instalación de /opt/bd y ejecuta los demonios; luser
posee /home/luser, pertenece también al grupo hadoop y representa al
cliente ordinario. Compartir grupo permite preparar recursos docentes
comunes sin ejecutar los ejercicios como administrador. /etc/profile.d/hadoop.sh
configura todos los shells de inicio (JAVA_HOME, HADOOP_HOME,
HADOOP_CONF_DIR, PATH), por lo que luser puede escribir hdfs, yarn o
mapred sin indicar su ruta absoluta.
Formateo e inicialización incluidos en la imagen¶
El NameNode necesita un espacio de nombres formateado antes del primer
arranque. Durante la construcción de namenode-image:
mkdir -p /var/data/hdfs/namenode
chown -R hdadmin:hadoop /var/data/hdfs/namenode
hdfs namenode -format -force -nonInteractive
Este formateo se ejecuta al construir la imagen, no cada vez que arranca un contenedor: repetirlo sobre un NameNode con datos generaría un nuevo identificador de clúster y haría que los DataNodes existentes dejasen de pertenecer a él. La construcción arranca temporalmente el NameNode, espera a que salga del modo seguro y crea la estructura inicial:
hdfs dfs -mkdir -p \
/user/hdadmin /user/luser \
/tmp /tmp/logs /tmp/hadoop-yarn/staging \
/datalake/raw/tpcds /datalake/silver /datalake/gold /warehouse
hdfs dfs -chmod 1777 /tmp /tmp/logs /tmp/hadoop-yarn/staging
hdfs dfs -chown luser /user/luser
hdfs dfs -chown -R luser:hadoop /datalake
hdfs dfs -chmod 770 \
/datalake /datalake/raw /datalake/raw/tpcds \
/datalake/silver /datalake/gold
hdfs dfs -chown luser:hadoop /warehouse
hdfs dfs -chmod 770 /warehouse
Después el NameNode se detiene de nuevo; el estado formateado y esos directorios pasan a formar parte de la imagen publicada. El metastore y Trino no se incluyen en esta imagen: se arrancarán como servicios independientes en la misma red y accederán remotamente a HDFS (a partir de la sesión 2; se estudian a fondo en las sesiones 6 y 7).
Arranque automático de los demonios¶
La imagen del NameNode termina generando /inicio.sh:
#!/usr/bin/env sh
set -eu
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk
export HADOOP_HOME=/opt/bd/hadoop
export HADOOP_CONF_DIR="$HADOOP_HOME/etc/hadoop"
su - hdadmin -c "JAVA_HOME=${JAVA_HOME} HADOOP_HOME=${HADOOP_HOME} HADOOP_CONF_DIR=${HADOOP_CONF_DIR} ${HADOOP_HOME}/bin/hdfs --daemon start namenode"
su - hdadmin -c "JAVA_HOME=${JAVA_HOME} HADOOP_HOME=${HADOOP_HOME} HADOOP_CONF_DIR=${HADOOP_CONF_DIR} ${HADOOP_HOME}/bin/yarn --daemon start resourcemanager"
su - hdadmin -c "JAVA_HOME=${JAVA_HOME} HADOOP_HOME=${HADOOP_HOME} HADOOP_CONF_DIR=${HADOOP_CONF_DIR} ${HADOOP_HOME}/bin/yarn --daemon start timelineserver"
# Bucle infinito
exec tail -f /dev/null
El Dockerfile declara este script como CMD, así que se ejecuta al crear el
contenedor; los demonios se inician como hdadmin, no como root. El
proceso tail mantiene activo el contenedor tras dejar los servicios en
segundo plano, y tini (declarado en la imagen base) permanece como proceso
inicial gestionando correctamente las señales. En la salida de jps, el
Timeline Server aparece con el nombre de clase Java
ApplicationHistoryServer, aunque las órdenes de Hadoop lo gestionen como
servicio timelineserver. Los DataNodes generan una versión equivalente que
arranca datanode y nodemanager en vez de namenode y
resourcemanager/timelineserver. Esto explica por qué docker compose start vuelve a poner en marcha los servicios sin que el alumnado ejecute
manualmente hdfs --daemon start en cuatro terminales.
Las imágenes publicadas incluyen variantes linux/amd64 y linux/arm64;
Docker selecciona automáticamente la adecuada para el equipo al descargar
la etiqueta 26-27.
Preparar la red del laboratorio¶
El fichero Compose declara hadoop-cluster como una red externa. Esto
hace que su ciclo de vida sea independiente del clúster Hadoop y permitirá
conectar más adelante otros contenedores —Trino, PostgreSQL, Hive
Metastore— sin incluirlos en el mismo fichero Compose.
La siguiente orden primero comprueba si la red ya existe y sólo la crea si hace falta; crear la red no arranca ningún contenedor, únicamente prepara el espacio de red y el DNS interno que compartirán los servicios. Si no imprime nada es que la red ya existía.
!docker network inspect hadoop-cluster >/dev/null 2>&1 || docker network create hadoop-cluster
Arrancar el clúster¶
La siguiente orden lee el fichero Compose, descarga las imágenes que falten
en el equipo y crea los cuatro contenedores. -d (detached) los deja en
segundo plano, de modo que la celda termina en cuanto están creados. La
primera ejecución puede tardar varios minutos, porque tiene que descargar
las dos imágenes; las siguientes suelen ser mucho más rápidas.
!docker compose -f ../entorno/compose-hadoop-cluster.yml up -d
Ver el estado de los contenedores¶
Se espera que namenode y los tres datanodeN aparezcan en estado
running. Que un contenedor esté en ejecución sólo significa que su proceso
principal sigue vivo: los demonios Hadoop necesitan además unos segundos para
arrancar y registrarse. Las comprobaciones de la siguiente sección son las
que confirman que el clúster está realmente preparado.
Si algún contenedor no aparece o figura como exited, o si las
comprobaciones siguientes siguen fallando después de esperar, ve a
«Diagnosticar un arranque incompleto», al final de este notebook.
!docker compose -f ../entorno/compose-hadoop-cluster.yml ps
Observar el clúster desde el navegador¶
Cuando el clúster esté listo, puedes abrir estas direcciones desde el
navegador del equipo anfitrión (usan localhost porque Docker publica esos
puertos sólo en la interfaz local del equipo; no hay que sustituir
localhost por la dirección de un DataNode):
- NameNode/HDFS: http://localhost:9870 (prueba también Utilities → Browse the file system para relacionar las órdenes de terminal con los ficheros visibles desde la interfaz)
- ResourceManager/YARN: http://localhost:8088
- YARN Timeline Server: http://localhost:8188/applicationhistory/ (la barra
final es necesaria en esta versión de Hadoop; sin ella el servidor responde
404 Not Found)
El Timeline Server no añade un quinto contenedor: el alias Docker
timelineserver apunta a namenode, donde el proceso
ApplicationHistoryServer se ejecuta junto al NameNode y al ResourceManager,
con su propio almacén LevelDB. Este servicio no es el JobHistoryServer
de MapReduce: el Timeline Server recoge el histórico genérico de
aplicaciones YARN (el que se usa en esta sesión), mientras que el
JobHistoryServer es otro servicio para los eventos detallados de trabajos
MapReduce.
Estas interfaces son una ayuda de observación; las comprobaciones reproducibles se hacen también desde este notebook.
Ejecutar las órdenes desde una terminal del contenedor¶
A partir de aquí, casi todas las celdas siguen el patrón
!docker exec namenode su - hdadmin -c '<orden>' o
!docker exec namenode su - luser -c '<orden>'. Cada celda lanza desde el
equipo anfitrión una única orden dentro del contenedor namenode como el
usuario indicado: hdadmin para las tareas de administración de los
demonios (dfsadmin, fsck, yarn node...) y luser para el trabajo
habitual con datos.
Las mismas órdenes se pueden escribir directamente en una terminal dentro de
namenode. Desde una terminal del equipo (en Windows, la de WSL2):
docker exec -it namenode bash
Ya dentro del contenedor, se cambia al usuario que aparezca en la celda y se
escribe la orden que va entre comillas tras -c. Para las celdas con
hdadmin:
su - hdadmin
hdfs dfsadmin -report
y para las celdas con luser:
su - luser
hdfs dfs -ls /
exit vuelve al usuario anterior y, repetido, cierra la terminal del
contenedor. Las celdas del notebook usan la forma con docker exec porque
así cada paso queda reproducible y se puede repetir de forma aislada; la
terminal es cómoda para probar variantes o encadenar varias órdenes seguidas
con el mismo usuario. Lo mismo vale para los DataNodes, cambiando namenode
por datanode1, datanode2 o datanode3.
Comprobar HDFS y YARN¶
hdfs dfsadmin -report pregunta al NameNode por los DataNodes registrados.
Tras el resumen de capacidad debe aparecer la línea Live datanodes (3):,
seguida de un bloque por cada DataNode. Si el número es menor que tres, o la
orden responde con un error de conexión poco después del arranque, espera
15-30 segundos y repite únicamente esta celda: es posible que los tres nodos
todavía no se hayan registrado.
!docker exec namenode su - hdadmin -c 'hdfs dfsadmin -report'
YARN administra recursos de procesamiento, no bloques HDFS, y sus nodos de
trabajo se consultan por separado. La salida esperada empieza por
Total Nodes:3 y tiene una fila por NodeManager con cuatro columnas:
Node-Id, Node-State (debe ser RUNNING), Node-Http-Address y
Number-of-Running-Containers (cero mientras no haya aplicaciones en
marcha). La opción -showDetails añade bajo cada nodo sus recursos: la
línea Configured Resources : <memory:2560, vCores:2> es lo que ese
NodeManager anuncia a YARN, los 2560 MiB y 2 vcores de la tabla de recursos
de más arriba. Es posible que HDFS tenga tres DataNodes vivos y YARN no
tenga tres NodeManagers, o al contrario, porque son servicios diferentes
aunque compartan contenedores:
hdfs dfsadmin -reportresponde a «¿dónde se pueden guardar bloques?»;yarn node -list -allresponde a «¿dónde se pueden ejecutar tareas?».
!docker exec namenode su - hdadmin -c 'yarn node -list -all -showDetails'
Relacionar servicios y procesos con jps¶
jps muestra los procesos Java visibles para el usuario que lo ejecuta. Los
demonios pertenecen a hdadmin, por lo que hay que usar esa identidad para
obtener una comprobación fiable. En el NameNode se espera encontrar
NameNode, ResourceManager y ApplicationHistoryServer (además del propio
proceso Jps, que sólo existe mientras se hace la consulta).
!docker exec namenode su - hdadmin -c 'jps'
En cada DataNode se espera encontrar DataNode y NodeManager.
!docker exec datanode1 su - hdadmin -c 'jps'
!docker exec datanode2 su - hdadmin -c 'jps'
!docker exec datanode3 su - hdadmin -c 'jps'
| Proceso | Qué mantiene o ejecuta |
|---|---|
NameNode |
Espacio de nombres, permisos y localización de los bloques HDFS |
DataNode |
Bloques de datos guardados en el almacenamiento local del nodo |
ResourceManager |
Vista global de recursos y planificación de aplicaciones |
ApplicationHistoryServer |
Histórico y consulta de información de aplicaciones YARN (Timeline Server) |
NodeManager |
Contenedores YARN y recursos disponibles en un nodo |
Los identificadores numéricos de proceso no tienen por qué coincidir entre contenedores ni mantenerse tras un reinicio; lo relevante son los nombres de los demonios.
Consultar el Timeline Server desde la red interna¶
El ResourceManager sabe si una aplicación está ejecutándose, ha terminado o
ha fallado. El Timeline Server añade una perspectiva histórica: recibe
los eventos y métricas que publican el ResourceManager y las aplicaciones, y
los conserva en su almacén local. La siguiente celda comprueba que la API
HTTP responde (la salida es un documento JSON) usando el alias interno, lo
que también verifica que el nombre timelineserver resuelve al contenedor
correcto. Más adelante, tras ejecutar WordCount, se volverá a esta interfaz
para localizar la aplicación ya finalizada.
!docker exec namenode curl --fail --silent http://timelineserver:8188/ws/v1/timeline/
Explorar las herramientas instaladas¶
Las imágenes no son cajas opacas. Aunque no haga falta construirlas, conviene
comprobar qué ejecutables contienen y desde dónde se invocan. Todas las
órdenes siguientes se ejecutan dentro de namenode como luser: será la
identidad habitual para trabajar con datos y, más adelante, para iniciar
Jupyter y Spark.
!docker exec namenode su - luser -c 'id'
command -v muestra el ejecutable que resolvería el shell a través de
PATH, sin llegar a ejecutar el programa. La imagen incluye Java 17, y
Python/Maven para sesiones posteriores. En namenode, python3 y pip3 se
resuelven dentro de /opt/tcdm/venv/bin: es el entorno Python del curso
descrito en «Cómo se construyen las imágenes del curso», que ya trae Jupyter
Lab y PySpark para las sesiones siguientes.
!docker exec namenode su - luser -c 'command -v java && java -version'
!docker exec namenode su - luser -c 'command -v python3 && python3 --version'
!docker exec namenode su - luser -c 'command -v pip3'
!docker exec namenode su - luser -c 'command -v jupyter spark-submit'
!docker exec namenode su - luser -c 'command -v mvn'
Los clientes de Hadoop deben resolverse todos dentro de /opt/bd/hadoop/bin,
sin que haga falta escribir la ruta completa: la instalación se expone
mediante variables de entorno ($HADOOP_HOME, $PATH) definidas en
/etc/profile.d/hadoop.sh.
!docker exec namenode su - luser -c 'command -v hadoop; command -v hdfs; command -v yarn; command -v mapred'
La primera línea de hadoop version debe indicar Hadoop 3.5.0; el resto de
la salida aporta datos de compilación y de la JVM, útiles cuando un mensaje
de error o una documentación externa dependen de una versión concreta.
!docker exec namenode su - luser -c 'hadoop version'
!docker exec namenode su - luser -c 'printf "%s\n" "$HADOOP_HOME"'
Explorar HDFS como luser¶
A partir de aquí todas las operaciones ordinarias de datos se ejecutan como
luser; hdadmin queda reservado para consultas administrativas (fsck,
dfsadmin). Trabajar con la identidad correcta permite detectar problemas de
permisos que quedarían ocultos si todo se hiciera como administrador.
La raíz de HDFS no es la raíz de Linux del contenedor: es el espacio de
nombres distribuido. Deberían aparecer /user, /tmp, /datalake y
/warehouse, cada uno con su propietario, grupo, permisos, tamaño y fecha.
!docker exec namenode su - luser -c 'hdfs dfs -ls /'
Cada usuario puede tener su área bajo /user (una convención parecida a
/home/<usuario> en Unix, aunque ambos sistemas de ficheros sean
independientes). Debe aparecer al menos /user/hdadmin y /user/luser.
!docker exec namenode su - luser -c 'hdfs dfs -ls /user'
/datalake/raw/tpcds es donde una sesión posterior materializará TPC-DS en
Parquet. /datalake/silver y /datalake/gold reservan las siguientes capas
medallion, y /warehouse queda para tablas gobernadas por catálogo. Estas
rutas deben pertenecer a luser:hadoop. En esta sesión sólo se comprueba
que existen y son accesibles.
!docker exec namenode su - luser -c 'hdfs dfs -ls -d /datalake /datalake/raw /datalake/raw/tpcds /datalake/silver /datalake/gold'
!docker exec namenode su - luser -c 'hdfs dfs -ls -d /warehouse'
Replicación y tamaño de bloque¶
El factor de replicación por defecto se consulta con getconf, sin
necesidad de editar ficheros de configuración. El valor esperado es 3: HDFS
intentará mantener tres copias de cada bloque, una en cada DataNode
disponible. Replicar tres veces no crea tres nombres de fichero ni hace que
hdfs dfs -ls muestre tres entradas — las réplicas son una decisión interna
de almacenamiento.
!docker exec namenode su - luser -c 'hdfs getconf -confKey dfs.replication'
El tamaño de bloque configurado corresponde a 64 MiB. Un bloque HDFS no equivale al bloque físico de un disco: es una unidad lógica grande, elegida para reducir metadatos y favorecer lecturas secuenciales de datos masivos. Los ficheros de esta sesión son mucho más pequeños y ocuparán un único bloque cada uno; en conjuntos de datos grandes, HDFS divide cada fichero en varios bloques que pueden almacenarse y procesarse en nodos diferentes.
!docker exec namenode su - luser -c 'hdfs getconf -confKey dfs.blocksize'
Comprobar la configuración efectiva de Hadoop¶
Los cuatro ficheros principales de configuración de Hadoop
(core-site.xml, hdfs-site.xml, yarn-site.xml y mapred-site.xml) viven
en $HADOOP_CONF_DIR, es decir /opt/bd/hadoop/etc/hadoop, y se escriben al
construir la imagen. Aquí se muestran las propiedades de cada uno, tal y como
quedaron escritas (el script de la imagen añade además comentarios que aquí
se omiten), y se explica justo debajo el significado de las más
importantes.
core-site.xml es común a NameNode y DataNodes, y fija fs.defaultFS: hace
que una ruta como /user/luser se interprete como
hdfs://namenode:9000/user/luser.
<?xml version="1.0"?>
<configuration>
<property>
<name>fs.defaultFS</name>
<value>hdfs://namenode:9000/</value>
<final>true</final>
</property>
<property>
<name>hadoop.tmp.dir</name>
<value>/var/tmp/hadoop-${user.name}</value>
<final>true</final>
</property>
</configuration>
El puerto 9000 de fs.defaultFS es el endpoint RPC que usan los clientes
HDFS (hdfs dfs, fsspec, Spark); no debe confundirse con la interfaz web
9870. hadoop.tmp.dir fija dónde escribe cada identidad sus temporales
locales, separados gracias a ${user.name}, que Hadoop resuelve en tiempo
de ejecución. Ambas propiedades llevan <final>true</final>: ningún trabajo
puede sustituirlas con su propia configuración, porque son decisiones
estructurales de este laboratorio, no ajustes que deba tocar quien lo usa.
hdfs-site.xml cambia según el tipo de nodo. La diferencia principal entre
el NameNode y un DataNode será dfs.namenode.name.dir frente a
dfs.datanode.data.dir: uno señala dónde se guardan metadatos y el otro
dónde se guardan bloques.
hdfs-site.xml en namenode:
<?xml version="1.0"?>
<configuration>
<property>
<name>dfs.replication</name>
<value>3</value>
<final>true</final>
</property>
<property>
<name>dfs.blocksize</name>
<value>64m</value>
<final>true</final>
</property>
<property>
<name>dfs.namenode.name.dir</name>
<value>file:///var/data/hdfs/namenode</value>
<final>true</final>
</property>
<property>
<name>dfs.image.compress</name>
<value>true</value>
</property>
<property>
<name>dfs.webhdfs.enabled</name>
<value>true</value>
</property>
<property>
<name>hadoop.http.authentication.type</name>
<value>simple</value>
</property>
<property>
<name>hadoop.http.filter.initializers</name>
<value></value>
</property>
<property>
<name>dfs.namenode.http-address</name>
<value>namenode:9870</value>
</property>
</configuration>
hdfs-site.xml en cada datanodeN:
<?xml version="1.0"?>
<configuration>
<property>
<name>dfs.replication</name>
<value>3</value>
<final>true</final>
</property>
<property>
<name>dfs.blocksize</name>
<value>64m</value>
<final>true</final>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>file:///var/data/hdfs/datanode</value>
<final>true</final>
</property>
<property>
<name>dfs.webhdfs.enabled</name>
<value>true</value>
</property>
</configuration>
dfs.namenode.name.dir y dfs.datanode.data.dir son las dos propiedades
que distinguen ambos ficheros: la primera es una ruta local del
contenedor namenode donde vive la imagen del espacio de nombres y el
registro de ediciones; la segunda es una ruta local de cada datanodeN
donde se guardan sus bloques. Aunque el texto de la ruta coincida entre los
tres DataNodes, cada uno la resuelve en su propio sistema de ficheros: no la
comparten. Por eso no se usa RAID para la redundancia de los bloques de
HDFS —esa responsabilidad ya la cumple la replicación entre nodos—,
mientras que perder el disco del NameNode sí sería grave, porque ahí sólo
hay una copia de los metadatos.
dfs.webhdfs.enabled=true activa la API HTTP de HDFS, que usarán fsspec,
PyArrow y Polars en una sesión posterior para leer datos sin pasar por el
cliente hdfs. dfs.namenode.http-address es la dirección que publica esa
interfaz de administración, la misma que se abre en http://localhost:9870.
compose-hadoop-cluster.yml monta cada una de estas dos rutas sobre un
volumen Docker con nombre (namenode-data, datanode1-data,
datanode2-data, datanode3-data), no sobre la capa de escritura del
contenedor. Por eso el espacio de nombres y los bloques sobreviven a
docker compose down y a la recreación de los contenedores: solo
desaparecen si se borra explícitamente el volumen (ver «Detener, limpiar y
recuperar el entorno», más adelante en esta sesión).
yarn-site.xml configura el ResourceManager (en namenode) y los
NodeManagers (en cada datanodeN). Compara los límites de planificador del
ResourceManager con los recursos que anuncia un NodeManager concreto.
Del yarn-site.xml de los DataNodes se muestra, tras el del NameNode, sólo el
bloque de propiedades propias del NodeManager: las otras ocho (host del
ResourceManager, Timeline Server y agregación de logs) son idénticas a las del
NameNode.
yarn-site.xml en namenode:
<?xml version="1.0"?>
<configuration>
<property>
<name>yarn.resourcemanager.hostname</name>
<value>resourcemanager</value>
<final>true</final>
</property>
<property>
<name>yarn.timeline-service.enabled</name>
<value>true</value>
<final>true</final>
</property>
<property>
<name>yarn.timeline-service.hostname</name>
<value>timelineserver</value>
<final>true</final>
</property>
<property>
<name>yarn.timeline-service.address</name>
<value>timelineserver:10200</value>
<final>true</final>
</property>
<property>
<name>yarn.timeline-service.webapp.address</name>
<value>timelineserver:8188</value>
<final>true</final>
</property>
<property>
<name>yarn.timeline-service.bind-host</name>
<value>0.0.0.0</value>
<final>true</final>
</property>
<property>
<name>yarn.system-metrics-publisher.enabled</name>
<value>true</value>
<final>true</final>
</property>
<property>
<name>yarn.timeline-service.generic-application-history.enabled</name>
<value>true</value>
<final>true</final>
</property>
<property>
<name>yarn.timeline-service.store-class</name>
<value>org.apache.hadoop.yarn.server.timeline.LeveldbTimelineStore</value>
<final>true</final>
</property>
<property>
<name>yarn.timeline-service.leveldb-timeline-store.path</name>
<value>/var/data/yarn/timeline</value>
<final>true</final>
</property>
<property>
<name>yarn.timeline-service.http-authentication.type</name>
<value>simple</value>
<final>true</final>
</property>
<property>
<name>yarn.timeline-service.http-authentication.simple.anonymous.allowed</name>
<value>true</value>
<final>true</final>
</property>
<property>
<name>yarn.log-aggregation-enable</name>
<value>true</value>
</property>
<property>
<name>yarn.nodemanager.remote-app-log-dir</name>
<value>/tmp/logs</value>
</property>
<property>
<name>yarn.nodemanager.log-aggregation.compression-type</name>
<value>gz</value>
</property>
<property>
<name>yarn.scheduler.maximum-allocation-vcores</name>
<value>2</value>
</property>
<property>
<name>yarn.scheduler.minimum-allocation-mb</name>
<value>256</value>
</property>
<property>
<name>yarn.scheduler.maximum-allocation-mb</name>
<value>2560</value>
</property>
</configuration>
Propiedades propias del NodeManager, en el yarn-site.xml de cada datanodeN:
<property>
<name>yarn.nodemanager.resource.detect-hardware-capabilities</name>
<value>false</value>
</property>
<property>
<name>yarn.nodemanager.resource.cpu-vcores</name>
<value>2</value>
</property>
<property>
<name>yarn.nodemanager.resource.memory-mb</name>
<value>2560</value>
</property>
<property>
<name>yarn.nodemanager.vmem-check-enabled</name>
<value>false</value>
</property>
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
<property>
<name>yarn.nodemanager.aux-services.mapreduce_shuffle.class</name>
<value>org.apache.hadoop.mapred.ShuffleHandler</value>
</property>
resourcemanager y timelineserver son alias de red del propio contenedor
namenode, para separar el nombre de cada servicio de su papel en HDFS
aunque compartan contenedor; el puerto RPC 10200 del Timeline Server sólo
se usa dentro de la red Docker, y su interfaz HTTP se publica en el host
como http://localhost:8188. La propiedad que permite que el
ResourceManager publique información de aplicaciones es
yarn.system-metrics-publisher.enabled (en Hadoop 3.5.0 la variante con el
prefijo resourcemanager. está deprecada).
La agregación de logs
(yarn.log-aggregation-enable) copia a /tmp/logs de HDFS los logs de los
contenedores, comprimidos como TFile con gz: no se puede leer ese
resultado con un cat directo, hace falta
yarn logs -applicationId <application_id>, que conoce el formato. Los
límites del planificador (yarn.scheduler.maximum-allocation-*) impiden
pedir un contenedor mayor que los recursos que anuncia un DataNode.
El Timeline Server guarda su histórico con LeveldbTimelineStore, en la ruta
local /var/data/yarn/timeline del contenedor namenode
(yarn.timeline-service.leveldb-timeline-store.path). A diferencia de
/var/data/hdfs/namenode, esa ruta no está montada en un volumen con nombre:
el histórico de aplicaciones desaparece con docker compose down aunque los
datos HDFS se conserven. yarn.timeline-service.bind-host=0.0.0.0 hace que el
servicio escuche en todas las interfaces del contenedor —Compose publica
después ese puerto sólo en 127.0.0.1—, y las dos propiedades
http-authentication (tipo simple, con acceso anónimo permitido) explican
que la interfaz http://localhost:8188/applicationhistory/ se abra en el
navegador sin credenciales, en línea con la autenticación simple del resto del
laboratorio.
En el yarn-site.xml de los NodeManagers,
yarn.nodemanager.resource.detect-hardware-capabilities=false evita que
YARN interprete los recursos del host en vez de los límites que se quieren
enseñar: cada NodeManager anuncia explícitamente 2 vcores y 2560 MiB,
dejando 512 MiB del límite Docker (3072 MiB) para el propio DataNode, el
NodeManager y otros procesos. La comprobación de memoria virtual se
desactiva (yarn.nodemanager.vmem-check-enabled=false) para evitar falsos
positivos por cómo Linux y las JVM reservan espacio de direcciones dentro
de un contenedor; esto no elimina el límite real de memoria de Docker ni el
presupuesto que YARN asigna a sus contenedores. mapreduce_shuffle es el
servicio auxiliar que permite a los reducers obtener de cada nodo las
salidas intermedias de los mapas: sin él, YARN podría iniciar procesos,
pero un trabajo MapReduce no completaría su fase de shuffle.
mapred-site.xml es común a las dos imágenes de servicio: los NodeManagers
son quienes ejecutan allí el ApplicationMaster, las tareas map y las
tareas reduce.
<?xml version="1.0"?>
<configuration>
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
<final>true</final>
</property>
<property>
<name>yarn.app.mapreduce.am.env</name>
<value>HADOOP_MAPRED_HOME=${HADOOP_HOME}</value>
</property>
<property>
<name>yarn.app.mapreduce.am.resource.cpu-vcores</name>
<value>1</value>
</property>
<property>
<name>yarn.app.mapreduce.am.resource.mb</name>
<value>512</value>
</property>
<property>
<name>mapreduce.job.heap.memory-mb.ratio</name>
<value>0.8</value>
</property>
<property>
<name>mapreduce.map.env</name>
<value>HADOOP_MAPRED_HOME=${HADOOP_HOME}</value>
</property>
<property>
<name>mapreduce.map.cpu.vcores</name>
<value>1</value>
</property>
<property>
<name>mapreduce.map.java.opts</name>
<value>-Xmx512M</value>
</property>
<property>
<name>mapreduce.map.memory.mb</name>
<value>768</value>
</property>
<property>
<name>mapreduce.reduce.env</name>
<value>HADOOP_MAPRED_HOME=${HADOOP_HOME}</value>
</property>
<property>
<name>mapreduce.reduce.cpu.vcores</name>
<value>1</value>
</property>
<property>
<name>mapreduce.reduce.java.opts</name>
<value>-Xmx512M</value>
</property>
<property>
<name>mapreduce.reduce.memory.mb</name>
<value>768</value>
</property>
</configuration>
mapreduce.framework.name=yarn evita la ejecución local del trabajo: los
recursos siempre se piden al clúster. El ApplicationMaster reserva 1 vcore
y 512 MiB; cada tarea map y reduce solicita 1 vcore y un contenedor de
768 MiB, pero limita el heap de su JVM a 512 MiB (-Xmx512M). El ratio
mapreduce.job.heap.memory-mb.ratio=0.8 sólo se aplicaría si faltara ese
-Xmx: Hadoop deduciría entonces un heap de 768 × 0,8 = 614,4 MiB. Aquí
el valor explícito tiene prioridad. La diferencia entre el contenedor y el
heap deja memoria para la propia JVM, bibliotecas nativas y otros
costes fuera del heap. HADOOP_MAPRED_HOME se propaga a los procesos
remotos (*.env) para que cualquier DataNode localice las bibliotecas de
MapReduce.
También se puede pedir a Hadoop una propiedad ya interpretada —incluyendo sustituciones y valores predeterminados— en lugar de leer el XML a mano. Leer el XML explica qué se escribió al construir la imagen; consultar la propiedad mediante Hadoop confirma qué valor está utilizando el proceso. Ambas formas de inspección son complementarias.
!docker exec namenode su - luser -c 'hdfs getconf -confKey fs.defaultFS'
Crear el área de trabajo de la sesión¶
Los datos de esta sesión se aíslan bajo /user/luser/s1/input. -p crea
también los directorios intermedios y no falla si ya existen, por lo que se
puede repetir esta celda al volver a hacer la sesión.
!docker exec namenode su - luser -c 'hdfs dfs -mkdir -p /user/luser/s1/input'
!docker exec namenode su - luser -c 'hdfs dfs -ls -R /user/luser/s1'
Operaciones básicas con ficheros en HDFS¶
Primero se crean tres ficheros locales en /tmp del contenedor
namenode: todavía no están replicados, no son visibles desde los
DataNodes mediante HDFS y desaparecerán si se elimina el contenedor. Esta
distinción entre ruta local y ruta HDFS será importante durante todo el
curso.
Los tres ficheros comparten varias palabras a propósito, para que el resultado de WordCount permita comprobar tanto recuentos repetidos como palabras que sólo aparecen una vez.
!docker exec namenode su - luser -c 'printf "hadoop hdfs mapreduce\nhadoop yarn\n" > /tmp/s1-a.txt'
!docker exec namenode su - luser -c 'printf "hdfs hadoop\nyarn recursos\n" > /tmp/s1-b.txt'
!docker exec namenode su - luser -c 'printf "mapreduce datos hadoop\ndatos datos\n" > /tmp/s1-c.txt'
Se puede comprobar el contenido local del primer fichero con cat, sin usar todavía el cliente HDFS:
!docker exec namenode su - luser -c 'cat /tmp/s1-a.txt'
Subir los ficheros a HDFS¶
-put -f transfiere el fichero local al directorio HDFS indicado; -f
permite reemplazar un fichero HDFS del mismo nombre si se repite la
sesión. Tener varios ficheros de entrada permite a Hadoop crear varias
tareas map: con datos tan pequeños el rendimiento no mejora, pero la
estructura ayuda a observar que una entrada puede dividirse en trabajos
independientes.
!docker exec namenode su - luser -c 'hdfs dfs -put -f /tmp/s1-a.txt /user/luser/s1/input/'
!docker exec namenode su - luser -c 'hdfs dfs -put -f /tmp/s1-b.txt /user/luser/s1/input/'
!docker exec namenode su - luser -c 'hdfs dfs -put -f /tmp/s1-c.txt /user/luser/s1/input/'
Listar y leer los datos distribuidos¶
Se esperan tres entradas; -h presenta los tamaños en formato legible. El
factor de replicación no multiplica el tamaño lógico mostrado en esta lista.
!docker exec namenode su - luser -c 'hdfs dfs -ls -h /user/luser/s1/input'
-cat envía los bytes recuperados desde los DataNodes a la salida estándar; no crea una copia local permanente.
!docker exec namenode su - luser -c 'hdfs dfs -cat /user/luser/s1/input/s1-a.txt'
Copiar, mover y descargar ficheros¶
-cp copia entre dos rutas que pertenecen a HDFS; no interviene el sistema
de ficheros local del contenedor.
!docker exec namenode su - luser -c 'hdfs dfs -cp /user/luser/s1/input/s1-a.txt /user/luser/s1/s1-a-copia.txt'
-mv cambia la ruta dentro de HDFS. Dentro de un mismo espacio de nombres, renombrar suele ser una operación de metadatos del NameNode: no hace falta volver a copiar los bloques.
!docker exec namenode su - luser -c 'hdfs dfs -mv /user/luser/s1/s1-a-copia.txt /user/luser/s1/s1-a-renombrado.txt'
El fichero renombrado queda directamente bajo /user/luser/s1, fuera de input, así que MapReduce no lo contará más adelante.
!docker exec namenode su - luser -c 'hdfs dfs -ls /user/luser/s1'
-get es la operación inversa a -put: descarga un fichero de HDFS al sistema local del contenedor. Tras esto, /tmp/s1-descargado.txt es una copia local que se puede comprobar con cat, sin usar ya el cliente HDFS.
!docker exec namenode su - luser -c 'hdfs dfs -get -f /user/luser/s1/s1-a-renombrado.txt /tmp/s1-descargado.txt'
!docker exec namenode su - luser -c 'cat /tmp/s1-descargado.txt'
Espacio, bloques y réplicas¶
-du -h muestra el tamaño lógico y el espacio ocupado por las réplicas. Con
factor de replicación tres, el segundo valor debería ser aproximadamente el
triple del primero para estos ficheros, aunque sean muy pequeños.
!docker exec namenode su - luser -c 'hdfs dfs -du -h /user/luser/s1/input'
fsck consulta el estado de bajo nivel de bloques y réplicas como administrador. La última línea debe indicar Status: HEALTHY; las ubicaciones muestran en qué DataNodes se guarda cada réplica y se pueden comparar con los tres nodos obtenidos antes con dfsadmin -report.
!docker exec namenode su - hdadmin -c 'hdfs fsck /user/luser/s1/input -files -blocks -locations'
El modelo de programación MapReduce¶
Antes de enviar un trabajo a YARN conviene fijar el modelo que MapReduce oculta detrás de esa orden. El programador sólo escribe dos funciones:
- map:
map(K1, V1) -> list(K2, V2). Recibe un par clave/valor de la entrada y produce una lista de pares intermedios. En WordCount,K1no se usa,V1es una línea de texto y cada palabra de esa línea produce un par intermedio(palabra, 1). - reduce:
reduce(K2, list(V2)) -> (K3, list(V3)). Recibe todos los valores intermedios asociados a una misma clave —Hadoop los agrupa durante la fase de shuffle— y produce el resultado final. En WordCount,list(V3)contiene un único valor: la suma de los1recibidos para esa palabra.
Dos piezas adicionales optimizan ese esquema sin cambiar el resultado:
- Un combinador (combiner) agrega localmente, en el mismo nodo que
ejecutó el
map, los valores de una misma clave antes de enviarlos por red. Reduce el tráfico del shuffle, pero sólo es correcto si la función es conmutativa y asociativa —sumar recuentos de palabras lo es—. - Un particionador decide a qué tarea
reduceva cada clave, típicamente conhash(K) mod R, dondeRes el número de reducers. Garantiza que todas las apariciones de una misma clave llegan al mismo reducer, sin lo cual no se podría sumar correctamente su recuento.
MapReduce también está diseñado para que un fallo aislado no tumbe el
trabajo completo. Un proceso maestro detecta mediante heartbeats qué
tareas siguen vivas; si una tarea falla, se reintenta en otro nodo, y si una
tarea concreta no avanza —una tarea rezagada o straggler— puede lanzarse
una segunda copia en paralelo en otro nodo y quedarse con la que termine
antes (ejecución especulativa). Las tareas map no dependen unas de otras,
así que pueden reejecutarse sin coordinación; las tareas reduce pueden
recuperarse porque las salidas de map quedan en disco local, no sólo en
memoria.
La siguiente figura resume el flujo completo, incluida la fase de shuffle
que agrupa por clave entre la fase map y la fase reduce:
Con este modelo en mente, el resto de la sección ejecuta WordCount tal cual lo haría cualquier trabajo MapReduce: preparar la entrada, enviar el trabajo a YARN y leer la salida.
Ejemplo: el mismo WordCount escrito con MrJob¶
El ejemplo wordcount que se ejecuta más abajo contra YARN está escrito en
Java. Para que la correspondencia entre la teoría y el código se vea de un
vistazo, esta es la misma cuenta de palabras escrita en Python con
MrJob, una biblioteca que expone
directamente las funciones mapper y reducer y oculta el resto del motor
MapReduce (shuffle, combinador, reintentos...). El curso ya no usa MrJob en
las prácticas —se trabaja con Spark—, pero verlo una vez ayuda a fijar el
modelo. Es un ejemplo sólo para leer: no se ejecuta en esta sesión y
MrJob no está instalado en el laboratorio.
from mrjob.job import MRJob
class MRWordCount(MRJob):
def mapper(self, _, line: str):
# map(K1, V1) -> list(K2, V2)
# K1 no se usa; V1 es la línea de texto.
# Cada palabra de la línea produce un par intermedio (palabra, 1).
for word in line.split():
yield word.lower(), 1
def combiner(self, word: str, counts: list[int]):
# Suma parcial en el mismo nodo que ejecutó el mapper, antes de
# enviar nada por la red. Válido porque sumar es conmutativo y
# asociativo (ver más arriba).
yield word, sum(counts)
def reducer(self, word: str, counts: list[int]):
# reduce(K2, list(V2)) -> (K3, list(V3))
# `counts` es list(V2): los valores que MrJob ya agrupó por clave
# durante el shuffle. Aquí la lista de salida tiene un único valor.
yield word, sum(counts)
if __name__ == "__main__":
MRWordCount.run()
Guardado como wordcount_mrjob.py, se ejecutaría en local sin Hadoop ni
clúster —MrJob agrupa las claves como haría el shuffle antes de llamar al
reducer—. Con el mismo contenido de s1-a.txt, s1-b.txt y s1-c.txt
que se subió a HDFS más arriba, la orden y su salida serían:
$ printf 'hadoop hdfs mapreduce\nhadoop yarn\nhdfs hadoop\nyarn recursos\nmapreduce datos hadoop\ndatos datos\n' \
| python3 wordcount_mrjob.py
"yarn" 2
"recursos" 1
"mapreduce" 2
"hdfs" 2
"hadoop" 4
"datos" 3
MrJob serializa cada par intermedio como JSON —de ahí las comillas en las
palabras—. El orden entre palabras distintas no está garantizado (aquí no
coincide con el alfabético, a diferencia de la salida de Hadoop más abajo,
que sí usa un único reducer y por eso queda ordenada); lo que importa es
que el recuento de cada palabra es el mismo que va a producir el
wordcount de Hadoop sobre estos mismos ficheros. Comparar ambas salidas
permite comprobar que mapper/reducer en MrJob y map/reduce en el
wordcount de Hadoop calculan exactamente lo mismo; lo único que cambia es
qué motor reparte el trabajo entre las máquinas.
Ejecutar un trabajo MapReduce con YARN¶
Se utiliza el ejemplo wordcount incluido en la instalación de Hadoop, que
cuenta cuántas veces aparece cada palabra. Aunque el programa es pequeño,
recorre el mismo camino básico que una aplicación MapReduce real: el cliente
prepara y envía la aplicación a YARN, el ResourceManager asigna recursos para
el ApplicationMaster, se crean tareas map que leen las entradas, los
resultados intermedios se agrupan por clave durante el shuffle, y una o más
tareas reduce producen los ficheros finales en HDFS. El JAR de ejemplos ya
forma parte de la imagen; el patrón hadoop-mapreduce-examples-*.jar evita
repetir el número de versión en la orden.
MapReduce exige que el directorio de salida no exista, para evitar sobrescribir resultados por accidente. Antes de (re)ejecutar el trabajo se borra sólo la salida anterior; los ficheros de entrada no se tocan.
!docker exec namenode su - luser -c 'hdfs dfs -rm -r -f /user/luser/s1/output'
Esta orden se lanza en namenode, pero eso no significa que el NameNode
ejecute las tareas: el proceso que arranca allí es el cliente. YARN
asigna los contenedores de ejecución a los NodeManagers de los datanodeN.
Durante la ejecución se muestran un identificador de aplicación y los
porcentajes de avance de las fases map y reduce; el trabajo debe terminar
con éxito. Si falla, conserva el identificador y revisa primero el mensaje de
error — borrar todo el clúster debería ser el último recurso, no la primera
respuesta.
!docker exec namenode su - luser -c 'yarn jar "$HADOOP_HOME"/share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar wordcount /user/luser/s1/input /user/luser/s1/output'
Debe aparecer _SUCCESS (finalización correcta) y al menos un fichero part-r-00000 (el sufijo r indica que es salida de una tarea reduce).
!docker exec namenode su - luser -c 'hdfs dfs -ls /user/luser/s1/output'
Se esperan estos recuentos (Hadoop separa palabra y contador con un tabulador, así que el espaciado exacto puede variar):
datos 3
hadoop 4
hdfs 2
mapreduce 2
recursos 1
yarn 2
!docker exec namenode su - luser -c 'hdfs dfs -cat /user/luser/s1/output/part-r-*'
Consultar la aplicación en YARN y en el Timeline Server¶
mapred job -list all presenta con la nomenclatura de MapReduce las
aplicaciones YARN de tipo MapReduce: consulta al ResourceManager, así que no
necesita un JobHistoryServer (este clúster no lo arranca, como se explicó al
observar el Timeline Server). Después de ejecutar wordcount debe aparecer con
un identificador job_... —con los mismos números que el application_... de
YARN— y estado SUCCEEDED.
!docker exec namenode su - hdadmin -c 'mapred job -list all'
yarn application -list -appStates ALL muestra el mismo trabajo desde la perspectiva de YARN. Busca un estado FINISHED y un resultado final SUCCEEDED; la interfaz web de http://localhost:8088 muestra la misma información de forma gráfica.
!docker exec namenode su - hdadmin -c 'yarn application -list -appStates ALL'
Ver en qué nodo se ejecutó cada tarea¶
La orden yarn jar se lanzó desde namenode, pero las tareas se ejecutaron
en los DataNodes. Se puede comprobar en el navegador, sin más órdenes:
- Abre el histórico del Timeline Server,
http://localhost:8188/applicationhistory/. Hay una fila por aplicación:
localiza la de nombre
word countcon el identificadorapplication_...que mostróyarn jar. - Pulsa ese identificador. La página de la aplicación muestra su estado
final y una tabla con sus intentos (
appattempt_...); lo normal es que haya uno solo. - Pulsa el identificador del intento. La tabla de esa página lista los
contenedores YARN que usó la aplicación, cada uno con su columna
Node: el
datanodeNen el que se ejecutó.
Con tres ficheros de entrada se esperan cinco contenedores. El primero
(..._000001) es el ApplicationMaster; los demás son las tres tareas map
—una por fichero— y la tarea reduce. Anota en qué nodo cayó cada uno: es
la respuesta a la pregunta guía de antes de lanzar el trabajo.
Los enlaces de la columna Node y los de los logs apuntan a nombres como
datanode1:8042, que sólo se resuelven dentro de la red Docker: desde tu
navegador no se abren, pero el nombre del nodo ya está a la vista.
Si no lo has visto a la primera, repite el trabajo —las celdas de «Ejecutar
un trabajo MapReduce con YARN», empezando por la que borra la salida— con
http://localhost:8088 abierto en otra pestaña. Mientras se ejecuta, la
aplicación aparece allí como RUNNING y el mismo recorrido —aplicación,
intento, contenedores— muestra los contenedores activos en ese momento. El
trabajo dura poco: si no llegas a tiempo, el histórico del Timeline Server
conserva la ejecución y ahora tendrá dos aplicaciones que comparar.
La distribución exacta de tareas entre DataNodes puede cambiar entre ejecuciones: un planificador distribuido no promete que cada trabajo pequeño utilice todos los nodos por igual. Lo que importa comprobar aquí es que el cliente entrega la aplicación a YARN, que los NodeManagers aportan contenedores y que el resultado se escribe en HDFS.
Detener, limpiar y recuperar el entorno¶
No ejecutes estas órdenes por accidente mientras trabajas: se presentan como
texto para copiarlas a una terminal cuando de verdad las necesites, no como
celdas de código, precisamente para que un «ejecutar todo» del notebook no
destruya tu propio trabajo. Ejecútalas en una terminal de tu
equipo, situada en la raíz de la distribución (el directorio que contiene
s1/ y entorno/); por eso aquí las rutas empiezan por entorno/ y no por
../entorno/ como en las celdas del notebook.
| Situación | Orden | Consecuencia |
|---|---|---|
| Parar temporalmente | docker compose -f entorno/compose-hadoop-cluster.yml stop |
Conserva contenedores y datos HDFS; se reanuda con start. Tras start los demonios tardan unos segundos en registrarse: repite dfsadmin -report y yarn node -list -all antes de continuar. |
| Reanudar | docker compose -f entorno/compose-hadoop-cluster.yml start |
Vuelve a poner en marcha los mismos contenedores. up -d también sirve: Compose reutiliza los contenedores existentes mientras su configuración no obligue a recrearlos. |
| Limpiar sólo tus datos de la sesión | docker exec namenode su - luser -c 'hdfs dfs -rm -r -f /user/luser/s1' |
Borra el área /user/luser/s1 para repetir la sesión desde cero sin regenerar el clúster. No borra /warehouse, /tmp, /user ni los directorios de otros usuarios, ni detiene contenedores ni borra /tmp local. |
| Eliminar los contenedores | docker compose -f entorno/compose-hadoop-cluster.yml down |
Elimina los contenedores, pero conserva los datos HDFS: viven en los volúmenes Docker tcdm-26-27-namenode-data y tcdm-26-27-datanodeN-data (en el Compose aparecen con los nombres cortos namenode-data/datanodeN-data), no en la capa de escritura de los contenedores. Sí borra el almacén local (no persistido) del Timeline Server. La red externa hadoop-cluster no se elimina. |
| Recrear los contenedores | down seguido de up -d |
Los mismos volúmenes HDFS se vuelven a montar: /user/luser, /datalake, /warehouse, los datos de /user/luser/s1 y un TPC-DS ya materializado siguen ahí. No hace falta reconstruir las imágenes. |
| Borrar también el HDFS (destructivo) | docker volume rm tcdm-26-27-namenode-data tcdm-26-27-datanode1-data tcdm-26-27-datanode2-data tcdm-26-27-datanode3-data (con los contenedores ya eliminados) |
Vacía el espacio de nombres y los bloques: el próximo up -d arranca con un HDFS recién formateado, como la primera vez. Es la única orden de esta tabla que borra /datalake/raw/tpcds y cualquier otro dato ya materializado en sesiones posteriores. |
El Makefile de entorno/ ofrece estas mismas operaciones para todos los
servicios del laboratorio a la vez y con los mismos nombres:
make -C entorno stop para los contenedores sin eliminarlos,
make -C entorno down los elimina conservando los volúmenes y
make -C entorno clean-hdfs borra además los volúmenes HDFS. Tras stop o
down se vuelve a arrancar con make -C entorno hadoop-up. La sección
«Referencia: arrancar y parar el laboratorio», al final de este notebook,
los reúne en una tabla.
Diagnosticar un arranque incompleto¶
Si algo no arranca como se espera, conviene mirar en este orden, en una terminal de tu equipo y desde la raíz de la distribución: qué contenedores siguen activos, las últimas líneas de todos los servicios y, si el problema parece localizado, los logs de un contenedor concreto:
docker compose -f entorno/compose-hadoop-cluster.yml ps -a
docker compose -f entorno/compose-hadoop-cluster.yml logs --tail=120
docker logs namenode --tail=120
docker logs datanode1 --tail=120
Los fallos más
habituales en esta fase son que Docker no tenga recursos suficientes, que la
red externa no exista todavía, o que los demonios sigan arrancando. Después
de revisar los logs, repite las comprobaciones de HDFS y YARN de este
notebook: no hace falta reconstruir las imágenes para una incidencia
habitual. entorno/README.md reúne, en «Solución de problemas», los fallos
más frecuentes de todas las sesiones.
Preguntas para interpretar la experiencia¶
Antes de dar por terminada la sesión, conviene poder responder con las propias palabras:
- ¿Qué diferencia hay entre que Docker muestre un contenedor como
runningy que Hadoop muestre un DataNode comoLive? - ¿Por qué
ls /tmp(en el contenedor) yhdfs dfs -ls /tmpno consultan el mismo lugar? - ¿Qué información guarda el NameNode y qué información guardan los DataNodes?
- ¿Por qué tres réplicas no aparecen como tres ficheros al ejecutar
hdfs dfs -ls? - ¿Qué diferencia existe entre un DataNode y un NodeManager, aunque ambos se ejecuten dentro del mismo contenedor?
- ¿Por qué MapReduce rechaza un directorio de salida que ya existe?
- ¿Qué parte de la orden
yarn jaractúa como cliente y dónde se ejecutan realmente las tareas? - ¿Qué diferencia hay entre la vista de aplicaciones activas del ResourceManager y el histórico del Timeline Server?
- ¿Por qué
timelineserverresuelve al mismo contenedor quenamenodey qué ventaja didáctica tiene conservar el nombre de servicio separado? - ¿Por qué los datos HDFS sobreviven a
stopy también adown, y qué hay que borrar para devolver el laboratorio al estado inicial de las imágenes?
Evidencias para la siguiente sesión¶
Antes de la segunda o tercera sesión tendrás una reunión individual breve (unos minutos) con el profesor para revisar el trabajo de esta sesión. Esa reunión combina dos partes distintas: una demostración en vivo sobre tu propio ordenador y una memoria escrita breve. Mantén el entorno de esta sesión operativo hasta entonces.
Qué mostrar en el ordenador durante la reunión¶
Ten preparado y a mano, funcionando en tu propio equipo:
docker compose ... pscon los cuatro contenedores en ejecución.hdfs dfsadmin -reportcon tres DataNodes vivos.yarn node -list -allcon tres NodeManagers en estadoRUNNING.- La interfaz de histórico del Timeline Server en http://localhost:8188/applicationhistory/, con una aplicación YARN visible.
- Los ficheros copiados en
/user/luser/s1/inputy las ubicaciones de bloques obtenidas conhdfs fsck. - El resultado correcto de WordCount en
/user/luser/s1/output(hadoop 4,mapreduce 2,datos 3,hdfs 2,yarn 2,recursos 1). - Que sabes detener y regenerar el clúster sin reconstruir las imágenes.
Memoria escrita (una o dos páginas)¶
Trae también un documento breve —una o dos páginas, no hace falta más— que
no se limite a pegar capturas de las órdenes anteriores: debe explicar con
tus propias palabras la relación entre NameNode y DataNode, la relación
entre ResourceManager y NodeManager, la diferencia entre una ruta local y
una ruta HDFS, y qué ocurre con los datos al usar stop frente a down.
A partir de ahora conserva este entorno: las siguientes sesiones usarán el mismo HDFS para acceder a datos desde Arrow, Spark y las herramientas de catálogo.
Preparación para la sesión 2¶
En esta sesión el kernel ha sido un Python local que Visual Studio Code
arranca por su cuenta: no has lanzado ningún servidor Jupyter. Desde S2 el
kernel se ejecuta dentro del contenedor namenode, como luser, y tendrás
que arrancar tú el servidor Jupyter allí y conectar el notebook a él. Antes
de abrir s2/s2.ipynb, en una terminal de tu equipo situada en la raíz de la
distribución:
Actualiza el material; trae
s2/y los ficheros deentorno/que añade esa sesión (el warehouse con Hive Metastore y Trino):cd ~/tcdm-public git pull
Comprueba que el clúster de esta sesión está en marcha. Si lo habías parado, esta orden lo arranca de nuevo con los mismos volúmenes HDFS —no hace falta borrar nada—; si ya funciona, no hace nada:

make -C entorno hadoop-up
PostgreSQL, Hive Metastore y Trino no se arrancan todavía: S2 los añade más adelante con
make -C entorno warehouse-up.Arranca Jupyter dentro de
namenode. En otra terminal de tu equipo, entra en el contenedor:
docker exec -it namenode bash
Esa terminal pasa a estar dentro de
namenode(el indicador muestraroot@namenode). Ahí, cambia alusery arranca Jupyter Lab:su - luser jupyter lab --ip=0.0.0.0 --port=8888 --no-browser --IdentityProvider.token=tcdm
su - lusercarga el entorno de login deluser, que pone primero en elPATHel Python del curso (/opt/tcdm/venv, con Jupyter Lab, PySpark y las bibliotecas de las sesiones).--IdentityProvider.token=tcdmfija el token de acceso, así que no hay que buscarlo en los mensajes de Jupyter: la dirección de conexión es siempre la misma. Deja esa terminal abierta mientras trabajes;Ctrl+Cdetiene Jupyter y dosexitte devuelven a tu equipo.make -C entorno jupyterhace lo mismo en una sola orden.En Visual Studio Code, abre
s2/s2.ipynby conéctalo con Select Kernel → Select Another Kernel → Existing Jupyter Server, usando la direcciónhttp://127.0.0.1:8888/lab?token=tcdmy eligiendo el kernel Python 3 (ipykernel).
El Python local que usaste en esta sesión ya no será necesario. El propio
notebook de S2 repite estos pasos con más detalle al principio, en «Antes de
empezar: Jupyter en namenode», también en su versión HTML publicada.
Referencia: arrancar y parar el laboratorio¶
Desde la sesión 2 el laboratorio se maneja con el Makefile de entorno/,
que agrupa las órdenes de Docker Compose de esta sesión y las aplica a todos
los servicios que se vayan añadiendo. Estas dos tablas las reúnen para
tenerlas a mano. Todas se teclean en una terminal de tu equipo, desde la
raíz de la distribución (el directorio que contiene s1/ y entorno/);
ninguna es una celda de notebook.
Arrancar¶
| Orden | Qué hace | Cuándo se usa |
|---|---|---|
make -C entorno hadoop-up |
Crea la red hadoop-cluster si falta y arranca namenode y los tres DataNodes. Si estaban parados, los reanuda; si ya funcionan, no hace nada |
Al empezar cualquier sesión. Equivale a las órdenes docker network create y docker compose ... up -d de esta sesión |
make -C entorno warehouse-up |
Lo anterior y, además, PostgreSQL, Hive Metastore y Trino | Desde la sesión 2; sus ficheros llegan con el git pull de esa sesión |
make -C entorno jupyter |
Arranca Jupyter Lab dentro de namenode como luser; la terminal queda ocupada mientras Jupyter esté en marcha |
Desde la sesión 2. Es el atajo de docker exec -it namenode bash, su - luser y jupyter lab ... |
make -C entorno status |
Lista los contenedores del laboratorio, tanto los que están en marcha como los parados | En cualquier momento, para saber en qué estado está el laboratorio |
Parar y borrar¶
Cuatro órdenes, de menos a más destructiva. Las dos primeras conservan todos los datos; las dos últimas borran volúmenes.
| Orden | Qué hace | Qué se conserva | Qué se pierde |
|---|---|---|---|
make -C entorno stop |
Para los contenedores sin eliminarlos (docker compose stop) |
Todo: los datos HDFS y también el interior de cada contenedor (ficheros locales, histórico del Timeline Server) | Sólo los procesos en marcha, entre ellos Jupyter |
make -C entorno down |
Elimina los contenedores (docker compose down) |
Los volúmenes Docker, con todos los datos HDFS | Lo que vivía dentro de los contenedores: ficheros locales e histórico del Timeline Server |
make -C entorno clean |
down y, además, borra los volúmenes de los servicios que añaden la sesión 2 (Hive Metastore) y la sesión 3 (almacenamiento de objetos) |
Todo HDFS | Las definiciones de tablas y los objetos de esos servicios. Con sólo el clúster de esta sesión equivale a down |
make -C entorno clean-hdfs |
clean y, además, borra los volúmenes HDFS: es el docker volume rm de «Detener, limpiar y recuperar el entorno» |
Sólo las imágenes ya descargadas | Todos los datos: el siguiente arranque parte de un HDFS recién formateado |
Para el uso normal entre sesiones basta stop. down sirve para recrear los
contenedores sin perder datos, por ejemplo si alguno ha quedado en mal
estado. clean y clean-hdfs son deliberadamente destructivas y se dejan
para cuando se quiera empezar de cero.
Después de cualquiera de las cuatro se vuelve a arrancar con
make -C entorno hadoop-up (o warehouse-up desde la sesión 2) y, desde la
sesión 2, hay que lanzar Jupyter de nuevo. Si has hecho un down, el
Timeline Server habrá perdido su histórico: para volver a tener una
aplicación visible en él —una de las evidencias de esta sesión— repite las
celdas de WordCount de «Ejecutar un trabajo MapReduce con YARN».