Hadoop vs Spark

Spark did not replace Hadoop; it replaced MapReduce. In this lesson Spark runs on our own YARN cluster and reads our HDFS files, gives exactly the same answers as our MapReduce job, and shows inside its plan the map, combiner and shuffle you already know. Then we measure where it wins: five passes over the same data take 199 s as MapReduce jobs and 35 s with Spark caching.

Module 13 · Hadoop in industry today⏱ 30 minIntermediateSpark 4.0.4 on our YARNMapReduce vs Spark, timedRuns in your browser (Python)8 practice problems
1 The story

Five questions, the same data

The canteen manager has five questions about the same 90 lakh orders: how many orders above ₹0, above ₹18, above ₹20, above ₹25, above ₹30. Riya writes them as five MapReduce jobs: 199 seconds. Kabir installs Apache Spark on the very same cluster, tells it to keep the data in memory after the first pass, and asks the same five questions: 35 seconds. Same HDFS, same YARN, same three workers.

MapReduce: 5 jobs, each reads HDFS and writes HDFS pass 1 · 39 s pass 2 · 39 s pass 3 · 39 s pass 4 · 41 s pass 5 · 41 s total 199 s · every pass starts an ApplicationMaster and reads 140 MB from disk again Spark with cache(): read once, keep it in the executors' memory 21.4 s 4.2 · 3.7 · 3.3 · 2.7 s from memory total 35.3 s · the first pass pays for reading and caching, the rest are almost free Real runs on our 3-worker lab. Spark ran ON Hadoop: data in HDFS, executors in YARN containers. myphatshala.in
Riya

So Spark is the new Hadoop? Should we throw HDFS and YARN away?

Arjun Sir

No. Look where Spark ran: its executors were YARN containers, and it read the file from HDFS. Spark replaced MapReduce, the processing engine. Storage and resource management stayed. Most "Hadoop clusters" today are HDFS + YARN running Spark, Hive on Tez, and a few old MapReduce jobs.

Kabir

And the speed-up isn't magic. Spark builds the whole job as a graph, keeps intermediate data in memory instead of writing it to HDFS between steps, and reuses the same executors for every step. The more passes, the bigger the win.

Arjun Sir

Today we run the same job three ways on our cluster (MapReduce, Spark RDDs, Spark DataFrames), read Spark's plan to find the map, the combiner and the shuffle we already know, and measure where caching helps.

2 Definition

Apache Spark, and how it relates to Hadoop

Formal definition (write this in exams)

Apache Spark is a distributed data processing engine that represents a computation as a directed acyclic graph (DAG) of transformations over partitioned datasets (RDDs, and the higher-level DataFrames and Datasets). Transformations are lazy: nothing runs until an action (such as collect or write) asks for a result. The DAG is cut into stages at shuffle boundaries; each stage runs as parallel tasks, one per partition, inside long-lived executor processes coordinated by a driver. Intermediate results can be kept in memory (caching/persistence), and lost partitions are recomputed from their lineage. For DataFrames and SQL, the Catalyst optimizer rewrites the plan (column pruning, predicate pushdown, partial aggregation) and the Tungsten engine executes it with compact binary memory layouts and generated code. Spark has no storage or cluster manager of its own: it commonly runs on Hadoop YARN and reads HDFS, or runs on Kubernetes or its standalone manager with cloud object storage.

In simple words

Spark is a faster engine you can plug into a Hadoop cluster instead of MapReduce. You describe the whole job once; Spark plans it, runs it in executors that stay alive, and keeps data in memory between steps instead of writing it to disk after every step.

Words you'll meet in this lesson

DriverThe process that runs your program, builds the DAG and schedules tasks.
ExecutorA long-lived JVM (on YARN: a container) that runs tasks and caches data.
RDDResilient Distributed Dataset: partitions plus the lineage to rebuild them.
DataFrameA table with a schema; written as SQL or column expressions and optimized by Catalyst.
Transformation / actionLazy steps (map, filter, groupBy) vs the call that runs them (collect, count, write).
StageTasks that run without a shuffle; a shuffle (Exchange) starts a new stage.
cache / persistKeep a dataset in executor memory (or memory and disk) for reuse.
LineageThe recipe of transformations; Spark recomputes lost partitions from it.

From our lab (Spark 4.0.4 on our YARN cluster, 3 executors of 512–768 MB, 1 core each)

Same answerSpark RDD (map + reduceByKey) and DataFrame (groupBy) gave exactly the numbers of ItemRevenue.java in Lesson 8.1: chai 50170 orders, ₹752,550 … RDD job 7.4 s, DataFrame job 11.3 s
Start-upspark-submit wall time 46 s: most of it is starting the YARN application and 3 executors, paid once per application
Five passes, no cache19.8, 15.6, 13.5, 13.6, 12.8 s = 75.3 s (each pass re-reads 140 MB from HDFS, but executors stay alive)
Five passes, cached21.4 s, then 4.2, 3.7, 3.3, 2.7 s = 35.3 s
Five MapReduce jobs39, 39, 39, 41, 41 s = 199 s (new ApplicationMaster and containers, HDFS read and write every time)

Spark vs MapReduce at three levels

Pick your level. New to this? Start at Beginner and climb.

