Fundamentos de la IA
¿Qué ocurre realmente al ejecutar un trabajo de Spark?

Introducción
A medida que los conjuntos de datos crecen, procesarlos eficientemente en una sola máquina puede resultar difícil debido a las limitaciones de la capacidad de la CPU, la memoria y el rendimiento del almacenamiento.
Apache Spark aborda estos desafíos al proporcionar un motor de procesamiento distribuido de datos que puede dividir las cargas de trabajo en particiones y ejecutar tareas en paralelo entre núcleos de CPU y varias máquinas.
Sin embargo, Spark no consiste simplemente en añadir más máquinas.
Comprender cómo funcionan conjuntamente sus componentes —controladores, ejecutores, RDD, DataFrames, particiones, trabajos, etapas y tareas— es esencial para diseñar canalizaciones de datos eficientes y fiables.
En este artículo, explicaré la arquitectura de Spark, sus mecanismos de ejecución y las principales consideraciones de rendimiento mediante un ejemplo práctico de agregación de ventas de comercio electrónico.
1. Por qué Apache Spark: procesamiento paralelo de datos
Apache Spark es un motor de computación distribuida diseñado para el procesamiento de datos a gran escala y la ingeniería de datos.
Spark procesa macrodatos mediante el procesamiento paralelo de datos:

Figura A. Arquitectura general de un clúster distribuido de Apache Spark (clúster, controlador y nodos de trabajo), con una comparación con la ejecución en un solo nodo (creada por Kuriko IWAI)
En comparación con el procesamiento en un solo nodo (a la derecha, Figura A), Spark divide el trabajo en tareas que se ejecutan en varios nodos y, cuando hay recursos disponibles, las distribuye entre distintas máquinas.
En este contexto, «distribuido» no significa que sea obligatorio utilizar varias máquinas. Spark puede ejecutarse en modo local en una sola máquina. Incluso en ese caso, los datos pueden dividirse en particiones y procesarse en paralelo.
2. Arquitectura básica de Apache Spark
Spark consta de varios componentes clave:
- Controlador de Spark (controlador): proceso central que coordina la aplicación de Spark.
- Administrador del clúster: servicio externo encargado de asignar recursos de hardware, como núcleos de CPU y memoria.
- Nodos de trabajo (nodo de trabajo): máquina física o virtual, o nodo de cómputo, dentro del clúster.
- Ejecutores: procesos de trabajo distribuidos que se inician en los nodos de trabajo.
- Java Virtual Machine (JVM): entorno de ejecución necesario para ejecutar el código de bytes de Spark (ya que Spark está escrito en Scala/Java).
En primer lugar, el controlador coordina una aplicación de Spark y la transforma en un grafo acíclico dirigido (DAG) de tareas de ejecución física; después, las programa en los ejecutores.
A continuación, el administrador del clúster asigna recursos según el sistema de gestión de recursos de la implementación.
Los nodos de trabajo proporcionan recursos de cómputo a los ejecutores, que ejecutan tareas y almacenan datos mientras estas se llevan a cabo.
Algunos entornos gestionados, como Databricks, utilizan un ejecutor por cada nodo de trabajo, pero esta no es una regla universal de Spark. Según la configuración del clúster, un nodo de trabajo puede alojar varios ejecutores.
2-1. Caso de uso de comercio electrónico: estructura de los datos de entrada
Imaginemos una pequeña empresa de comercio electrónico.
Tiene 10.000 registros de ventas, cada uno con una unidad de mantenimiento de existencias (SKU) y la cantidad vendida. Supongamos que hay tres SKU posibles: A, B, y C.
| SKU | Registros | Cantidad | Suma esperada |
|---|---|---|---|
| A | 4,000 | 2 | 8,000 |
| B | 3,500 | 3 | 10,500 |
| C | 2,300 | 4 | 9,200 |
| No válidos / devoluciones | 200 | -1 | -200 |
| Total general | 10.000 |
Tabla 1. Ejemplo de desglose de registros de ventas de comercio electrónico y agregaciones esperadas.
Nuestro objetivo es calcular el total de unidades válidas vendidas por SKU:
- Excluir los registros cuya
qty <= 0, y luego - Agrupar las muestras por SKU.
En el conjunto de datos completo del ejemplo, supongamos que quedan 9.800 de los 10.000 registros después del filtrado. Estos son los totales finales que se espera obtener con Spark:
[("A", 8000), ("B", 10500), ("C", 9200)]
2-2. Clúster de Spark
Definiré un clúster simplificado para nuestro caso de uso.
Nodos de trabajo
Configuraré 3 nodos de trabajo, cada uno con una instancia de 8 vCPU y 32 GiB de RAM:

