Saltar al contenido

lección 2

De Hadoop a Spark: la historia del procesamiento a escala

De MapReduce (2004) a Spark (2014), su arquitectura y los conceptos que lo sostienen: RDD, DataFrame, linaje, Catalyst y el DAG.

70 min

### El paper que cambió la industria

Año 2003. Google tiene un problema que nadie más tiene todavía: necesita indexar TODO Internet. Miles de millones de páginas web. Ninguna base de datos del mundo puede con eso. Ningún supercomputador es suficiente. Así que los ingenieros de Google inventan algo nuevo: un sistema para dividir un cálculo enorme entre miles de máquinas baratas que trabajan en paralelo.

En 2004 publican el famoso paper "MapReduce: Simplified Data Processing on Large Clusters". La idea era elegantemente simple: cualquier cómputo se puede expresar en dos fases. Primero un MAP (aplicas una función a cada elemento de forma independiente) y luego un REDUCE (combinas los resultados parciales en un resultado final). Si puedes expresar tu problema en estas dos fases, Google te garantiza que se ejecutará en paralelo sobre miles de máquinas sin que tú te preocupes por la coordinación.

Ejemplo mental: quieres contar cuántas veces aparece cada palabra en 10.000 libros. MAP: cada máquina recibe 100 libros y produce pares (palabra, 1) por cada ocurrencia. REDUCE: otra máquina recibe todos los pares de la misma palabra y los suma. Resultado: un diccionario con las frecuencias de todas las palabras. Simple, escalable, elegante.

### Hadoop: MapReduce para el resto del mundo

Google publicó el paper pero no el código. En 2006, Doug Cutting (que estaba construyendo un motor de búsqueda open source) implementó MapReduce como software libre. Lo llamó Hadoop, por el elefante de peluche de su hijo. Hadoop tenía dos componentes: HDFS (un sistema de archivos distribuido, inspirado en otro paper de Google) y MapReduce (el motor de cómputo).

Hadoop fue una revolución. Por primera vez, empresas normales (no solo Google) podían procesar terabytes de datos usando clusters de máquinas baratas. Yahoo, Facebook, LinkedIn y cientos de empresas adoptaron Hadoop entre 2008 y 2013. Nació toda una industria alrededor del "Big Data".

Pero Hadoop tenía un problema enorme que lo hacía doloroso de usar en la práctica...

### El dolor de MapReduce: verboso, lento, frustrante

Escribir un programa en MapReduce era como escribir una carta formal para pedir un vaso de agua. Para hacer algo tan simple como contar palabras, necesitabas escribir 60-80 líneas de Java repartidas en tres clases: un Mapper, un Reducer y un Driver. Para un JOIN entre dos tablas, podías necesitar 200+ líneas de código y entender conceptos como particionadores personalizados y ordenación secundaria.

1# Así se veía un "word count" conceptual en MapReduce (pseudo-código)
2# En la realidad, esto eran 80 líneas de Java con boilerplate
3
4# 1. Clase Mapper (se ejecuta en cada nodo)
5class WordCountMapper:
6 def map(self, key, value):
7 # key = número de línea, value = texto de la línea
8 for word in value.split():
9 emit(word, 1) # emite (palabra, 1) para cada ocurrencia
10
11# 2. Clase Reducer (se ejecuta tras el shuffle)
12class WordCountReducer:
13 def reduce(self, key, values):
14 # key = una palabra, values = lista de 1s
15 emit(key, sum(values)) # suma todas las ocurrencias
16
17# 3. Clase Driver (configura y lanza el job)
18class WordCountDriver:
19 def main(self):
20 job = Job("word-count")
21 job.setMapper(WordCountMapper)
22 job.setReducer(WordCountReducer)
23 job.setInput("/datos/libros/")
24 job.setOutput("/resultados/frecuencias/")
25 job.submit() # lanza al cluster y espera

MapReduce: 3 clases y mucho boilerplate para contar palabras

Pero el verdadero problema no era la verbosidad del código — era la LENTITUD. MapReduce escribía todos los resultados intermedios a disco (HDFS) entre cada fase Map y cada fase Reduce. Si tu pipeline tenía 5 pasos encadenados, los datos se escribían y leían del disco 5 veces. Los discos duros son lentos. Terriblemente lentos comparados con la RAM.