Beginner A kitchen that keeps the ingredients out

  • MapReduce is a cook who puts every ingredient back in the cold store after each step, then fetches it again for the next step.
  • Spark keeps what it will need again on the counter (memory), plans the whole recipe before starting, and keeps the same helpers (executors) for the whole meal.
  • Both cook in the same kitchen: the store room is HDFS and the head chef handing out stoves is YARN.

Intermediate The same ideas, new names

  • map/filter are the same functions. reduceByKey is map output + combiner + shuffle + reducer in one call; Spark combines on the map side automatically (the plan shows partial_count).
  • The shuffle is called an Exchange in the plan; hashpartitioning(item, 200) is the HashPartitioner from Lesson 9.2 with 200 partitions.
  • An input split becomes a partition (our 4.7 MB file: 2 partitions); a container runs an executor, which runs many tasks, one after another, without starting a new JVM.
  • The MapReduce ApplicationMaster role is played by Spark's driver (with a small AM on YARN to request executors).

Advanced Where the speed comes from, and where it doesn't

  • No HDFS between steps: a multi-stage job is one application; shuffle files stay on local disk and stage outputs are not written to HDFS with replication 3.
  • Long-lived executors: no JVM and container start-up per task or per job; our 5 MapReduce jobs paid that 5 times.
  • Caching + lineage: reuse data across passes; if an executor dies, only lost partitions are recomputed.
  • Catalyst/Tungsten: read only needed columns (ReadSchema item, amount), aggregate in compact binary form, generate code.
  • Limits: memory is finite (spills to disk), a single one-pass scan of a huge file is disk-bound for both engines, and a badly partitioned shuffle is slow in any engine.

Exam tip: For "Compare Hadoop MapReduce and Spark", say first that Spark is an engine that usually runs on Hadoop (YARN, HDFS), then compare: disk vs memory between steps, one job per step vs one DAG, JVM per task vs long-lived executors, re-run task vs lineage recomputation, Java API vs SQL/Python/Scala/R + streaming + ML. Give one number: 5 passes took 199 s with MapReduce and 35.3 s with cached Spark on the same cluster.

3 Types and ways to use it

MapReduce and Spark side by side

AspectHadoop MapReduceApache Spark
Unit of workOne job = one map + one reduce phaseOne application = a DAG of many stages
Between stepsWrite to HDFS (replication 3), read againMemory or local shuffle files
ProcessesNew JVM per task attempt (YarnChild)Executors live for the whole application
Iterative workSlow: every pass is a job (our 5 passes: 199 s)Fast with cache (35.3 s)
Fault toleranceRe-run the failed task from HDFS inputRecompute lost partitions from lineage
APIsJava (Streaming for other languages)SQL, Python, Scala, Java, R; DataFrames
Beyond batchNoStructured Streaming, MLlib, GraphFrames
Memory needLow; streams through dataHigher; spills to disk when short
Still used forOld jobs, DistCp, HAR, simple huge scansMost new batch, SQL, ML and streaming

Architecture at a glance: one cluster, two engines

Left: a MapReduce job (Lesson 8.6). Right: a Spark application on the same YARN and HDFS. The yellow boxes are what each engine writes to HDFS between steps.

MapReduce job Spark application client: hadoop jar MRAppMaster (one per job) map JVM map JVM map JVM start, run, exit sorted map output on local disk → shuffle reduce JVM job output written to HDFS (×3) next pass = next job: start again, read HDFS again driver: DAG, stages small AM: asks YARN for executors executor 1 executor 2 executor 3 task, task, … task, task, … task, task, … cached part. cached part. cached part. shuffle (Exchange) between executors executors live for the whole application; only the final result goes to HDFS YARN: containers on worker1, worker2, worker3 (for both engines) HDFS: the input blocks (orders_x30.csv, 140 MB) read by both myphatshala.in

Animated example: a fraud model that needs ten passes

Ten passes over UPI transactions 6 steps
Animated example fraud_model · last 30 days of UPI transactions 09:00 · the model needs 10 passes overthe same data (gradient steps).Per-pass costs below are our lab's ratio, scaled up:MapReduce 40 units per pass, cached Spark 3.5. MapReduce write HDFS, read HDFS, new job… 10 × 40 = 400 units 10:00 · each pass is a separate job:start containers, read, compute, write 3 copies. Afternoon · still training.Most time went to reading and writing, not maths. Spark read 43 + 9 × 3.5 ≈ 75 units Next day · same data, Spark on the same YARN:pass 1 reads HDFS and keeps it in executor memory. Passes 2–10 read from memory.Executors stay up; no job start-up per pass. About 5× less time: model ready before lunch.Same HDFS, same YARN. Only the engine changed. myphatshala.in

Spark does not need YARN. It also runs on Kubernetes and on its own standalone manager, usually reading cloud object storage (S3, GCS, ADLS) instead of HDFS. On Hadoop clusters and on EMR, Dataproc and Cloudera it normally runs on YARN, exactly as in our lab.

4 Step by step

The same job three ways, on our cluster

