One Busy Pump Can Keep Every Worker Waiting
Why Matching Records Can Leave One Worker Waiting
BOTIE
Spark SQL’s 2015 paper described planning table calculations as operations the engine can optimise and execute across a cluster. The plan still has to respect the grouping we requested. If one pump owns most of the work, sending all of that group to one task can leave the other workers waiting. More machines help only when the work can actually be divided between them.
We will split one pump’s work into temporary partial groups while preserving its original identifier. A count can later add those partial counts. Spark may already perform local aggregation or choose an adaptive plan, so the workshop exposes a routing decision we can reason about rather than predicting a real cluster’s task counts.
Sources for this history
We Added Workers. Why Did the Job Barely Move?
Botie can filter each file independently. Now he wants a count for each pump across all files. Most pumps have a few readings, but P7 supplies nearly everything. Several workers finish while the worker responsible for P7 keeps running.
The problem is how the records meet. Adding workers does not divide a group that is still assigned to one worker. We need to see the routing rule before deciding whether more machines, a different join plan or a partial aggregation would help.
The workers are local lists of equal-cost records, routed by our stated teaching hash. Real Spark plans can aggregate before moving data, and a join needs its own correct treatment of both inputs.
Step 1 of 1
The worker boundary follows the key
The same original pump key occurs four times.
| Reading | pump | Proposed partial group |
|---|---|---|
| r0 | A | A#0 |
| r1 | A | A#1 |
| r2 | A | A#0 |
| r3 | A | A#1 |
| r4 | B | B#0 |
r0
- pump
- A
- Proposed partial group
- A#0
r1
- pump
- A
- Proposed partial group
- A#1
r2
- pump
- A
- Proposed partial group
- A#0
r3
- pump
- A
- Proposed partial group
- A#1
r4
- pump
- B
- Proposed partial group
- B#0
Ask which records must meet
The original entity key survives; the temporary key affects where partial work is performed.
Give the hot group more than one route
The filter worked, but almost all accepted records reached one worker.
BOTIE
Record count on each teaching worker
- 04
- 11
Follow the operations
1. Accepted records
[ { "pump": "A" }, { "pump": "A" }, { "pump": "A" }, { "pump": "A" }, { "pump": "B" } ]2. Original entity field
pump
3. Pump to split into partial groups
A
4. Partial groups for that pump
1
5. Split one hot key into partial groups
[ { "pump": "A", "routingKey": "A#0" }, { "pump": "A", "routingKey": "A#0" }, { "pump": "A", "routingKey": "A#0" }, { "pump": "A", "routingKey": "A#0" }, { "pump": "B", "routingKey": "B#0" } ]6. Teaching worker count
2
7. Route matching keys together
[ [ { "pump": "A", "routingKey": "A#0" }, { "pump": "A", "routingKey": "A#0" }, { "pump": "A", "routingKey": "A#0" }, { "pump": "A", "routingKey": "A#0" } ], [ { "pump": "B", "routingKey": "B#0" } ] ]8. Count each worker's records
- 04
- 11
9. Result
- 04
- 11
The workers are local lists of equal-cost records, routed by our stated teaching hash. Real Spark plans can aggregate before moving data, and a join needs its own correct treatment of both inputs.
1. Ask which records must meet
A filter can decide whether a reading passes by looking at that reading alone. Counting all readings from P7 is different: P7 may occur in files handled by several workers. Those workers have to combine their contributions before they can give us P7’s total.
The grouping value, P7 here, is called the key. A partition is one portion of the work. A shuffle moves records or partial summaries between partitions so that the contributions for a key can be combined. The movement can take longer than the final addition.
In Spark’s physical plan, an exchange marks a place where that movement is required.
2. Averages hide the last unfinished partition
Suppose one worker receives most records. The other workers cannot complete that unsplit group merely by becoming idle. This uneven work distribution is skew. Counts are a first clue; row sizes and the cost of processing each row can also differ.
Look at the slowest tasks and shuffle statistics rather than only the average task time. A long tail can come from a large key, a large record, a spill to disk or another resource problem. Our workshop supplies equal-cost records so routing is the only variable.
Follow one hot pump through the routing rule before deciding whether more partitions would change its destination.
Count the original keys before changing their routes
A dictionary maps each original pump identifier to its accumulated count. The routing marker does not belong in this logical count.
counts = {}
for pump in ["A", "A", "B"]:
counts[pump] = counts.get(pump, 0) + 1
print(counts){'A': 2, 'B': 1}Keep this original-key result as a correctness reference when later experimenting with worker routes.
3. Make partial groups only when they can be recombined
For a count, split a hot pump into temporary subgroups, count each subgroup, then add counts by the original pump. Adding a small subgroup marker is often called salting. Keep the original key because the marker is an execution detail, not a new pump.
This reasoning relies on a mergeable calculation. Summing counts is valid; averaging subgroup averages without their counts is not. Joins need their own correct treatment of both sides. Arbitrarily salting one join input can lose matches.
A subgroup counts = [2, 2]
A final count = 2 + 2 = 4
B final count = 1Inspect the existing partial aggregates before adding an extra salting stage.
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
What can keep the calculation slow after adding idle workers?
Use the information below.
Four records have key A and one has key B. Every matching key is assigned to one worker.4. Inspect a grouped calculation in Spark
The reference groups by pump and requests counts. Explain shows the engine’s chosen plan; the bounded display then requests the result. A larger workload can use adaptive execution, but the exact plan depends on Spark and Databricks versions and the available statistics.
Keep a small unpartitioned reference result while changing execution strategy. Performance work has failed if it produces a faster answer for a different grouping rule.
On a real workload, compare the slow-task distribution before and after a justified plan change.
Run in your own environment
Inspect the exchange around a grouped count
This optional reference runs in your own Databricks environment. The browser exercise below uses ordinary Python and does not contact Databricks.
Use your own Databricks Spark notebook. The generated physical plan is runtime-dependent.
This teaching input is tiny. Inspect a larger approved dataset’s task metrics separately before drawing performance conclusions.
raw = spark.createDataFrame(
[("A", 0), ("A", 1), ("A", 2), ("A", 3), ("B", 4)],
"pump string, reading_id long",
)
counts = raw.groupBy("pump").count()
counts.explain("formatted")
counts.orderBy("pump").show()The correct count per original pump is the invariant. Exchanges and partial aggregates are implementation choices around it.
Official interface documentation. Documentation checked .
5. Use the declared hash only to understand the workshop
The workshop adds routingKey to each dictionary. It cycles the hot key through the requested number of salt groups, then routes by a stated character-code hash. Count each resulting worker list, including empty lists.
That hash makes our expected answers inspectable. Spark uses different hashing and can change its physical partitioning. The resulting worker counts are therefore outputs of this small routing model, not predictions of a real Databricks task layout.
The Python challenge returns group counts under the original pump key as well as worker loads. Both matter: better balance and unchanged meaning.
6. Test a different frequency pattern
Yesterday P7 produced most of the readings. Today another pump may be busiest. A routing choice should follow the records that actually arrived, so inspect the new key frequencies before reusing yesterday’s hot-key setting. Creating extra groups also adds work that our count-only model leaves out.
Check two results together: how much work reaches the busiest worker, and whether the final counts still describe the original pumps. An evenly divided list is useful only if every observation still contributes to the right total.
Next we will hold the calculation steady and change which version of the readings it receives.
Check yourself
Questions on Parts 1 to 6
Question 1 of 4
Which merge preserves individual observation weight?
Use the information below.
Partial groups have means 10 and 30 and counts 3 and 1.Try it with Botie
Build with Botie
Write and run Python