Un job que procesaba 1 TB de datos con 3 pasos podía tardar 2-3 horas, de las cuales el 80% era esperar a que los discos leyeran y escribieran datos intermedios que nadie necesitaba guardar permanentemente. Era como si cada vez que necesitas un ingrediente mientras cocinas, tuvieras que guardarlo en el almacén del sótano y luego ir a buscarlo de nuevo.

En 2012-2013, vi equipos enteros dedicados solo a "optimizar MapReduce jobs". Ingenieros brillantes pasando semanas afinando particionadores, combiners y compresión intermedia para bajar un job de 4 horas a 2 horas. Era una pérdida de talento absurda. Spark eliminó gran parte de ese sufrimiento.

### Spark: la revolución de la memoria

En 2009, un estudiante de doctorado en UC Berkeley llamado Matei Zaharia tenía una frustración: estaba intentando hacer machine learning iterativo sobre datos grandes con Hadoop, y cada iteración escribía a disco y tardaba una eternidad. Se preguntó: ¿y si los datos intermedios se quedan en memoria RAM entre pasos en vez de ir al disco?

Esa pregunta dio origen a Spark. El paper original (2010) introducía los RDDs (Resilient Distributed Datasets): colecciones de datos que viven en memoria distribuida entre los nodos del cluster. Los datos intermedios no van al disco — se pasan de una transformación a la siguiente directamente en RAM. El resultado: Spark era 10-100x más rápido que MapReduce para la mayoría de workloads.

En 2014, Spark se graduó como proyecto top-level de Apache. En 2015, superó a Hadoop MapReduce en popularidad. Para 2017, prácticamente nadie escribía MapReduce nuevo — todo era Spark. Y la API era radicalmente más simple:

1# El mismo word count... en Spark (PySpark)
2from pyspark.sql import SparkSession
3
4spark = SparkSession.builder.appName("word-count").getOrCreate()
5
6# Lee los archivos, cuenta palabras — 4 líneas vs 80
7resultado = (
8 spark.read.text("/datos/libros/")
9 .selectExpr("explode(split(value, ' ')) as palabra")
10 .groupBy("palabra")
11 .count()
12 .orderBy("count", ascending=False)
13)
14
15resultado.show(20)
16# +----------+------+
17# | palabra | count|
18# +----------+------+
19# | the| 84723|
20# | of| 56291|
21# | and| 48902|
22# ...

El mismo word count en Spark: 4 líneas funcionales vs 80 de MapReduce

### La línea temporal del Big Data

  1. 01.2003 — Google publica el paper de GFS (Google File System)
  2. 02.2004 — Google publica el paper de MapReduce
  3. 03.2006 — Doug Cutting crea Hadoop (HDFS + MapReduce open source)
  4. 04.2008-2012 — Era dorada de Hadoop: toda empresa quiere un cluster
  5. 05.2009 — Matei Zaharia empieza Spark en UC Berkeley
  6. 06.2010 — Paper original de Spark (RDDs: datos en memoria)
  7. 07.2013 — Spark introduce DataFrames (API tabular, como SQL)
  8. 08.2014 — Spark se convierte en proyecto Apache top-level
  9. 09.2015 — Spark supera a MapReduce en adopción
  10. 10.2016 — Spark 2.0: API unificada DataFrames + SQL + Streaming
  11. 11.2020 — Spark 3.0: Adaptive Query Execution, mejoras masivas
  12. 12.2022 — Spark 3.3: soporte nativo para Pandas API on Spark
  13. 13.2024 — Spark 4.0: PySpark como API principal, mejoras de rendimiento
  14. 14.2025 — Spark sigue siendo el estándar para datos distribuidos. Alternativas (Polars, DuckDB) para datos que caben en un solo nodo
La evolución: Hadoop dominó 2006-2014, Spark tomó el relevo y sigue reinando

### ¿Por qué Spark es más rápido? La diferencia clave

La diferencia fundamental es dónde viven los datos intermedios. En MapReduce, entre cada paso los datos van al disco (HDFS). En Spark, se quedan en memoria RAM. La RAM es 100-1000x más rápida que un disco duro y 10-100x más rápida que un SSD. Esa diferencia se multiplica por cada paso del pipeline.