We unpacked spark-4.0.4-bin-hadoop3 in the namenode container and pointed it at our Hadoop configuration. No Spark daemons were started: YARN starts everything per application.

  1. Submit to YARN

    export SPARK_HOME=/tmp/opt/spark-4.0.4-bin-hadoop3 HADOOP_CONF_DIR=/opt/hadoop/etc/hadoop
    $SPARK_HOME/bin/spark-submit --master yarn --num-executors 3 --executor-memory 512m \
      --executor-cores 1 --driver-memory 512m item_revenue_spark.py /user/hadoop/data/canteen_orders.csv
    spark-submit wall time 46 s (includes starting the YARN application and 3 executors)
    --master yarn makes the client ask our ResourceManager for an application, exactly like hadoop jar. The driver runs here (client mode); the AM and 3 executors run in containers on the workers.
  2. The RDD way: MapReduce thinking

    .map(lambda line: line.split(","))
    .filter(lambda f: len(f) == 3 and f[0] != "order_id")
    .map(lambda f: (f[1], (1, int(f[2]))))                  # map: (item, (1, amount))
    .reduceByKey(lambda a, b: (a[0] + b[0], a[1] + b[1]))   # combine + shuffle + reduce
    RDD chai 50170 752550 RDD dosa 49948 2247660 … RDD job took 7.4 s, partitions: 2
    The same mapper and reducer as ItemRevenue.java, in four lines, with the same answers. Two partitions = two input splits of the 4.7 MB file.
  3. The DataFrame way: SQL thinking

    df = spark.read.csv(path, header=True, inferSchema=True)
    df.groupBy("item").agg(F.count("*").alias("orders"), F.sum("amount").alias("revenue")).orderBy("item")
    | chai| 50170| 752550| | dosa| 49948|2247660| DataFrame job took 11.3 s
    You say what, Catalyst decides how. It took longer here because inferSchema reads the file once just to guess types; with a declared schema it reads once.
  4. Read the plan: find MapReduce inside

    Sort [item ASC]
    +- Exchange rangepartitioning(item, 200)
       +- HashAggregate(keys=[item], functions=[count(1), sum(amount)])          ← reduce
          +- Exchange hashpartitioning(item, 200)                               ← shuffle
             +- HashAggregate(keys=[item], functions=[partial_count(1), partial_sum(amount)])   ← combiner
                +- FileScan csv [item, amount] ReadSchema: struct<item:string,amount:int>   ← map + column pruning
    Bottom to top: read only two columns, partially aggregate inside each map task (the combiner of Lesson 9.1), shuffle by hash of item (Lesson 9.2), finish the aggregate, then a second shuffle to sort globally.
  5. Five passes: no cache vs cache

    no cache: 19.8, 15.6, 13.5, 13.6, 12.8 s  = 75.3 s
    cache   : 21.4,  4.2,  3.7,  3.3,  2.7 s  = 35.3 s
    Without cache Spark re-reads HDFS every pass, but executors stay alive, so it is already far faster than starting five jobs. With persist(MEMORY_AND_DISK) pass 1 costs a bit more (it fills the cache); later passes skip HDFS completely.
  6. The MapReduce way: five jobs

    for k in 1 2 3 4 5; do hadoop jar mr.jar ItemRevenue orders_x30.csv /out/iter$k; done
    MapReduce pass 1: 39 s … pass 5: 41 s MapReduce: 5 passes took 199 s
    Each job starts an ApplicationMaster and new map and reduce JVMs, reads 140 MB from HDFS and writes its result with replication 3. Nothing is reused between jobs.

Our numbers come from three workers sharing one laptop. Absolute times will differ on real hardware; the shape will not: the more passes over the same data, the bigger Spark's advantage. For a single pass over a huge file, both engines are limited by disks and the gap shrinks.

5 See it happen

Spark on our YARN cluster

Six steps of our real runs: submit, the RDD job, the plan, five uncached passes, five cached passes, and five MapReduce jobs.

MapReduce vs Spark, same cluster 6 steps
What you see hadoop@namenode : /tmp/mr $ spark-submit --master yarn …queue: root.defaultwall time 46 s (YARN + 3 executors) RDD chai 50170 752550RDD dosa 49948 2247660…RDD job took 7.4 s, partitions: 2 HashAggregate(count, sum) Exchange hashpartitioning(item) HashAggregate(partial_count…) FileScan csv [item, amount] $ … iterative_spark.py nocachepass 1 19.8 s pass 2 15.6 spass 3 13.5 s pass 4 13.6 spass 5 12.8 s5 passes took 75.3 s $ … iterative_spark.py cachepass 1 21.4 s pass 2 4.2 spass 3 3.7 s pass 4 3.3 spass 5 2.7 s5 passes took 35.3 s $ bash iterative_mr.shpass 1 39 s pass 2 39 spass 3 39 s pass 4 41 spass 5 41 s5 passes took 199 s myphatshala.in
Behind the screen driver (namenode) ResourceManager HDFS blocks of the input file executor 1 executor 2 executor 3 YARN gives 1 AM + 3 executor containers;they stay up until the application ends map taskmap taskreduce task2 partitions → 2 tasks, combine, shuffle, reduce Catalyst: read only item and amount,aggregate before the shuffle (like a combiner) every pass reads HDFS again(but the executors are already running) cachedcachedcachedpass 1 fills the cache;passes 2–5 never touch HDFS job 1job 2job 3job 4job 5each job: new AM, new JVMs, read + write HDFS myphatshala.in
6 Real-world use