Figura B. Asignación de recursos entre los nodos de trabajo (creada por Kuriko IWAI)
En total, hay 24 vCPU disponibles en el sistema.
Asignación de CPU y memoria a los ejecutores de Spark
Supongamos que asigno:
- 7 ejecutores.
- 2 núcleos de vCPU para cada ejecutor.
- 3 GiB de memoria de heap de la JVM para cada ejecutor.
La aplicación de Spark solicita al administrador del clúster que asigne recursos a estos 7 ejecutores.
A continuación, el administrador del clúster decide cómo asignar las CPU y la memoria en función del uso de recursos de cada trabajador.
Por ejemplo, decide crear 14 ranuras para tareas: 2 núcleos de vCPU por ejecutor × 7 ejecutores:

Figura C-1. Núcleos de vCPU, memoria y asignación de ejecutores entre los trabajadores, con un desequilibrio de recursos en el trabajador A
Ten en cuenta que los 3 GiB de memoria asignados al ejecutor no representan toda su memoria, ya que también debe reservar memoria adicional para los trabajadores y otras ejecuciones.
Suponiendo que una sola tarea requiere 1 vCPU (spark.task.cpus = 1), cada trabajador asigna sus vCPU:

Figura C-2. Asignación paralela de vCPU entre los trabajadores
Esto muestra dos puntos clave:
- Cada trabajador tiene su propio nivel de uso de CPU (en la Figura C-2, el uso del trabajador A es del 75 %, mientras que el de los trabajadores B y C se mantiene en el 50 %).
- Aunque el sistema tiene 24 núcleos de vCPU (8 vCPU por trabajador × 3 trabajadores), solo puede utilizar 14 núcleos de vCPU; los 10 restantes quedan sin usar para estas tareas.
2-3. La máquina virtual de Java (JVM) detrás de Spark
Apache Spark está construido sobre la máquina virtual de Java (JVM).
La JVM es un entorno de ejecución necesario para ejecutar el código de bytes de Spark.
Como Spark está escrito en Scala/Java:
- Al usar Scala o Java en una aplicación: el código se ejecuta de forma nativa dentro de la JVM.
- Al usar PySpark (Python): un proceso de Python ejecuta el código, pero se comunica con un controlador basado en la JVM mediante un puente como Py4J. Los ejecutores de Spark en los nodos trabajadores siguen ejecutándose dentro de una JVM, que luego administra los trabajadores de Python para ejecutar código Python y funciones definidas por el usuario en Python (Python UDF).
En ambos casos, la JVM administra los hilos de ejecución, la asignación de memoria y la recolección de basura tanto para el controlador como para los ejecutores.
JVM del controlador & de los ejecutores en una aplicación PySpark
En una aplicación PySpark, el controlador de Python se comunica con el controlador de Spark que se ejecuta en la JVM;
- Controlador de Python: donde se escribe y ejecuta el código PySpark de la aplicación.
- JVM del controlador: coordina la ejecución y la planificación de Spark.
- JVM del ejecutor: ejecuta las tareas de Spark y administra los recursos de ejecución. Cuando las tareas requieren ejecutar código Python, intervienen procesos de trabajo de Python.
- JVM del cliente (proceso del cliente o máquina desde la que se envía la aplicación): JVM que se ejecuta en la máquina local o en un nodo de borde o de puerta de enlace.
Como se muestra en la figura, la JVM del cliente se conecta primero a la JVM del controlador, que luego coordina con las JVM de los ejecutores:

Figura D. Relación general entre las JVM del cliente, del controlador y de los ejecutores
A partir de aquí, la JVM del controlador aloja el controlador de Spark para administrar la aplicación, mientras que las JVM de los ejecutores alojan los ejecutores individuales encargados de ejecutar esas tareas.
Cada controlador de JVM dispone de un heap de JVM para procesar datos, almacenar en caché y gestionar objetos de usuario de forma eficiente.
2-4. Análisis en profundidad de los conjuntos de datos distribuidos resilientes (RDD)
Un conjunto de datos distribuido resiliente (RDD) es una colección inmutable y particionada de elementos que Spark puede procesar en paralelo.
Los RDD registran las dependencias entre las transformaciones, lo que permite a Spark volver a calcular las particiones perdidas en muchos escenarios de fallo.
El nombre refleja tres propiedades:
- Resiliente: Spark registra el linaje —qué RDD depende de qué RDD anterior— y, a menudo, puede reconstruir las particiones que faltan en lugar de mantener réplicas de cada resultado intermedio.
- Distribuido: el conjunto de datos se representa mediante particiones que Spark puede procesar en distintos ejecutores. No es necesario contar con varias máquinas físicas; el modo local también admite RDD.
- Conjunto de datos: un RDD puede contener pares de clave y valor, números enteros, cadenas, tuplas y otros elementos compatibles, no solo filas relacionales. Las API de RDD incluyen operaciones como
map,filter,reduceByKey,join, yaggregateByKey.
Un RDD es una abstracción lógica de colección y cálculo, no una declaración de que determinadas particiones estén asignadas permanentemente a determinados trabajadores.
Particiones
Una partición es un fragmento lógico de datos y normalmente corresponde a una tarea dentro de una etapa.
Supongamos que creamos 100 particiones (estas 100 particiones iniciales son una elección didáctica explícita).
Con 10.000 elementos de origen, hay aproximadamente 100 elementos por partición si la entrada se distribuye de manera más o menos uniforme:

Figura E. Asignación paralela de particiones entre 100 divisiones iniciales de datos
3. API de RDD frente a API de DataFrame: optimización interna de Catalyst
El ejemplo original de RDD ayuda a entender el linaje, las particiones y la redistribución de datos (shuffle).
Sin embargo, en los procesos ETL estructurados habituales, la API de DataFrame es el punto de partida preferido, porque ofrece columnas, información del esquema, expresiones similares a SQL y operaciones visibles para el optimizador.
El diagrama siguiente ilustra la relación:

Figura F. Relación entre los conceptos de grafos de Spark: linaje de RDD, DataFrame y DAG de ejecución de Spark
Al igual que el RDD tiene un linaje que define las transformaciones, el plan de consulta de DataFrame representa su consulta estructurada.
Spark construye el trabajo de ejecución a partir de las dependencias y los operadores físicos.
La ejecución de DataFrame puede utilizar internamente mecanismos basados en RDD, pero eso no significa que los usuarios tengan que construir manualmente el linaje de RDD al escribir código de DataFrame.
3-1. Plan de consulta de DataFrame
Un plan de consulta de DataFrame es la secuencia de pasos lógicos y físicos que genera un motor de ejecución (el recuadro morado de la Figura L).
Una consulta de DataFrame pasa por varias representaciones:
- Plan lógico: descripción de las operaciones solicitadas (lectura, filtrado, agrupación y agregación).
- Plan lógico analizado y optimizado: Spark resuelve las referencias y aplica las optimizaciones lógicas pertinentes, como la eliminación de columnas o el filtrado anticipado de predicados.
- Plan físico: Spark elige operadores ejecutables, por ejemplo, lecturas, agregaciones parciales y finales, intercambios, combinaciones y ordenaciones.
- Ejecución: El plan físico se ejecuta mediante los mecanismos de ejecución de Spark y, en última instancia, implica tareas en los ejecutores.
En nuestro caso de uso, el plan físico puede incluir los siguientes pasos:
- Agregación hash parcial,
- intercambio por SKU, y
- agregación hash final.
3-2. Comparación de acciones: API de RDD frente a API de DataFrame
Comparemos las API de RDD y DataFrame tomando como ejemplo la agregación por SKU.
API de RDD
Los elementos son (sku, qty) pares. Nuestra lógica de agregación en Python es explícita y Spark sigue las dependencias del RDD:
result_rdd = (
raw_rdd
.filter(lambda row: row[1] > 0)
.reduceByKey(lambda a, b: a + b, numPartitions=4)
)
rdd_results = result_rdd.collect()
# Example:
# [('A', 8000), ('B', 10500), ('C', 9200)]
API de DataFrame
Supongamos que sales_df tiene las columnas sku (de tipo cadena) y qty (numérica):
from pyspark.sql import functions as F
result_df = (
sales_df
.filter(F.col("qty") > 0)
.groupBy("sku")
.agg(F.sum("qty").alias("total_qty"))
)
df_results = result_df.collect()
# Example:
# [Row(sku='A', total_qty=8000),
# Row(sku='B', total_qty=10500),
# Row(sku='C', total_qty=9200)]
En este caso, ambos ejemplos resuelven el mismo problema empresarial:
Filtrar los registros de ventas no válidos y agregar las cantidades válidas por SKU.
Y en ambos casos, collect() activa la ejecución de Spark y devuelve todos los registros resultantes al controlador.
La diferencia clave está en la representación del resultado:
- API de RDD: devuelve elementos de Python, como
(sku, total_qty)tuplas. - API de DataFrame: devuelve objetos
Rowestructurados con campos con nombre, comoskuytotal_qty.
A continuación, se muestra una comparación general de las API de RDD y DataFrame:
| API de RDD | API de DataFrame / SQL | |
|---|---|---|
| Representación de datos | Elementos generales, incluidos objetos arbitrarios compatibles | Filas con columnas con nombre y esquema |
| Estilo de programación | Transformaciones de bajo nivel | Transformaciones relacionales declarativas |
| Agregaciones | Sí: reduceByKey, aggregateByKey, etc. |
Sí: groupBy, agg, SQL |
| Optimización de consultas | Se expone menos información semántica relacional | Optimización de Catalyst y de la ejecución física |
| Uso habitual | Procesamiento especializado de bajo nivel / enseñanza de los mecanismos de ejecución | ETL estructurado, análisis y creación de características |
| Ejecución | Trabajos, etapas y tareas de Spark | Trabajos, etapas y tareas de Spark |
Tabla 2. API de RDD frente a API de DataFrame: comparación de características y nivel de abstracción (elaborada por Kuriko IWAI)
4. Conclusión
El objetivo de ajustar Spark no es maximizar por separado el número de particiones ni el uso de la CPU.
Se trata de reducir el costo y la latencia de la ejecución de extremo a extremo sin introducir inestabilidad.
En este artículo, conocemos las relaciones fundamentales de su sistema:
- Recursos: Los nodos de trabajo alojan ejecutores; los ejecutores ejecutan tareas; la CPU y la memoria configuradas limitan la concurrencia.
- Programación con RDD: Las transformaciones generan definiciones inmutables de RDD y su linaje; las acciones desencadenan el cálculo.
- Programación con DataFrame: Las transformaciones generan operaciones de consulta lógica; la optimización selecciona operadores físicos ejecutables; las acciones desencadenan el cálculo.
- Ejecución: Spark organiza el trabajo en trabajos, etapas y tareas, y las tareas procesan particiones en los ejecutores. Los intercambios de datos (shuffles) y otras dependencias crean límites de coordinación entre las etapas.
Comprender cómo Spark ejecuta cargas de trabajo distribuidas es solo el punto de partida.
Puedes aprender un flujo de cálculo de siete pasos con la API de RDD.