Imagina que cocinas una cena de 5 platos. En el modelo MapReduce, después de cortar cada ingrediente lo guardas en el frigorífico del sótano, y cuando lo necesitas para el siguiente paso bajas a buscarlo. En el modelo Spark, dejas todo en la encimera de la cocina, al alcance de la mano. El resultado es el mismo, pero uno tarda 3 horas y otro 45 minutos.

Consejo de senior: cuando alguien te diga "usamos Hadoop" en una entrevista, pregunta si se refieren a HDFS (el almacenamiento, que sigue vigente) o a MapReduce (el motor de cómputo, que está prácticamente muerto). Muchos clusters "Hadoop" en realidad ejecutan Spark sobre HDFS.

### La arquitectura de Spark en 5 minutos

Un cluster Spark tiene tres componentes principales: el Driver (tu programa, el cerebro que coordina), el Cluster Manager (asigna recursos) y los Executors (los trabajadores que procesan datos). Cuando envías un job, el Driver crea un plan de ejecución, el Cluster Manager le asigna executors, y cada executor procesa su porción de datos en paralelo.

  • Driver: tu programa PySpark. Decide QUÉ hacer y en qué orden. No procesa datos directamente.
  • Cluster Manager: YARN, Mesos, Kubernetes o Standalone. Gestiona los recursos del cluster.
  • Executors: procesos JVM en los nodos del cluster. Cada uno tiene su porción de memoria y CPUs. Hacen el trabajo real.
  • Tasks: la unidad mínima de trabajo. Un executor puede ejecutar varias tasks en paralelo (una por core).
El Driver decide QUÉ hacer, el Cluster Manager reparte recursos y los Executors ejecutan las tasks en paralelo, cada uno sobre su porción de datos

Fíjate en un detalle: hasta ahora hemos hablado de la arquitectura FÍSICA, las máquinas. El Driver es una máquina, los Executors son máquinas. Pero Spark no te obliga a pensar en máquinas cuando programas. Tú manipulas unas abstracciones LÓGICAS que Spark se encarga de repartir por esas máquinas. Esas abstracciones (RDD, DataFrame, el linaje, el DAG) son el verdadero corazón de Spark, y son lo que vas a usar en cada ejercicio del resto de la skill. Vamos a explicarlas bien, porque Spark es complicado y no quiero que uses ni una sola palabra sin entenderla.

### RDD: el ladrillo original sobre el que se construyó todo

Cuando Matei Zaharia inventó Spark, la pieza central de su tesis fue una estructura de datos con un nombre que asusta: RDD, Resilient Distributed Dataset. Suena a jerga académica, pero cada una de esas tres palabras describe una idea concreta y genial. Vamos a desmontar el nombre pieza por pieza, porque si entiendes el RDD entiendes por qué Spark funciona como funciona.

Empecemos por el final: "Dataset". Un RDD es, ante todo, una colección de objetos. Registros. Filas de un CSV, líneas de un log, tuplas, objetos de Python... lo que sea. Piensa en él como en una lista gigante de Python, pero que puede tener miles de millones de elementos. Esa es la parte fácil: un RDD es una colección de datos.

Ahora la parte interesante: "Distributed" (distribuido). Esa colección gigante NO vive en una sola máquina. Está cortada en trozos llamados PARTICIONES, y cada partición vive en la memoria de un executor distinto. Si tienes 1.000 millones de filas y 10 executors, Spark parte la colección en, digamos, 200 particiones y las reparte: cada executor se queda con unas 20 particiones en su RAM. Cuando aplicas una operación, cada executor la ejecuta sobre SUS particiones, en paralelo, sin hablar con los demás. Ahí está el paralelismo.

La analogía: imagina que tienes que corregir un examen de un millón de alumnos. Si lo haces tú solo (una máquina, Pandas), tardas meses. Lo que haces es partir el montón de exámenes en 200 pilas (particiones) y repartirlas entre 10 profesores (executors). Cada profesor corrige sus pilas a la vez que los demás. Una partición es una pila de exámenes: la unidad de trabajo que un profesor coge y procesa de una sentada. Ese es el detalle que explica el df.rdd.getNumPartitions() que verás en los ejercicios: te dice en cuántas pilas está cortada tu colección ahora mismo.

Y llegamos a la palabra que da nombre a la idea: "Resilient" (resiliente, tolerante a fallos). Aquí está el golpe de genio de Zaharia. En un cluster de 100 máquinas baratas, que se muera una máquina no es una desgracia excepcional: es el pan de cada día. Y si un executor muere, se lleva a la tumba las particiones que tenía en su RAM. ¿Cómo sobrevive Spark a eso sin perder datos?