Spark on Hadoop at work

Versions as of October 2026: Spark 4.2 (July 2026), 4.1 and 4.0 are current lines and 3.5 is the long-term line with security fixes until November 2027. Managed platforms often lag: Amazon EMR 7.14 ships Spark 3.5.8, Google Dataproc 2.3.37 ships Spark 3.5.3 and Cloudera Base 7.3.2 ships Spark 3.5 (sources below).

fraud model10 passes, cached

1. Fraud and risk models in fintech

Payment and lending companies train models on months of transactions with many passes over the same data; MLlib or Spark-prepared features with cached DataFrames replaced chains of MapReduce jobs.

Transactions (HDFS)→Spark ML, cached→Risk scores
nightly ETLMR → Spark SQL

2. Rewriting nightly ETL in telecom

Operators that once ran call-record summaries as dozens of chained MapReduce jobs rewrite them as one Spark SQL application, removing the HDFS writes between steps.

CDR files→one Spark DAG→Billing tables
live

3. Near-real-time dashboards in e-commerce

During a festival sale, Spark Structured Streaming reads orders from Kafka in micro-batches and updates sales per city every minute: something MapReduce was never built for.

Kafka→Structured Streaming→Dashboard
SQL notebookanalysts

4. Interactive analysis in healthcare research

Researchers query de-identified hospital records with Spark SQL from notebooks; queries that took minutes as MapReduce return in seconds when tables are cached and stored as Parquet (Lesson 10.2).

Parquet on HDFS→Spark SQL→Notebook charts
MRSpark

5. Both engines side by side in government data platforms

Large public-sector clusters keep old, stable MapReduce jobs (and tools like DistCp) running while all new work is written in Spark on the same YARN queues; migration happens job by job.

Old MR jobs+New Spark jobs→One YARN cluster

Simplified; organisations differ in tools and policies. Sources: endoflife.date/apache-spark, Amazon EMR 7.x versions, Dataproc 2.3 versions, Cloudera Base 7.3.2.

7 Run the code

A model of k passes, fitted to our runs

The per-pass times below are our measurements. The model predicts any number of passes; check that k = 5 gives back the lab totals (199 s, 75.3 s, 35.3 s). It leaves out Spark's one-time application start-up (about 46 s in our lab), which MapReduce pays inside every pass.

# Per-pass times measured on our lab (5 passes over 9 M orders, 140 MB)
MR_PASS = 199 / 5                     # every pass is a new MapReduce job: start + read + write
NOCACHE_FIRST, NOCACHE_NEXT = 19.8, (15.6 + 13.5 + 13.6 + 12.8) / 4
CACHE_FIRST, CACHE_NEXT = 21.4, (4.2 + 3.7 + 3.3 + 2.7) / 4

def mapreduce(k):
    return k * MR_PASS

def spark(k, cache):
    if cache:
        return CACHE_FIRST + (k - 1) * CACHE_NEXT
    return NOCACHE_FIRST + (k - 1) * NOCACHE_NEXT

for k in [1, 5, 10, 20]:
    mr, nc, c = mapreduce(k), spark(k, False), spark(k, True)
    print(f"{k:>2} passes: MapReduce {mr:6.1f} s | Spark {nc:6.1f} s | Spark+cache {c:5.1f} s | {mr / c:4.1f}x")
1 passes: MapReduce 39.8 s | Spark 19.8 s | Spark+cache 21.4 s | 1.9x 5 passes: MapReduce 199.0 s | Spark 75.3 s | Spark+cache 35.3 s | 5.6x 10 passes: MapReduce 398.0 s | Spark 144.7 s | Spark+cache 52.7 s | 7.6x 20 passes: MapReduce 796.0 s | Spark 283.4 s | Spark+cache 87.4 s | 9.1x
  • MR_PASS = 199 / 5 # every pass is a new MapReduce job: start + read + writeFive ItemRevenue jobs took 39, 39, 39, 41 and 41 s: about 40 s each, every time.
  • CACHE_FIRST, CACHE_NEXT = 21.4, (4.2 + 3.7 + 3.3 + 2.7) / 4Pass 1 pays for reading and filling the cache; the next passes cost about 3.5 s.
  • return CACHE_FIRST + (k - 1) * CACHE_NEXTThe cached line grows slowly, so the advantage grows with every extra pass.

Run it on your PC: Spark on the Module 8 cluster

Spark needs no daemons of its own here: unpack it on the namenode container, point HADOOP_CONF_DIR at the Hadoop configuration, and spark-submit --master yarn asks our ResourceManager for an ApplicationMaster and executors, just like hadoop jar. Executors of 512–768 MB fit our 2 GB NodeManagers. The outputs below are real; Spark's INFO/WARN log lines and the YARN status report are filtered out.

Item revenue: RDD and DataFrame

# item_revenue_spark.py: the same job as ItemRevenue.java (Lesson 8.1), in PySpark, twice:
# with the RDD API (map / reduceByKey, MapReduce thinking) and with DataFrames (SQL thinking).
import sys, time
from pyspark.sql import SparkSession, functions as F
spark = SparkSession.builder.appName("item revenue (spark)").getOrCreate()
path = sys.argv[1]

