◐ ai-teacher

🔥 Spark & Databricks · Chapter 2

Spark's execution model: driver, executors, stages

Your cluster has 40 cores and your stage has 4 partitions. Thirty-six cores are being paid for to do nothing.

Study this lesson free in the app →

The basics

Spark's execution model is the division of labour inside a Spark cluster: one driver plans the work, many executors do it, and the unit of work they hand around is a task.

Why it exists: one machine cannot hold or scan a terabyte of rides. Spark splits the data into partitions and runs the same code on each partition in parallel, on different machines. Somebody has to decide who processes what, and somebody has to actually process it — those are the driver and the executors.

Mental model: a restaurant. The head chef (driver) reads the order, writes the plan, and hands out tickets. Line cooks (executors) each have a few burners (cores). One ticket (task) = one dish from one tray of ingredients (partition). The number of dishes cooking at once is the number of burners, not the number of cooks.

Words you must know 5

Driver
the process that runs your program, builds the plan and schedules tasks
Executor
a JVM process on a worker node that runs tasks and caches data
Core / slot
one task can run per core at a time
Task
one partition processed by one stage
Stage
a set of tasks that run the same code with no shuffle between them

Explained

Meghalaya Cabs runs its nightly fare rollup on a 10-node cluster. The Spark UI shows a stage with 4 tasks — because the input was 4 big gzip files, and gzip cannot be split.

Four cores busy. Thirty-six idle. The job takes 50 minutes and the bill counts all 40 cores.

Nothing was wrong with the code. Somebody did not know how work maps to cores.

The wedding kitchen

A head chef gets an order for 2,000 plates. She does not cook. She reads the menu, breaks it into steps, and writes tickets: one ticket per tray of ingredients.

She has 10 line cooks, each with 4 burners. Forty dishes can cook at once. If she writes 400 tickets, the kitchen works through them in 10 rounds.

If she writes only 4 tickets — four enormous trays — 36 burners stay cold, however many cooks she hires.

And if one cook's station catches fire, she rewrites his tickets for someone else. If the head chef walks out, the whole order is lost.

One action, end to end

The driver is the only process that knows the whole plan. Executors only ever see one task at a time per core.

Who does what

Driver — runs main() / your notebook, holds the SparkSession, builds the DAG, splits it into stages, schedules tasks, tracks which executor holds which shuffle output, and receives anything you collect().

Executors — long-lived JVMs started for your application. Each has spark.executor.cores cores and spark.executor.memory of heap. They run tasks, write shuffle files to local disk, and hold cached partitions.

Cluster manager — allocates containers. It knows nothing about DataFrames.

Task — the smallest unit: one stage's code applied to one partition. A task runs on one core.

==Parallelism = min(partitions, executors × cores).== Too few partitions wastes cores; too many tiny ones wastes time on scheduling overhead (each task costs a few ms to launch).

Inside one executor (spark.executor.memory = 16g)

 container  ≈ 16g heap + 1.6g overhead (max(384MB, 10%))
 ┌──────────────────────────────────────────────┐
 │ reserved            300 MB                   │
 ├──────────────────────────────────────────────┤
 │ unified  = 0.6 × (16384 − 300) ≈ 9.6 GB      │
 │  ┌─────────────────────┬──────────────────┐  │
 │  │ storage (cache)     │ execution        │  │
 │  │ ~50% protected      │ joins, sorts,    │  │
 │  │ storageFraction=0.5 │ aggregations     │  │
 │  └─────────────────────┴──────────────────┘  │
 │      boundary moves: execution can evict     │
 │      cache above the protected half          │
 ├──────────────────────────────────────────────┤
 │ user memory  0.4 × 16084 ≈ 6.4 GB            │
 │ (your objects, UDF data structures)          │
 └──────────────────────────────────────────────┘
 4 cores → ~2.4 GB of unified memory per running task

Sizing the Meghalaya Cabs cluster

spark = (SparkSession.builder
  .appName('mc_fare_rollup')
  .config('spark.executor.instances', 10)
  .config('spark.executor.cores', 4)        # 4 tasks at once per executor
  .config('spark.executor.memory', '16g')   # JVM heap
  .config('spark.executor.memoryOverhead', '2g')  # off-heap, Python workers
  .config('spark.driver.memory', '8g')
  .getOrCreate())