La respuesta obvia (la de Hadoop) era: replica todo tres veces. Guarda cada partición en tres máquinas distintas, y si una cae usa otra copia. Funciona, pero es carísimo: triplicas el almacenamiento y pagas un peaje constante copiando datos por la red. Zaharia propuso algo mucho más elegante: en lugar de guardar copias de los datos, Spark guarda la RECETA para reconstruirlos. A esa receta se le llama LINAJE (lineage).

El linaje es la secuencia exacta de transformaciones que creó cada RDD: "leí este fichero, filtré por ciudad, calculé esta columna". Spark recuerda esa cadena completa. Así que cuando un executor muere y se pierde la partición 47, Spark no entra en pánico: mira el linaje, ve cómo se calculó la partición 47 desde su origen, y la RECALCULA en otro executor que esté vivo. No necesita copias caras porque siempre sabe cómo volver a fabricar cualquier trozo de datos desde el principio.

La analogía que a mí me hizo click: imagina una receta de tarta escrita paso a paso. Si a mitad de camino se te cae al suelo el bol con la masa (se muere un executor), no necesitas tener tres tartas de repuesto en la nevera. Tienes la receta: vuelves al paso donde estabas y rehaces esa masa concreta. El linaje es la receta; la resiliencia es poder rehacer cualquier paso porque tienes las instrucciones apuntadas.

Esto conecta con otra propiedad fundamental: los RDD son INMUTABLES. Un RDD nunca se modifica. Cuando aplicas una transformación (un filtro, un mapeo), no cambias el RDD original: creas uno nuevo que apunta al anterior como su "padre". Por eso el linaje es posible: como nada se sobreescribe, la cadena de "quién viene de quién" queda intacta y Spark siempre puede recorrerla hacia atrás para reconstruir lo que haga falta.

Junta las tres ideas y entiendes por qué el RDD fue revolucionario frente a MapReduce. MapReduce, para sobrevivir a fallos, escribía los datos intermedios a disco entre cada paso (lento, como vimos). El RDD mantiene los datos en RAM y consigue la tolerancia a fallos "gratis" gracias al linaje, sin tocar el disco. Datos en memoria + resiliencia por recálculo: esa combinación es lo que hizo a Spark entre 10 y 100 veces más rápido.

Consejo de senior: el 99% del tiempo NO vas a escribir RDDs a mano (ahora usamos DataFrames, ya lo verás). Pero entender el RDD no es folclore histórico: cuando en producción veas un stage que se recalcula entero porque se cayó un nodo, o un error de OutOfMemory al cachear, o te preguntes por qué .cache() existe, todo eso son consecuencias directas del modelo RDD. El que entiende el ladrillo entiende el edificio.

### DataFrame: el RDD que aprendió a leer tablas

El RDD era potente pero crudo. Para Spark, un RDD era una bolsa de objetos opacos: sabía que había un millón de "cosas", pero no sabía si esas cosas tenían columnas, ni de qué tipo eran, ni nada. Programar con RDDs era como trabajar con listas de tuplas sin nombres: fila[3] es el precio... o era fila[4]? Funcionaba, pero era incómodo y, sobre todo, Spark no podía optimizar casi nada porque no entendía la forma de tus datos.

En 2013 llegó la evolución: el DataFrame. Un DataFrame es, ni más ni menos, un RDD con ESQUEMA. Es decir: la misma colección distribuida e inmutable de siempre, pero ahora organizada en columnas con nombre y tipo, exactamente como una tabla de SQL o un DataFrame de Pandas. En vez de fila[3], escribes col("precio"). En vez de una bolsa de objetos, tienes una tabla que Spark entiende de verdad.

Y aquí está la relación clave que tienes que grabarte: un DataFrame se construye SOBRE un RDD. Por debajo, un DataFrame sigue siendo un RDD (de objetos Row) más un esquema que describe las columnas. El DataFrame es la capa bonita y con nombres; el RDD es la maquinaria de particiones, distribución y linaje que sigue funcionando debajo. Son la misma cosa vista a dos alturas distintas.

