What is a shuffle, and why is it expensive?
Summary
- A shuffle is Spark's mechanism for re-distributing data so that it's grouped differently across partitions. The Spark docs say it "typically involves copying data across executors and machines, making the shuffle a complex and costly operation."
- Operations that can cause a shuffle include
repartitionandcoalesce, ByKey operations such asgroupByKeyandreduceByKey, and joins such ascogroupandjoin. - The cost comes from disk I/O, data serialization and network I/O, plus in-memory data structures that can spill to disk and intermediate files that stay on disk until garbage collection.
- On Databricks, Photon "uses a redesigned columnar shuffle to increase throughput for large-scale joins."
What is a shuffle, and why is it expensive?
"The shuffle is Spark's mechanism for re-distributing data so that it's grouped differently across partitions." Take reduceByKey: all values for one key must be in the same place to compute the result, but they may sit on different partitions or machines. Spark has to perform an all-to-all operation: read from all partitions to find the values for every key, then bring values together across partitions to compute each key's result. That step is the shuffle.
Which operations cause a shuffle?
The RDD programming guide names three families: repartition operations such as repartition and coalesce, ByKey operations (except for counting) such as groupByKey and reduceByKey, and join operations such as cogroup and join. The API docs add detail: RDD repartition "uses a shuffle to redistribute data", while RDD coalesce takes a shuffle flag (default False) for "whether to add a shuffle step".
Why is a shuffle expensive?
| Cost | What the Spark docs say |
|---|---|
| Disk I/O | Map task results stay in memory until they can't fit, then are sorted by target partition and written to a single file. Reduce tasks read the relevant sorted blocks |
| Serialization and network I/O | The shuffle "involves disk I/O, data serialization, and network I/O" |
| Memory and spill | reduceByKey and aggregateByKey build in-memory data structures on the map side, and ByKey operations build them on the reduce side. When data does not fit, Spark spills them to disk, adding disk I/O and garbage collection |
| Intermediate files | Shuffle files are kept until the RDDs are garbage collected, so long-running jobs can use a large amount of disk space |
How do I see a shuffle in the Spark UI?
Every SparkContext launches a Web UI, by default on port 4040 (http://<driver-node>:4040). Open a stage from the Stages tab. The stage DAG labels nodes by operation scope, such as Exchange, and the summary metrics include:
| Metric | Definition from the Spark docs |
|---|---|
| Shuffle Read Size / Records | Total shuffle bytes read, from local and remote executors |
| Shuffle Remote Reads | Total shuffle bytes read from remote executors |
| Shuffle Read Fetch Wait Time | Time tasks spent blocked waiting for shuffle data from remote machines |
| Shuffle Write Time | Time tasks spent writing shuffle data |
| Shuffle spill (memory) | Size of the deserialized form of the shuffled data in memory |
| Shuffle spill (disk) | Size of the serialized form of the data on disk |
How do I reduce shuffle cost?
- Aggregate with
reduceByKeyoraggregateByKey. ThegroupByKeydocs say that for a sum or average per key these "will provide much better performance".reduceByKeyalso merges locally on each mapper before sending results to a reducer.
from operator import add
rdd = sc.parallelize([("a", 1), ("b", 1), ("a", 1)])
sorted(rdd.groupByKey().mapValues(sum).collect()) # groups all values first
sorted(rdd.reduceByKey(add).collect()) # merges locally first
- Reduce partitions with
coalesce. The DataFrame docs say going from many partitions to fewer "results in a narrow dependency" with no shuffle, but a drastic coalesce, such as to one partition, may run on fewer nodes than you like. Callingrepartition()instead "will add a shuffle step" but keeps upstream partitions running in parallel. - Sort while repartitioning.
repartitionAndSortWithinPartitionscan push the sorting down into the shuffle machinery. - Raise parallelism if reduce tasks run out of memory. Shuffle operations "build a hash table within each task", and the tuning guide's simplest fix is to increase the level of parallelism so each task's input is smaller.
- Persist an RDD you will reuse. The guide recommends calling
persiston the resulting RDD.
Which settings affect shuffles?
| Setting | Default | What the Spark docs say |
|---|---|---|
spark.shuffle.compress |
true |
Whether to compress map output files |
spark.shuffle.spill.compress |
true |
Whether to compress data spilled during shuffles |
spark.reducer.maxSizeInFlight |
48m |
Maximum size of map outputs to fetch simultaneously from each reduce task, a fixed memory overhead per reduce task |
How does Databricks handle shuffles?
Photon is "the Databricks-native vectorized query engine." Its architecture "uses a redesigned columnar shuffle to increase throughput for large-scale joins." The Photon docs also say it doesn't support RDD APIs, so Photon does not run the RDD examples above.
For Spark SQL, spark.sql.shuffle.partitions defaults to 200 on Databricks. Setting it to auto "enables auto-optimized shuffle, which automatically determines this number based on the query plan and the query input data size." The Databricks Spark UI guide recommends spark.sql.shuffle.partitions=auto for stages with high shuffle.
FAQs
Which setting controls the number of shuffle partitions?
For Spark SQL, spark.sql.shuffle.partitions sets the number of partitions used when shuffling data for joins or aggregations (default 200). For RDD operations, pass numPartitions or set spark.default.parallelism.
How long do shuffle files stay on disk?
The guide says they are preserved until the corresponding RDDs are no longer used and are garbage collected, so they don't need to be re-created if the lineage is re-computed. The scratch directory is spark.local.dir.
The information provided herein is for general informational purposes only and may not reflect the most current product capabilities or configurations.