t = time.time()
rdd = (spark.sparkContext.textFile(path)
       .map(lambda line: line.split(","))
       .filter(lambda f: len(f) == 3 and f[0] != "order_id")
       .map(lambda f: (f[1], (1, int(f[2]))))                       # map: (item, (1, amount))
       .reduceByKey(lambda a, b: (a[0] + b[0], a[1] + b[1])))       # shuffle + reduce (with map-side combine)
for item, (n, total) in sorted(rdd.collect()):
    print(f"RDD  {item:9} {n:>8} {total:>10}")
print(f"RDD job took {time.time() - t:.1f} s, partitions: {rdd.getNumPartitions()}")

t = time.time()
df = spark.read.csv(path, header=True, inferSchema=True)
out = df.groupBy("item").agg(F.count("*").alias("orders"), F.sum("amount").alias("revenue")).orderBy("item")
out.show()
print(f"DataFrame job took {time.time() - t:.1f} s")
out.explain()
spark.stop()

# One-time setup on the namenode container of the Module 8 lab (no Spark daemons needed):
#   curl -O https://dlcdn.apache.org/spark/spark-4.0.4/spark-4.0.4-bin-hadoop3.tgz
#   mkdir -p /tmp/opt && tar xzf spark-4.0.4-bin-hadoop3.tgz -C /tmp/opt
#   export SPARK_HOME=/tmp/opt/spark-4.0.4-bin-hadoop3 HADOOP_CONF_DIR=/opt/hadoop/etc/hadoop
#   $SPARK_HOME/bin/spark-submit --master yarn --num-executors 3 --executor-memory 512m \
#       --executor-cores 1 --driver-memory 512m item_revenue_spark.py /user/hadoop/data/canteen_orders.csv
RDD chai 50170 752550 RDD dosa 49948 2247660 RDD idli 49984 1499520 RDD poha 49891 1247275 RDD samosa 50021 900378 RDD vada pav 49986 999720 RDD job took 7.4 s, partitions: 2 +--------+------+-------+ | item|orders|revenue| +--------+------+-------+ | chai| 50170| 752550| | dosa| 49948|2247660| | idli| 49984|1499520| | poha| 49891|1247275| | samosa| 50021| 900378| |vada pav| 49986| 999720| +--------+------+-------+ DataFrame job took 11.3 s == Physical Plan == AdaptiveSparkPlan isFinalPlan=false +- Sort [item#18 ASC NULLS FIRST], true, 0 +- Exchange rangepartitioning(item#18 ASC NULLS FIRST, 200), ENSURE_REQUIREMENTS, [plan_id=90] +- HashAggregate(keys=[item#18], functions=[count(1), sum(amount#19)]) +- Exchange hashpartitioning(item#18, 200), ENSURE_REQUIREMENTS, [plan_id=87] +- HashAggregate(keys=[item#18], functions=[partial_count(1), partial_sum(amount#19)]) +- FileScan csv [item#18,amount#19] Batched: false, DataFilters: [], Format: CSV, Location: InMemoryFileIndex(1 paths)[hdfs://namenode/user/hadoop/data/canteen_orders.csv], PartitionFilters: [], PushedFilters: [], ReadSchema: struct<item:string,amount:int> spark-submit wall time 46 s (includes starting the YARN application and 3 executors)

Same numbers as ItemRevenue.java (Lesson 8.1). The DataFrame is slower here because inferSchema reads the file once more to guess the column types.

Five passes, without and with cache

# iterative_spark.py: five passes over the same 140 MB (9 million orders).
# Pass k counts orders per item with amount >= threshold k. With cache(), only pass 1 reads HDFS.
import sys, time
from pyspark import StorageLevel
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("five passes (spark)").getOrCreate()
cache = sys.argv[2] == "cache"
orders = (spark.sparkContext.textFile(sys.argv[1])
          .map(lambda l: l.split(","))
          .filter(lambda f: f[0] != "order_id")
          .map(lambda f: (f[1], int(f[2]))))
if cache:
    orders = orders.persist(StorageLevel.MEMORY_AND_DISK)
total = time.time()
for k, threshold in enumerate([0, 18, 20, 25, 30], 1):
    t = time.time()
    counts = orders.filter(lambda p, th=threshold: p[1] >= th).map(lambda p: (p[0], 1)).reduceByKey(lambda a, b: a + b).collect()
    print(f"pass {k}: amount >= {threshold:2}: {sum(c for _, c in counts):>9,} orders, {time.time() - t:5.1f} s")
print(f"cache={cache}: 5 passes took {time.time() - total:.1f} s")
spark.stop()

# One-time setup on the namenode container of the Module 8 lab (no Spark daemons needed):
#   curl -O https://dlcdn.apache.org/spark/spark-4.0.4/spark-4.0.4-bin-hadoop3.tgz
#   mkdir -p /tmp/opt && tar xzf spark-4.0.4-bin-hadoop3.tgz -C /tmp/opt
#   export SPARK_HOME=/tmp/opt/spark-4.0.4-bin-hadoop3 HADOOP_CONF_DIR=/opt/hadoop/etc/hadoop
#   for c in nocache cache; do
#     $SPARK_HOME/bin/spark-submit --master yarn --num-executors 3 --executor-memory 768m \
#       --executor-cores 1 --driver-memory 512m iterative_spark.py /user/hadoop/data/orders_x30.csv $c
#   done
pass 1: amount >= 0: 9,000,000 orders, 19.8 s pass 2: amount >= 18: 7,494,900 orders, 15.6 s pass 3: amount >= 20: 5,994,270 orders, 13.5 s pass 4: amount >= 25: 4,494,690 orders, 13.6 s pass 5: amount >= 30: 2,997,960 orders, 12.8 s cache=False: 5 passes took 75.3 s pass 1: amount >= 0: 9,000,000 orders, 21.4 s pass 2: amount >= 18: 7,494,900 orders, 4.2 s pass 3: amount >= 20: 5,994,270 orders, 3.7 s pass 4: amount >= 25: 4,494,690 orders, 3.3 s pass 5: amount >= 30: 2,997,960 orders, 2.7 s cache=True: 5 passes took 35.3 s

orders_x30.csv is the 140 MB, 9 million order file from Lesson 8.1. Timings vary from run to run; the pattern does not.

The same five passes as MapReduce jobs

# iterative_mr.sh: the MapReduce way - every pass is a new job that reads HDFS again
cd /tmp/mr; total=$(date +%s)
for k in 1 2 3 4 5; do
  s=$(date +%s); hdfs dfs -rm -r -f -skipTrash /out/iter$k > /dev/null 2>&1
  hadoop jar mr.jar ItemRevenue /user/hadoop/data/orders_x30.csv /out/iter$k > /dev/null 2>&1
  echo "MapReduce pass $k: $(( $(date +%s) - s )) s"
done
echo "MapReduce: 5 passes took $(( $(date +%s) - total )) s"

# Run inside the namenode container:  bash iterative_mr.sh
MapReduce pass 1: 39 s MapReduce pass 2: 39 s MapReduce pass 3: 39 s MapReduce pass 4: 41 s MapReduce pass 5: 41 s MapReduce: 5 passes took 199 s

Each pass runs ItemRevenue over the whole file as a new job: new ApplicationMaster, new map and reduce JVMs, a full HDFS read and an HDFS write.

8 Line by line

Lazy transformations, one action

Spark records transformations and runs them only when an action asks for a result. Watch the plan grow without any data moving, then the action run it all at once.

Worked examples: real problems, solved and traced in memory

Four decisions engineers face when choosing between MapReduce and Spark, worked with our measured numbers.

Example 1BeginnerFintech · iterations

Ten passes for a fraud model

Problem statement

A UPI fraud team needs 10 passes over the same data. Using our lab's per-pass times, how long would MapReduce and cached Spark take, and how does Spark's one-time 46 s start-up change the answer?

Input
MR 39.8 s/pass · cache 21.4 s + 3.475 s/pass · start-up 46 s
Expected output
MapReduce 398 s, Spark+cache 52.7 s, with start-up 98.7 s (4.0x)

Approach

  1. MapReduce: passes × 39.8 s.
  2. Spark: first pass + 9 cheap passes.
  3. Add Spark's start-up once and compare again.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

Example 2IntermediateTelecom · memory

Will the data fit in the cache?

Problem statement

A telecom company wants to cache 2 TB of call records (CSV) for a 10-pass analysis. It has 100 executors of 16 GB. Spark's unified memory is (heap − 300 MB) × 0.6, shared by cache and execution. Stored as a columnar cache, the data is roughly the size of Parquet (our Lesson 10.2 ratio: 4.4 MB Parquet for 19.4 MB CSV). Does it fit?

Input
2 TB CSV · 100 × 16 GB executors · ratio 4.4/19.4
Expected output
memory 942 GB, data cached ~464 GB, fits: True

Approach

  1. Usable memory per executor = (16,384 − 300) × 0.6 MB.
  2. Total = per executor × 100.
  3. Cached size ≈ 2 TB × Parquet ratio.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

Example 3IntermediateE-commerce · choice

Which engine for which job?

Problem statement

An e-commerce platform has three workloads: (a) a nightly one-pass copy of 50 TB of logs into an archive on a memory-poor old cluster, (b) a sales dashboard refreshed every minute from Kafka, (c) a recommendation model with 30 passes. Pick an engine for each.

Input
3 workloads
Expected output
a: one-pass 50 TB copy       -> MapReduce/DistCp is fine
b: dashboard every minute    -> Spark Structured Streaming
c: 30-pass model             -> Spark + cache

Approach

  1. One pass, no reuse, little memory: MapReduce (or DistCp) is fine.
  2. Streaming: Spark Structured Streaming.
  3. Many passes: Spark with cache.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

Example 4AdvancedInsurance · migration

Is rewriting 400 MapReduce jobs worth it?

Problem statement

An insurer runs 400 MapReduce jobs nightly, using 6,000 container-hours a night. Rewriting a job in Spark SQL takes 2 developer-days (₹8,000 per day). Assume the rewritten jobs use 55% fewer container-hours, and a container-hour costs ₹4 (illustrative). How long until the rewrite pays back?

Input
400 jobs · 2 days × ₹8,000 · 6,000 container-hours/night · −55% · ₹4/hour
Expected output
rewrite Rs 6,400,000, saves Rs 13,200 a night, payback 485 nights

Approach

  1. Cost = jobs × days × daily rate.
  2. Saving per night = 6,000 × 0.55 × ₹4.
  3. Payback = cost ÷ saving per night.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

9 Quiz and revision

Check yourself

Quick revision

Lesson 13.2 in one screen

  • Spark is a DAG engine with lazy transformations, actions, long-lived executors, a driver, caching and lineage; Catalyst and Tungsten optimize DataFrames and SQL.
  • Spark usually runs on Hadoop: YARN containers for executors, HDFS for data. It replaced MapReduce, not HDFS or YARN.
  • Mapping: map/filter = map, reduceByKey = combiner + shuffle + reduce, Exchange = shuffle, partition = split, executor = container.
  • Lab: same answers as Lesson 8.1; RDD 7.4 s, DataFrame 11.3 s; spark-submit 46 s including start-up.
  • Lab, 5 passes over 9 M orders: MapReduce 199 s; Spark 75.3 s; Spark + cache 35.3 s (passes 2–5: 2.7–4.2 s).
  • Spark wins most on iterative, interactive and streaming work; a single huge scan is disk-bound for both.
  • Oct 2026: Spark 4.2/4.1/4.0 current, 3.5 LTS to Nov 2027; EMR 7.14, Dataproc 2.3 and Cloudera 7.3.2 ship Spark 3.5.

Interview and viva questions

Is Spark a replacement for Hadoop?

Spark replaces Hadoop MapReduce as the processing engine, not Hadoop as a whole. Spark has no storage or resource manager of its own, so on most Hadoop clusters it runs on YARN and reads HDFS. It can also run on Kubernetes or its standalone manager with cloud object storage, which is how many new platforms use it.

Why is Spark faster than MapReduce?

Spark plans a whole multi-step job as one DAG, keeps intermediate data in memory or on local disk instead of writing it to HDFS between steps, reuses long-lived executors instead of starting a JVM per task, can cache datasets across passes, and optimizes DataFrame/SQL plans with Catalyst and Tungsten. In our lab, five passes took 199 s with MapReduce and 35.3 s with cached Spark.

Is MapReduce still used?

Yes, but mostly for existing jobs and tools built on it, such as DistCp and Hadoop Archive. New batch, SQL, machine learning and streaming work is usually written in Spark (or Hive on Tez, Flink, Trino), often running on the same YARN cluster as the old MapReduce jobs.

What is the difference between an RDD and a DataFrame?

An RDD is a distributed collection of objects transformed with functions you write; Spark cannot see inside those functions. A DataFrame is a table with a schema, written with SQL or column expressions, which the Catalyst optimizer can rewrite (for example reading only needed columns and aggregating before the shuffle). DataFrames are the recommended API for most work.

Does Spark need more memory than MapReduce?

Usually yes. Executors are long-lived and use memory for caching and for shuffles and joins; when memory runs short Spark spills to disk, and badly sized executors can fail with out-of-memory errors. MapReduce streams data through short-lived tasks and needs less memory, at the cost of disk I/O between steps.

Practice problems

Exercises using the real MapReduce and Spark numbers from our cluster.

P1Easy

Speed-ups

Compute how many times faster cached Spark was than MapReduce, and than uncached Spark, for 5 passes.

Input
199 s · 75.3 s · 35.3 s
Expected output
vs MapReduce 5.6x, vs uncached Spark 2.1x
Show solution and explanation
print(f"vs MapReduce {199 / 35.3:.1f}x, vs uncached Spark {75.3 / 35.3:.1f}x")
vs MapReduce 5.6x, vs uncached Spark 2.1x

How it works: About 5.6× and 2.1×. Part of the first gap is the HDFS write and job start-up in every MapReduce pass, the rest is caching.

P2Easy

Cost of the first pass

What share of the cached run's 35.3 s was spent in pass 1?

Input
21.4 s of 35.3 s
Expected output
61%
Show solution and explanation
print(f"{21.4 / 35.3:.0%}")
61%

How it works: About 61%: reading HDFS and filling the cache dominate a short run.

P3Easy

Same answer?

Check that Spark's chai revenue equals the MapReduce result from Lesson 8.1 and that the price works out.

Input
50170 orders, 752550
Expected output
True
Show solution and explanation
print(752550 == 50170 * 15)
True

How it works: True: every chai costs ₹15 in this dataset, and both engines agree.

P4Medium

When does caching pay?

The cached run's first pass costs 1.6 s more than the uncached one, but every later pass saves about 10.4 s. From how many passes is caching worth it?

Input
21.4 vs 19.8 · 3.475 vs 13.875
Expected output
1 passes: cache 21.4 no cache 19.8
2 passes: cache 24.9 no cache 33.7
3 passes: cache 28.3 no cache 47.5
Show solution and explanation
for k in range(1, 4):
    print(k, "passes: cache", round(21.4 + (k - 1) * 3.475, 1), "no cache", round(19.8 + (k - 1) * 13.875, 1))
1 passes: cache 21.4 no cache 19.8 2 passes: cache 24.9 no cache 33.7 3 passes: cache 28.3 no cache 47.5

How it works: From 2 passes on. With a single pass, caching only costs time.

P5Medium

Word count, Spark style

Simulate flatMap → map → reduceByKey on three lines.

Input
3 lines of feedback
Expected output
[('chai', 2), ('cold', 1), ('dosa', 1), ('hot', 1), ('late', 1), ('was', 3)]
Show solution and explanation
lines = ["chai was hot", "dosa was cold", "chai was late"]
words = [w for line in lines for w in line.split()]       # flatMap
pairs = [(w, 1) for w in words]                           # map
counts = {}
for w, n in pairs:                                        # reduceByKey
    counts[w] = counts.get(w, 0) + n
print(sorted(counts.items()))
[('chai', 2), ('cold', 1), ('dosa', 1), ('hot', 1), ('late', 1), ('was', 3)]

How it works: The same three steps as Lesson 8.6's WordCount.java, written as one chain in Spark: lines.flatMap(split).map(lambda w: (w, 1)).reduceByKey(add).

P6Medium

Partitions from blocks

textFile makes about one partition per HDFS block. How many partitions do 140 MB and 4.7 MB files get with 128 MB blocks?

Input
140,759,640 and 4,692,009 bytes
Expected output
140759640 -> 2 partitions
4692009 -> 2 partitions
Show solution and explanation
import math
for size in [140_759_640, 4_692_009]:
    print(size, "->", max(2, math.ceil(size / (128 * 1024 * 1024))), "partitions")
140759640 -> 2 partitions 4692009 -> 2 partitions

How it works: Both get 2 (textFile asks for at least 2 partitions by default), matching "partitions: 2" in our RDD run.

P7Business problem

Cluster time saved per day

A bank runs 120 iterative jobs a day, 10 passes each. Using our per-pass numbers, how many cluster-hours a day does cached Spark save over MapReduce?

Input
120 jobs × 10 passes
Expected output
10.0 hours of job time saved per day
Show solution and explanation
mr = 10 * 39.8
spark = 21.4 + 9 * 3.475 + 46
print(f"{120 * (mr - spark) / 3600:.1f} hours of job time saved per day")
10.0 hours of job time saved per day

How it works: About 10 hours of job time a day on our lab's scale (illustrative); on real clusters, multiply by the containers each job holds.

P8Advanced

Recompute from lineage

Three cached partitions; executor 2 dies and loses partition 1. Recompute only that partition from its lineage and check the total is unchanged.

Input
3 partitions, lineage = filter ≥ 18 then double
Expected output
recomputed 1 partition; total same: True
Show solution and explanation
source = [[15, 18, 20], [25, 30, 45], [18, 80, 15]]
lineage = lambda part: [x * 2 for x in part if x >= 18]
cache = [lineage(p) for p in source]
before = sum(map(sum, cache))
cache[1] = None                              # executor lost
recomputed = 0
for i, p in enumerate(cache):
    if p is None:
        cache[i] = lineage(source[i]); recomputed += 1
print("recomputed", recomputed, "partition; total same:", sum(map(sum, cache)) == before)
recomputed 1 partition; total same: True

How it works: Only the lost partition is rebuilt, from its source block and its recipe. MapReduce instead re-runs the failed task from its HDFS input.

Real-world scenario questions (asked in company interviews)

Hadoop vs Spark is asked in almost every big data interview, often as a trap: the best answers start by saying Spark usually runs on Hadoop.

Concepts · basicsFresher

"Hadoop or Spark: which is better?"

An opening question in a data engineering interview.

The interviewer is checking: that you know they are not alternatives.

Show a strong answer
  • Hadoop is storage (HDFS) + resources (YARN) + an engine (MapReduce); Spark is an engine, commonly running on YARN and HDFS.
  • As engines, Spark is faster for multi-step, iterative and interactive work: memory between steps, long-lived executors, Catalyst.
  • Example: on one cluster, five passes took 199 s as MapReduce jobs and 35 s with cached Spark.
Internals · plansExperienced

"Explain this Spark plan in MapReduce terms"

The interviewer shows FileScan → HashAggregate(partial) → Exchange → HashAggregate.

The interviewer is checking: real understanding of shuffles.

Show a strong answer
  • FileScan with a narrow ReadSchema is the map input with column pruning.
  • The partial aggregate is a combiner inside each map task; Exchange hashpartitioning is the shuffle with a hash partitioner.
  • The final aggregate is the reducer; a stage boundary sits at each Exchange.
Operations · memoryExperienced

A Spark job that was fast in testing fails with executor OOM in production

The team cached a large DataFrame and joins it with another.

The interviewer is checking: memory sizing and options.

Show a strong answer
  • Check executor memory vs cached size (Storage tab in the Spark UI), skewed keys, and shuffle partition counts.
  • Use MEMORY_AND_DISK, cache only what is reused, cache columnar DataFrames rather than object RDDs, raise memory or partitions.
  • Fix skew (salting, AQE skew join) and broadcast small tables (Lesson 9.6).
Architecture · migrationLead

"Should we rewrite all our MapReduce jobs in Spark?"

A company has 400 stable MapReduce jobs on a Hadoop cluster.

The interviewer is checking: pragmatic migration thinking.

Show a strong answer
  • Not all at once: rank jobs by cluster-hours and business value; rewrite the heaviest and the multi-step chains first.
  • Run old and new side by side on the same YARN queues and compare outputs row by row before switching.
  • Keep jobs that are one-pass, stable and cheap; write all new work in Spark SQL/DataFrames.