rides = spark.read.parquet('s3://mc-lake/silver/rides/')
print(rides.rdd.getNumPartitions())   # 320 → 320 tasks in the scan stage
# 10 executors × 4 cores = 40 slots → 320 / 40 = 8 waves

print(spark.sparkContext.defaultParallelism)  # usually = total cores = 40

Rule of thumb most teams use: 4–5 cores per executor. One core per executor wastes JVM overhead; 16 cores per executor gives each task a thin slice of memory and garbage-collection pauses that stall all 16.

What changes at 100×

Waves matter more than cores. 4,001 tasks on 4,000 slots = 2 waves, the second with one task. Aim for task counts a little under a multiple of slots, or enough tasks (3–4× slots) that the tail is small.

The driver becomes the bottleneck with huge plans or many files: listing 2 million small files, broadcasting a 1GB table, or collect()ing results all happen on one JVM. Raise driver memory or, better, stop sending it data.

Dynamic allocation adds and removes executors with load — good for shared clusters, but executors that hold shuffle files can only be removed safely with an external shuffle service or shuffle tracking.

Executor loss is routine at scale. Spot instances get reclaimed; Spark recomputes the lost partitions from lineage. Driver loss is not routine — it ends the application.

Gotchas

Words to know

DriverThe process running your program; builds the plan, schedules tasks and receives collected results.
ExecutorA JVM process on a worker node that runs tasks, writes shuffle files and caches data for one application.
TaskOne stage's code applied to one partition, run on one core.
SlotCapacity to run one task: total slots = executors × spark.executor.cores.
WaveOne round of tasks filling all slots; a stage needs ceil(tasks ÷ slots) waves.
Unified memoryspark.memory.fraction (0.6) of heap minus 300MB, shared between execution and storage.
Memory overheadOff-heap container memory for JVM internals and Python workers; default max(384MB, 10% of executor memory).

In two minutes

Test yourself

1. Which process builds the physical plan and splits it into stages?

  1. The cluster manager
  2. Each executor, independently
  3. The driver
  4. The worker node's OS scheduler
Show the answer

The driver — Only the driver sees the whole program. The cluster manager hands out containers and executors just run the tasks they are given.

2. A stage has 320 partitions. The cluster has 10 executors with 4 cores each. How many waves?

  1. 8
  2. 32
  3. 4
  4. 1
Show the answer

8 — Slots = 10 × 4 = 40. 320 ÷ 40 = 8 waves. 32 would be the partitions per executor, not waves.

3. What is one task?

  1. One action
  2. One partition processed by one stage
  3. One executor's lifetime
  4. One row
Show the answer

One partition processed by one stage — An action is a job; a job has stages; a stage has one task per partition.

4. Meghalaya Cabs reads 4 gzip CSV files of 5GB each on a 40-core cluster. The scan stage is slow and the UI shows 4 tasks. Best fix?

  1. Add 20 more executors
  2. Increase spark.executor.memory
  3. Set spark.sql.shuffle.partitions = 400
  4. Store the files splittable (Parquet, or many smaller files), or repartition right after reading
Show the answer

Store the files splittable (Parquet, or many smaller files), or repartition right after reading — gzip is not splittable, so each file is one partition and one task. More executors stay idle. shuffle.partitions only affects stages after a shuffle, not the scan.

5. A job calls rides.toPandas() on 200M rows and fails with OutOfMemoryError. Which process ran out?

  1. The driver
  2. Every executor
  3. The cluster manager
  4. The shuffle service
Show the answer

The driver — toPandas() and collect() pull every row into the driver's memory. Executors were fine — they only had to send their partitions.

6. Executors have spark.executor.memory = 16g and 4 cores. Roughly how much unified (execution + storage) memory can one running task count on?

  1. 16 GB
  2. About 2.4 GB
  3. About 4 GB
  4. 300 MB
Show the answer

About 2.4 GB — Unified = 0.6 × (16384 − 300 MB) ≈ 9.6 GB, shared by 4 concurrent tasks ≈ 2.4 GB each. 4 GB is the naive 16 ÷ 4 that forgets reserved and user memory.

There are 9 questions, 3 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →