What triggers a shuffle, how shuffle read/write works, where it spills, and the strategies that reduce or avoid it.
⏱ 10 min readTopics chapter readerLevel · Performance
01 · Orientation
Start here
⏱ 5 min · Topic 1 of 9
In the chapter "Transformations, Actions & Lazy Evaluation", a wide step needed a shuffle. A shuffle moves rows between partitions, so rows that belong together end up in one place.
A shuffle is often the most costly step in a Spark job. In an interview, you are often asked to find it in a slow job and make it smaller.
This chapter has four parts.
How a shuffle works, step by step, on the shop data.
Which steps cause a shuffle, and why a shuffle is slow.
How many pieces a shuffle makes, and the setting for it.
How to make a shuffle smaller, and how to read its numbers in the Spark UI.
The job is total revenue for each customer, over all orders. We do not filter by status this time. That makes customer 3 appear in two partitions, so the shuffle has real work to do.
02 · What happens
How a shuffle works
⏱ 10 min · Topic 2 of 9
We use the model from the architecture chapter: 3 partitions of 2 orders. Each partition has one task. Look at the job and the three partitions.
Step 1: each task adds up its own rowsworked example
Six order rows became five partial rows. Task 2 has only order 105 for customer 3, and its amount is NULL, so its partial sum is NULL.
Customer 3 is now in Task 1 and in Task 2. To get one total for customer 3, those two rows must meet in one place.
Spark chooses the place with a hash. A hash turns the customer_id into a number. Spark divides that number by N, the number of new partitions, and keeps the remainder.
Real Spark uses its own hash. We use a simple model instead: customer_id % 3. The symbol % gives the remainder after dividing by 3.
customer_id
customer_id % 3
Goes to new partition
1
1
new partition 1
2
2
new partition 2
3
0
new partition 0
4
1
new partition 1
Now comes the shuffle write.
Each stage 0 task sorts its partial rows by new partition. It writes them to one file on the local disk of its machine.
A small index file says where each part starts. Look at the parts.
Stage 0 task
Part for new partition 0
Part for new partition 1
Part for new partition 2
Task 0
nothing
(1, 65.00)
nothing
Task 1
(3, 15.50)
nothing
(2, 60.00)
Task 2
(3, NULL)
(4, 80.00)
nothing
Stage 0 is now finished, and stage 1 starts.
This is the shuffle read. Each stage 1 task fetches its part from every stage 0 file, over the network.
Step 2: each new task reads its files and adds upworked example
(3, 15.50) from stage 0 Task 1, (3, NULL) from stage 0 Task 2
customer 3: 15.50
New task 1
(1, 65.00) from stage 0 Task 0, (4, 80.00) from stage 0 Task 2
customer 1: 65.00, customer 4: 80.00
New task 2
(2, 60.00) from stage 0 Task 1
customer 2: 60.00
sum skips NULL. So the sum of 15.50 and NULL is 15.50 for customer 3. (A plain 15.50 + NULL would give NULL.) Order 103 is refunded, but there is no filter, so customer 2 gets 60.00.
show() prints 1: 65.00, 2: 60.00, 3: 15.50, 4: 80.00. The column amount is DECIMAL(10,2), as in the DataFrames chapter, so Spark prints two decimals. The row order can change from run to run. Customer 5, in the customers table, has no orders, so that customer is not there.
Every row for one customer reached one task. That was the goal of the shuffle.
Watch out
The % 3 rule is only a model. Real Spark uses a different hash, and the number of partitions is usually not 3. Your real partitions will not match this table.
03 · Find it
What causes a shuffle
⏱ 6 min · Topic 3 of 9
A shuffle happens when rows from many partitions must meet. The groupBy above is one case. Here are the common ones, with the reason for each.
Step
Shop example
Why rows must move
groupBy with an aggregate
groupBy("customer_id").sum("amount")
Rows of one customer sit in many partitions.
join (in most cases)
orders.join(customers, "customer_id")
Rows with the same customer_id must meet.
distinct
orders.select("customer_id").distinct()
Equal rows must meet to remove copies.
orderBy
orders.orderBy("amount")
Spark must put rows in order across all partitions.
repartition
orders.repartition(8)
You ask Spark to spread rows again.
window function with partitionBy (a later chapter)
The lower HashAggregate is step 1 above. The Exchange is the shuffle. The upper HashAggregate is step 2.
Count the Exchange lines. Two groupBy steps on different keys give two Exchanges, so two shuffles.
04 · The cost
Why a shuffle is slow
⏱ 5 min · Topic 4 of 9
A narrow step works in memory, inside one task. A shuffle uses the disk and the network as well. There are four costs.
Serialize and write: Spark turns each row into bytes (this is serialization) and saves them to local disk.
Network: the new tasks fetch those files from other machines. The network is slower than memory.
Read and rebuild: each new task reads the bytes and turns them back into rows.
A stage boundary: stage 1 cannot start until every task of stage 0 has finished writing.
Spark keeps the files on disk for a reason. If a stage 1 task fails, it can fetch the files again. The stage 0 tasks do not need to run again.
Sometimes there is a fifth cost.
If a task gets more data than fits in memory, Spark writes the extra to disk while it works. This is called a spill. A spill adds more disk work.
Watch out
Shuffle files fill the local disk of each machine. A job with a very large shuffle can fail with "No space left on device".
05 · One setting
How many pieces after
⏱ 5 min · Topic 5 of 9
The setting spark.sql.shuffle.partitions says how many partitions a shuffle makes. The default is 200. In our model we used 3, so we could draw it.
Without help, 200 partitions would give our 5 partial rows 200 tiny tasks. Most would get no rows at all. That is a waste.
Spark has a feature for this, called AQE (Adaptive Query Execution). It is on by default since Spark 3.2. After the shuffle, AQE can see how small the partitions are, and it can merge small ones into fewer tasks.
You can still set the number yourself. Look at one example.
Change the number of shuffle partitionsworked example
Too many partitions gives many tiny tasks. Too few gives big tasks that can spill. The right number depends on how much data you shuffle.
The chapter "Partitioning & Parallelism" explains how to choose the number. The chapter "Data Skew & Salting" explains what happens when one customer has most of the rows.
06 · Spend less
Make the shuffle smaller
⏱ 6 min · Topic 6 of 9
You often cannot remove a shuffle. You can often make it smaller. A smaller shuffle writes less to disk and sends less over the network.
The first way is to filter early. Compare the job from the start of this chapter with the same job on paid orders only.
With the filter, Task 1 drops refunded order 103, and Task 2 drops pending order 105. Three partial rows are left: (1, 65.00), (3, 15.50) and (4, 80.00). The answer is different because you ask a different question.
Spark often moves a filter down by itself. Writing the filter early still makes your plan easy to read. Do not expect it when the filter uses the result of a UDF (a Python function you give to Spark).
Here are the other ways. Each one removes rows or removes a whole shuffle.
Select only the columns you need. Rows with fewer columns are cheaper to write and send.
Use built-in aggregates like sum and count. Spark adds up inside each task first, as in step 1. A Python function that gets all rows of a group cannot do this. Then every row crosses the shuffle.
Join with a small table by broadcast. Spark sends the small table to every machine, so the big table does not move. The chapter "Joins in Spark" teaches this.
Skip steps you do not need. Do not use repartition or distinct without a reason. Do one orderBy at the end, not in the middle.
Interview question
The question is "A job is slow. How do you find and reduce the shuffle?" A good answer: "I find the Exchange lines in explain(). I filter early and select fewer columns. I broadcast small tables. I remove extra repartition and orderBy steps."
07 · Measure it
Shuffle numbers in the UI
⏱ 6 min · Topic 7 of 9
The Spark UI shows how much a shuffle moved. Open a job, then click one stage to see its numbers.
A stage that ends in an Exchange shows a Shuffle Write number. A stage that starts after it shows a Shuffle Read number.
Look at what each number means for our model job.
UI number
Which stage
Meaning
In our model
Shuffle Write Size / Records
Stage 0
Data the tasks saved to local disk
5 partial rows
Shuffle Read Size / Records
Stage 1
Data the tasks fetched from other tasks
5 partial rows
Spill (Memory) / Spill (Disk)
Any stage
Data that did not fit in memory
None (the data is tiny)
The UI says "records". A record is a row. The UI also shows the size in bytes.
A real run has larger numbers that will not match this table.
Three checks help you find the problem.
Shuffle Write is very large compared with your input. Filter or select earlier.
There is spill. The partitions are too big for memory.
A few tasks read much more than the rest. This is skew, and the next chapter covers it.
08 · Recap
Check yourself
⏱ 4 min · Topic 8 of 9
Four points to keep.
A shuffle moves rows so rows with the same key meet. Each task writes files to local disk, and the next stage reads them over the network.
groupBy, join, distinct, orderBy, repartition and window partitionBy cause shuffles. An Exchange line in explain() marks each one.
A shuffle costs disk, network, serialization and a stage boundary. Too much data in one task makes it spill.
Make a shuffle smaller: filter early, select fewer columns, broadcast small tables, and skip needless steps.
Answer these without looking back.
In our model, which two partial rows for customer 3 meet in the shuffle, and what is the final total?
Why is the shuffle write kept on disk and not only in memory?
Name three steps that cause a shuffle and one that does not.
The default for spark.sql.shuffle.partitions is 200. Why can that be a poor choice for a tiny job?
09 · Next Chapter
Next Chapter
You now know how a shuffle works, why it is slow, and how to make it smaller.
⏱ 3 min · Topic 9 of 9
Next chapter
Data Skew & Salting
You now know how a shuffle works, why it is slow, and how to make it smaller.
Next you see what happens when one key has most of the rows, and how to fix it.