Esto explica por fin un detalle que aparece en los ejercicios de las próximas lecciones: cuando escribes df.rdd estás "bajando" desde el DataFrame hasta el RDD que tiene debajo. Y como el RDD es quien sabe de particiones, df.rdd.getNumPartitions() te devuelve en cuántas particiones está repartida tu tabla. No es magia ni una API rara: es que atraviesas la capa DataFrame para preguntarle al RDD subyacente, que es el que gestiona la distribución física.

1# Un DataFrame por fuera es una tabla con columnas y tipos...
2df = spark.read.option("header", "true").option("inferSchema", "true").csv("ventas.csv")
3df.printSchema()
4# root
5# |-- ciudad: string
6# |-- producto: string
7# |-- precio_unitario: double
8# |-- cantidad: integer
9
10# ...pero por debajo sigue habiendo un RDD gestionando las particiones.
11# "Bajamos" al RDD subyacente para preguntarle cosas físicas:
12print(df.rdd.getNumPartitions()) # ej: 8 -> en 8 trozos repartidos entre executors
13
14# df.rdd NO es un objeto nuevo y raro: es el RDD que SIEMPRE estuvo debajo del DataFrame.
15# El DataFrame = ese RDD + un esquema (nombres y tipos de columna).

El DataFrame es la capa con esquema; el RDD que tiene debajo sigue gestionando las particiones

Entonces, si el DataFrame es solo un RDD con esquema, por qué usamos DataFrames el 99% del tiempo y casi nunca RDDs a pelo? Por una razón enorme: como el DataFrame conoce la forma de tus datos (columnas y tipos), Spark puede OPTIMIZAR tu código automáticamente antes de ejecutarlo. Y ese optimizador tiene nombre.

### Catalyst y Tungsten: el cerebro y los músculos

Cuando escribes operaciones sobre un DataFrame, Spark NO ejecuta tu código tal y como lo escribiste. Primero lo pasa por un componente llamado CATALYST, que es el optimizador de consultas. Catalyst mira todo lo que quieres hacer, lo analiza y lo reescribe en una versión equivalente pero más eficiente, igual que un compilador optimiza el código que escribes o un ajedrecista experto reordena sus jugadas para llegar antes al mismo jaque mate.

Dos ejemplos de lo que hace Catalyst, para que se entienda la idea (los verás en detalle en la próxima lección): si pides tres columnas de una tabla que tiene cincuenta, Catalyst lee solo esas tres del disco y se ahorra las otras cuarenta y siete. Y si filtras las filas de Madrid después de un cruce carísimo, Catalyst es capaz de "empujar" ese filtro hacia el principio, para cruzar muchísimas menos filas. Tú escribes en el orden que te resulta natural; Catalyst reordena para que sea rápido.

Su compañero se llama TUNGSTEN, y es el motor de ejecución de bajo nivel. Si Catalyst es el cerebro que decide el mejor plan, Tungsten son los músculos que lo ejecutan: gestiona la memoria de forma muy eficiente (sin el desperdicio típico de los objetos Java) y genera código máquina ajustado para exprimir la CPU. No necesitas configurar nada de esto; trabajan solos por debajo. Lo importante que debes llevarte es el porqué: como el RDD "crudo" no tiene esquema, Catalyst no puede optimizarlo, y por eso el RDD a pelo hoy casi no se usa. El DataFrame es rápido precisamente porque le da a Catalyst y Tungsten el contexto que necesitan para hacer su magia.

Consejo de senior: esta es la regla práctica que le daría a mi yo de hace años. Usa siempre DataFrames (o SQL de Spark, que va por el mismo Catalyst). Baja a RDD solo en el rarísimo caso de que necesites un control de bajo nivel que la API de DataFrame no te da. Cada vez que dudes entre "lo hago con una función de columna del DataFrame" o "lo hago con un map de RDD y una función Python", elige el DataFrame: dejas que Catalyst optimice en vez de convertirte tú en el cuello de botella.

### El DAG: el mapa de cómo se construyen tus datos

Ya tenemos casi todas las piezas. Falta poner nombre a algo que ya has visto sin saberlo: el linaje del que hablábamos con los RDDs, esa cadena de "este dato viene de este otro", tiene forma de grafo. Y ese grafo tiene un nombre técnico que aparecerá una y otra vez: el DAG, Directed Acyclic Graph, o grafo dirigido acíclico.

