0%
Big Data Engines: Spark, Lakehouse, and Streaming at Scale
The Shape of the Problem
Spark
Query Engines
Streaming at Scale
Pipelines in Production
Shuffle Internals
Every lesson so far has treated the shuffle as a boundary with a price attached. This one opens it. The shuffle is where the majority of the time in a large pipeline is spent, where the majority of its failures originate, and where almost every remedy discussed elsewhere in this course, partition counts, skew, spill, memory sizing, actually lands. Understanding it precisely changes how you read every other number an engine reports.
The one sentence definition
A shuffle is the mechanism by which every record in a dataset is moved to the one task that is responsible for its key. That is the entire purpose, and everything else about the implementation follows from it.
The reason it is expensive is contained in the word every. A narrow transformation lets each task read its input and write its output without consulting anything else, so a thousand tasks can run in complete isolation. A shuffle requires that each of a thousand producing tasks make its output available to each of a thousand consuming tasks, which means a million potential transfers, and it requires that all of the producers finish before any consumer can be certain it has seen everything for its keys.
Those two properties, the quadratic connection count and the barrier, are not implementation details that a cleverer engine could remove. They are what the operation means. Every real improvement in shuffle performance over the last fifteen years has been about making each transfer cheaper or avoiding the shuffle entirely, never about removing the barrier.
The three things that have to happen
A shuffle decomposes into three phases, and it helps enormously to keep them separate when reading any measurement.
The write side runs inside the producing tasks. Each one takes its output records, computes a partition number for each from the key, and writes the records grouped by that number into local storage. Nothing crosses the network yet. This phase ends when the last producing task has written and reported its output locations.
The transfer side runs inside the consuming tasks. Each one knows its own partition number and asks every producing location for the block belonging to that number. These requests go out in parallel with a bounded concurrency, and the bytes arrive over the network.
The read side also runs inside the consuming tasks, after or interleaved with the transfer. The arriving blocks have to be combined, which depending on the operation means concatenating them, merging them in key order, or inserting them into a hash table.
The reason to separate these is that they fail differently and they are fixed differently. A slow write side is a disk or serialisation problem. A slow transfer side is a network or concurrency problem. A slow read side is a memory problem. Treating shuffle time as one number conflates three unrelated diagnoses.
Why the barrier is the expensive part
Of the two properties, engineers tend to worry about the transfer volume and underestimate the barrier, which is usually the larger cost.
Consider a stage of a thousand producing tasks where nine hundred and ninety nine finish in forty seconds and one takes eleven minutes. The transfer volume is unchanged by that one task. The barrier means the consuming stage cannot begin until it finishes, so a cluster sized for a thousand parallel tasks sits almost entirely idle for ten minutes. The shuffle has cost ten minutes of wall clock and almost nothing in bytes.
This is why the diagnostic habit from the previous lesson, descending to the task duration distribution, matters so much here. A shuffle boundary converts the worst task in the producing stage into a delay for the entire consuming stage. Without the boundary, that same slow task would overlap with other work and largely disappear.
The barrier also explains a counterintuitive result that appears repeatedly in real pipelines. Adding more machines to a pipeline whose stages are dominated by a straggler produces no improvement at all, because the straggler was never waiting for capacity. The money bought slots that were already idle.
What the partition function does, and what it cannot do
The assignment of a record to a partition is done by a partition function, which in the default case is a hash of the key reduced modulo the partition count. This is a deliberate and generally good choice, because a hash spreads similar keys apart and gives a near uniform distribution for keys that are themselves reasonably varied.
The crucial point is what it cannot do. A hash distributes keys evenly. It does not distribute records evenly, and those are different things whenever one key has far more records than another. If forty percent of your rows carry the same customer identifier, then forty percent of your rows hash to the same number, land in the same partition and are processed by one task. No partition count, no hash function and no amount of cluster makes that otherwise.
This single fact is the origin of most of the pain in large pipelines, and it is the subject of the next lesson. For now the thing to fix in your mental model is that the partition function is not where skew comes from and not where it can be fixed. It is faithfully doing what it was asked.
What gets written, and in what shape
It is worth being concrete about the artefact the write side produces, because its shape explains several otherwise mysterious limits.
For each producing task, the engine writes one data file containing that task's records grouped by partition number, and one small index file giving the byte offset at which each partition's group starts. A consuming task that wants partition seventeen from producer four reads the index, seeks to the right offset, and streams the bytes.
This design, one file per producer rather than one file per producer and partition pair, is the single most important optimisation in the history of shuffle implementations. The earlier design produced a file for every pair, so a thousand producers and a thousand consumers created a million files, and the file system rather than the network became the bottleneck. Consolidating to one file per producer reduced that to a thousand.
The consequence worth remembering is that a producing task's output is a single sequential write and a consuming task's input is a seek and a scan from each producer. The write side is therefore efficient on almost any storage, and the read side is sensitive to seek cost, which is why shuffle storage on network attached disks behaves so much worse than the same volume on local disks.
Where the files live, and why that matters
The shuffle output has to survive from the moment the producer writes it until the last consumer has read it, which can be a long time if the consuming stage is slow or retried.
By default the files live on the local disk of the machine that produced them, and are served by the executor process that wrote them. This is fast and requires no extra infrastructure, and it has a serious consequence. If that executor dies, the files become unavailable, and every consuming task that has not yet fetched its block fails. The engine then has to re run the producing tasks whose output was lost, which is the one failure mode that is not local to a single task.
The alternative is an external shuffle service, a separate process on each machine that owns the files and serves them independently of the executors. With it, an executor can die, or be removed by dynamic allocation, without taking its shuffle output with it. This is why the previous lesson described the shuffle service as effectively a prerequisite for dynamic allocation.
A third option, increasingly common, is to write the shuffle to remote storage entirely, which decouples it from the compute nodes at the cost of making every read a network read. Whether that trade is good depends on how often you lose nodes, and on spot instances it very often is.
What to carry into the rest of the lesson
Three things from this section are used by everything that follows.
The shuffle has three phases that fail and are fixed differently, so measure them separately. The barrier is usually a larger cost than the bytes, so the task duration distribution of the producing stage matters more than its total output. And the partition function distributes keys rather than records, so an even partition count does not imply even partitions.
The next section opens the write side, where the most common and most expensive mistake in the whole operation lives.