Vamos con el nombre otra vez, porque describe exactamente lo que es. Un GRAFO es simplemente un conjunto de nodos (cada operación: leer, filtrar, agrupar) conectados por flechas (las dependencias entre ellas). "DIRIGIDO" significa que las flechas tienen sentido: los datos fluyen en una única dirección, del origen hacia el resultado, nunca al revés; el filtro depende de la lectura, no la lectura del filtro. "ACÍCLICO" significa que no hay ciclos: no puedes volver a un paso anterior dando vueltas, el flujo siempre avanza hacia adelante hasta el resultado final. Un DAG es, en el fondo, una lista de pasos con sus dependencias que nunca se muerde la cola.

Y aquí se cierra el círculo con todo lo anterior: el linaje de un RDD ES ese DAG. Cuando encadenas transformaciones (lee, filtra, calcula una columna, agrupa), Spark va construyendo internamente ese grafo dirigido acíclico de dependencias. El DAG y el linaje son la misma cosa: el mapa de cómo se fabrica cada trozo de tus datos a partir de sus orígenes. Por eso Spark puede recalcular una partición perdida (recorre el DAG hacia atrás) y por eso Catalyst puede optimizar (reordena los nodos del DAG antes de ejecutar). Todo es el mismo grafo.

DataFrame (con esquema) → RDD subyacente → particiones repartidas entre executors. df.rdd baja de la capa tabular a la física; si un executor muere, sus particiones se recalculan gracias al linaje

Un último apunte para no dejar cabos sueltos: puede que te preguntes cuándo se construye y se ejecuta este DAG. La respuesta es que Spark es "perezoso": va apuntando las transformaciones en el DAG sin ejecutar nada, y solo cuando le pides un resultado concreto (una "acción") ejecuta el plan entero optimizado. Esa distinción entre transformaciones y acciones es tan importante que tiene su propia lección: la verás en detalle en la lección 4. Por ahora quédate con el mapa mental: RDD (datos distribuidos, inmutables, resilientes por linaje) → DataFrame (ese RDD con esquema) → Catalyst y Tungsten (que optimizan y ejecutan) → DAG (el linaje hecho grafo).

En la lección 3 vamos a levantar nuestro propio cluster Spark con Docker para que puedas experimentar con todo esto de forma local: verás las particiones de verdad, contarás cuántas hay con getNumPartitions() y observarás cómo el DataFrame reparte el trabajo entre los executors. Pero antes, practiquemos lo que hemos aprendido sobre la historia, la arquitectura y los conceptos fundamentales de Spark.

## ejercicios

[01]

MapReduce vs Spark: la diferencia en código

Escribe el equivalente en PySpark de este pseudo-código MapReduce: dado un archivo de logs con líneas "NIVEL mensaje" (ej: "ERROR conexión timeout"), cuenta cuántos logs hay de cada nivel (INFO, WARN, ERROR).

Cargando editor...
[02]

Calcular la ventaja de Spark sobre MapReduce

Un pipeline tiene 4 pasos. En cada paso intermedio se escriben 10 GB de datos. Un HDD lee/escribe a 150 MB/s. La RAM opera a 25 GB/s. Calcula cuánto tiempo pierden MapReduce (disco) y Spark (RAM) solo en I/O intermedio.

Cargando editor...
[03]

Identificar los componentes de Spark

Completa las descripciones de cada componente de la arquitectura Spark. Para cada componente, escribe una frase que explique su rol.

Cargando editor...
[04]

RDD vs DataFrame: clasifica las afirmaciones

Un compañero junior te suelta 6 afirmaciones sobre RDD y DataFrame, pero mezcla churras con merinas. Clasifica cada una como "RDD", "DataFrame" o "AMBOS", y añade una nota corta que justifique tu decisión. Recuerda: un DataFrame es un RDD con esquema, así que muchas propiedades del RDD las heredan los dos.

Cargando editor...
[05]

Se cae un executor: explica cómo el linaje salva el día

Estás corriendo un job Spark de madrugada. A mitad de ejecución, uno de los 5 executors muere (la máquina se quedó sin RAM y el cluster manager la mata). El executor tenía en memoria las particiones P3 y P4. Explica, paso a paso, qué hace Spark para no perder esos datos, y por qué NO necesita tener copias replicadas de P3 y P4. Escribe tu razonamiento en la variable explicacion.

Cargando editor...

Regístrate para guardar tu progreso.

## comentarios

Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.

Inicia sesión para comentar y responder.

cargando comentarios...