P06 — MapReduce-Style Computation Framework
Run it first. There is a companion page that builds this project's machinery as numbered, independently runnable blocks and then assembles them into one measured system: P06 hands-on — block by block (
handson/h*.py). Every number on it was produced by running the code. Read it alongside the milestones below.
Medium · 88 hours · Weeks 68–75 · Stage 3 · Go
Table of Contents
- The Loop, Instantiated
- Why This Project Matters
- Prerequisites
- Duration and Size
- Central Technical Questions
- Architecture
- Showcase — Why Backup Tasks Exist
- Implementation Milestones
- Concepts To Study
- Primary-Source Readings
- Experiments
- Benchmarks and Metrics
- Correctness Tests
- Failure Tests
- Expected Difficulties
- Scope Boundaries
- Deliverables
- Exit Criteria
- Extension Ideas
- Connections
- References
The Loop, Instantiated
| Step | For this project |
|---|---|
| 1. Problem | Run a computation over more data than one machine holds, on unreliable machines, without the programmer writing any fault-tolerance code |
| 2. Constraints | Workers fail. Workers get slow. Some keys have far more data than others. The network is the scarcest resource |
| 3. Naive design | Yours. Most people invent: an RPC fan-out with manual retry; a shared queue of work items; "just use goroutines on a big machine" |
| 4. Predicted failure | Predict which of these kills you first: worker failure mid-task, one hot key, or shuffle bandwidth |
| 5. Minimal implementation | Coordinator + workers, map phase, local shuffle, reduce phase, on one machine with multiple processes |
| 6. Correctness | Output identical to a single-process implementation, for every fault schedule |
| 7. Instrumentation | Per-task timing, shuffle bytes, per-partition key counts, worker utilisation timeline |
| 8. Baseline | The single-process version. And a hand-written distributed version of the same job |
| 9. Bottleneck | Is the job bound by map compute, shuffle bytes, reduce skew, or coordinator round trips? |
| 10. Hypothesis | Speculative execution recovers most of the straggler penalty above a threshold task count. Predict the threshold |
| 11. Modification | Backup tasks |
| 12. Experiment | Straggler severity × frequency × with/without backup |
| 13. Failure analysis | Every job that produced wrong output gets a full schedule reconstruction |
| 14. Report | Why the restricted programming model is what makes any of this possible |
Why This Project Matters
MapReduce's contribution was not the algorithm — grouping by key and aggregating is older than computing. It was the observation that if you restrict what a programmer may express, you can automate fault tolerance.
Because a map function is required to be a pure function of one input record, and a reduce function a pure function of one key's values, the framework knows that any task can be re-executed anywhere at any time with the same result. That single property is what lets it retry, speculate, and reschedule without the user writing a line of recovery code. Take the restriction away — let map read shared mutable state — and every one of those mechanisms becomes unsound.
This is the most transferable idea in the whole journey. The same trade appears in React's pure render functions, in Spark's lineage-based recovery, in Terraform's declarative resources, in every idempotent HTTP API you have designed, and in functional programming generally. Restriction buys automation. This project is where you feel the price and the payoff at the same time, because you will write both the framework and the hand-rolled distributed alternative and compare them.
Prerequisites
- P05 complete — the fault injector, membership, and failure detector are reused directly
- Go concurrency; comfort with process-level parallelism
- Familiarity with EMR/Hive from your day job is an asset here: you have operated this model for years and can now build it
Duration and Size
Medium, 88 hours, 8 weeks.
| Tier | Contents | Hours |
|---|---|---|
| MVI | Coordinator, workers, input splitting, map, hash-partitioned shuffle to local files, sort, reduce, task retry on worker failure. Word count runs correctly under worker kills. | 40 |
| Standard | + speculative execution, coordinator checkpointing and recovery, data locality, combiners, a skew study, configurable task granularity, a multi-job benchmark suite. | 88 |
| Extension | A lineage/DAG execution model in the Spark style, with a measured comparison of recovery cost against re-execution. | +35–50 |
Central Technical Questions
- What must be true of a map function for the framework to retry it freely? State the contract precisely.
- Why is the shuffle the hard part? It is an all-to-all data movement, and all-to-all is the pattern that scales worst.
- What is the right task size? Too large and stragglers dominate; too small and coordinator overhead does. Derive both bounds.
- Why does one hot key ruin everything, and what are the four available fixes?
- How much does speculative execution actually buy, and when does it cost more than it saves?
- What does the coordinator's failure cost you, and what would it take to survive it?
Architecture
Write your naive design first.
input files ──► split into M chunks (64 MB nominal)
│
┌─────────────────┴──────────────────┐
▼ ▼
┌─────────┐ ┌─────────┐
│ map │ emit(k,v) ──► partition │ map │ R local files each
│ task 1 │ by hash(k)%R ──► sort ──│ task M │ (the shuffle write)
└────┬────┘ └────┬────┘
└────────────┬───────────────────────┘
│ reduce task r pulls its partition from ALL M mappers
▼ (this is the all-to-all: M × R transfers)
┌──────────────┐
│ reduce task r│ merge-sort R inputs ──► reduce(k, [v]) ──► output
└──────────────┘
coordinator: task table (idle/in-progress/done), worker heartbeats,
retry on failure, speculative duplicate on slow, atomic rename on commit
Task granularity, derived
Let \(M\) be the number of map tasks, \(W\) workers, \(c\) the per-task coordinator overhead, and \(T\) the total work.
- Per-task overhead costs \(Mc\) in total. Larger \(M\) → more overhead.
- Straggler exposure: job time is bounded below by the slowest single task, roughly \(T/M\) for balanced work. Larger \(M\) → smaller tail exposure.
- Load balance: with \(M \gg W\), dynamic assignment smooths worker speed differences. The classic rule of thumb is \(M \approx 10W\) or more.
So \(M\) is squeezed from both sides, and the optimum depends on \(c\), which you must measure. The original paper used M=200,000, R=5,000 on 2,000 machines — that is 100 map tasks per machine, and the ratio is the interesting part, not the absolute numbers.
Why stragglers dominate, quantified
Job completion is the maximum over tasks, not the mean, and maxima behave badly. Simulated: 200 tasks of 10 s nominal on 20 workers, greedy list scheduling, 2,000 trials:
| scenario | mean completion | vs ideal | with backup tasks |
|---|---|---|---|
| no stragglers | 100.00 s | 1.00× | — |
| 1% of tasks 10× slower | 112.45 s | 1.12× | 108.64 s (1.09×) |
| 5% of tasks 10× slower | 149.87 s | 1.50× | 110.01 s (1.10×) |
| 1% of tasks 50× slower | 448.20 s | 4.48× | 108.64 s (1.09×) |
| 5% of tasks 3× slower | 114.09 s | 1.14× | 110.01 s (1.10×) |
Two tasks out of two hundred, at 50× speed, inflate the job 4.5×. Backup tasks recover almost all of it. That is why §3.6 of the paper exists, and it is a far more persuasive argument as a table you generated than as a sentence you read.
The same effect at the request level is Dean & Barroso's tail-at-scale result. If a request touches \(N\) independent components, each exceeding its p99 latency 1% of the time, the probability that at least one is slow is \(1 - 0.99^N\):
| N | P(at least one slow) |
|---|---|
| 1 | 1.00% |
| 10 | 9.56% |
| 100 | 63.40% |
| 1000 | 99.996% |
At 100 components, the majority of requests hit a p99 event. Tail latency is not an edge case at scale; it is the common case. Memorise this table.
Skew, and the four fixes
Hash partitioning assumes keys are roughly uniform. Real key distributions are Zipfian:
in a news corpus, "the" may be 5% of all tokens, and every occurrence lands on one
reducer. Options:
- Combiners — pre-aggregate on the map side. Works only for associative and commutative reduce functions. Cheapest and most effective when applicable.
- Salting — append a random suffix to hot keys, reduce in two passes. Costs a second shuffle.
- Range partitioning with sampling — sample the keyspace, choose boundaries to equalise bytes rather than key count. What TeraSort does.
- Skew-aware splitting — detect hot keys at runtime and give them dedicated reducers.
Implement 1 and at least one other, and measure both against the unmitigated case.
Showcase — Why Backup Tasks Exist
Forty minutes, before you build a coordinator. Job completion is the maximum over tasks, and maxima behave badly. Simulate it and the case for §3.6 makes itself.
# P06 -- why one straggler in two hundred tasks costs 4.5x.
import random, statistics
random.seed(0)
def job(ntasks=200, nworkers=20, frac=0.0, mult=1, backup=False, trials=800):
out=[]
for _ in range(trials):
t=[10.0*(mult if random.random()<frac else 1.0) for _ in range(ntasks)]
if backup: t=[min(x, 20.0) for x in t] # duplicate: cap at 2x nominal
w=[0.0]*nworkers
for x in sorted(t, reverse=True):
i=w.index(min(w)); w[i]+=x
out.append(max(w))
return statistics.fmean(out)
base=job()
print(f"{'scenario':<28}{'completion':>12}{'vs ideal':>10}{'w/ backup':>12}")
for frac,mult,lbl in ((0.0,1,"no stragglers"),(0.01,10,"1% at 10x"),
(0.05,10,"5% at 10x"),(0.01,50,"1% at 50x")):
a=job(frac=frac,mult=mult); b=job(frac=frac,mult=mult,backup=True)
print(f"{lbl:<28}{a:>10.1f}s{a/base:>9.2f}x{b:>10.1f}s")
print("\\nTwo tasks out of two hundred inflate the job 4.5x. That is MapReduce 3.6.")
scenario completion vs ideal w/ backup
no stragglers 100.0s 1.00x 100.0s
1% at 10x 112.4s 1.12x 108.5s
5% at 10x 149.5s 1.50x 110.0s
1% at 50x 447.5s 4.47x 108.5s
\nTwo tasks out of two hundred inflate the job 4.5x. That is MapReduce 3.6.
Two tasks out of two hundred inflate the job 4.5×, and backup tasks recover almost all of it. That table is a far more persuasive argument for speculative execution than the paragraph in the paper, because you generated it. It is also E4 before you have written a line of the framework.
Implementation Milestones
| # | Milestone | Hours | Done when |
|---|---|---|---|
| 1 | Repo, job/task/worker interfaces, input splitting with record-boundary handling | 6 | A split never cuts a record in half — test with a record spanning a boundary |
| 2 | Coordinator: task table, assignment, heartbeats, timeouts | 8 | Reuses P05's failure detector |
| 3 | Map worker: run user fn, partition by hash, sort, spill to local files | 9 | Word count produces correct per-partition files |
| 4 | Shuffle: reduce workers pull their partition from every mapper | 9 | Bytes transferred instrumented and matching prediction |
| 5 | Reduce worker: k-way merge of M sorted inputs, then user reduce fn | 8 | Output matches single-process for word count |
| 6 | Atomic output commit via temp file + rename | 4 | A killed reducer leaves no partial output visible |
| 7 | Task retry on worker failure | 6 | Job completes with 50% of workers killed mid-run |
| 8 | Combiners | 5 | Shuffle bytes drop measurably on word count |
| 9 | Speculative execution with a progress-rate estimator | 9 | E4 reproduces the straggler table above |
| 10 | Coordinator checkpointing + recovery | 7 | Coordinator killed mid-job; job resumes, does not restart |
| 11 | Data locality: prefer a worker holding the input split | 5 | Locality hit rate measured |
| 12 | Benchmark suite: word count, inverted index, sort, join, PageRank iteration | 7 | All five run and are validated |
| 13 | Experiments + report | 5 | All rows filled |
Concepts To Study
- The map/reduce contract: purity, determinism, and what each buys the framework
- Input splitting and record boundaries — the unglamorous detail that breaks correctness silently
- The shuffle: sort-based vs hash-based; why sorting on the map side makes the reduce side a merge
- External merge sort: k-way merge, the memory/pass trade
- Partitioning: hash, range, custom; sampling for range boundaries
- Combiners and the associativity requirement
- Stragglers and backup tasks; progress-rate estimation
- Task granularity and the derivation above
- At-least-once execution + idempotent commit = exactly-once effect. The atomic rename is what makes this work, and it is the same trick as P03's flush
- Coordinator fault tolerance: checkpointing vs re-execution
- Data locality and the memory/network hierarchy at cluster scale
- Lineage (Spark) as an alternative to re-execution from input
Primary-Source Readings
Budget: 11 hours.
| Reading | Why | Hours |
|---|---|---|
| Dean, J., Ghemawat, S. MapReduce: Simplified Data Processing on Large Clusters. OSDI 2004 | The source. §3.6 on backup tasks is what E4 tests | 2.5 |
| Ghemawat, S., Gobioff, H., Leung, S.-T. The Google File System. SOSP 2003 | The storage assumptions MapReduce is built on — especially append semantics | 2 |
| Zaharia, M. et al. Resilient Distributed Datasets. NSDI 2012 | Why lineage beats re-execution, and what MapReduce gets wrong for iterative jobs | 2 |
| Dean, J., Barroso, L. A. The Tail at Scale. CACM 56(2), 2013 | The table above; the general theory of stragglers | 1.5 |
| Zaharia, M. et al. Improving MapReduce Performance in Heterogeneous Environments. OSDI 2008 | The LATE scheduler — naive speculation is actively harmful on heterogeneous clusters | 1.5 |
| Isard, M. et al. Dryad. EuroSys 2007 | The general-DAG generalisation | 1 |
| Verma, A. et al. Large-scale cluster management at Google with Borg. EuroSys 2015 | Where the tasks actually run | 0.5 |
Experiments
| # | Experiment | Sweep | Predict first |
|---|---|---|---|
| E1 | Task granularity | M ∈ {W, 2W, 10W, 100W} | The optimum, from the derivation. Then find where it actually is |
| E2 | Cluster size | W ∈ {1,2,4,8,16} | Speedup curve; predict where Amdahl bites |
| E3 | Key skew | Zipfian α ∈ {0, 0.5, 1.0, 1.5} | Reduce-phase imbalance; predict the p99/p50 task-time ratio |
| E4 | Speculative execution | straggler {1%,5%} × {3×,10×,50×} × on/off | Reproduce the simulated table with real tasks |
| E5 | Naive vs LATE speculation | on a heterogeneous cluster (throttled workers) | Naive speculation should hurt. Predict by how much |
| E6 | Worker failure | kill {10%,30%,50%} mid-job | Completion-time penalty; predict it is sublinear |
| E7 | Coordinator failure | kill at 25%/50%/75% progress | With/without checkpointing |
| E8 | Shuffle volume | with/without combiner, on word count | Bytes and time; predict the reduction from the key distribution |
| E9 | Network bottleneck | throttle to {10,100,1000} Mbps | Where does shuffle stop being free? |
| E10 | Data locality | on/off | Predict the win; it should be smaller than you expect within a rack |
| E11 | Skew mitigation | none / combiner / salting / range partition | Which wins, and at what cost |
| E12 | Framework vs hand-written | same job both ways | Lines of code, fault-tolerance behaviour, performance |
E12 is the point of the project. Write word count as a hand-rolled distributed Go program with manual retry. Then write it as a map and a reduce function. Compare: lines of code, what happens when you kill a worker, and how long each took to make correct. The framework version will be slower and dramatically more robust, and articulating why that trade is usually right is the report's thesis.
E5 is the counterintuitive one. Naive speculation ("duplicate any task below the average progress rate") assumes homogeneous workers. On a heterogeneous cluster it duplicates every task on the slow machines, consuming the capacity that would have finished them. It makes things worse, and measuring that is a genuinely good result.
Benchmarks and Metrics
| Metric | Notes |
|---|---|
| Job completion time | p50 and p95 across repeated runs — jobs vary |
| Task duration distribution | The distribution; the max is what matters |
| Worker utilisation timeline | A stacked plot over time; the shape shows straggler tails immediately |
| Shuffle bytes | Total, and per map-reduce pair |
| Shuffle time as % of job | The number that decides whether locality work pays |
| Speculative tasks launched | And how many actually won |
| Wasted work | CPU-seconds in tasks that were killed or lost |
| Retries per job | By cause |
| Coordinator RPC rate | The scaling limit on task count |
| Locality hit rate | Fraction of map tasks reading a local split |
| Recovery time | Coordinator restart to job resumption |
Correctness Tests
- Output equals single-process output, byte-for-byte after sorting, for every job in the benchmark suite.
- Under every fault schedule. Same assertion, with the P05 injector running.
- No duplicate output despite at-least-once task execution — the atomic-rename property.
- Record boundaries: a record spanning a split boundary appears exactly once.
- Partition completeness: every emitted key lands in exactly one reduce partition.
- Sort order within each reduce input.
- Combiner equivalence: results identical with and without the combiner. A non-associative reduce fn must be rejected, not silently mis-computed.
- Deterministic replay: same seed, same fault schedule, same output.
- Empty inputs, single-record inputs, one-key inputs all work.
Failure Tests
| Injection | Required behaviour |
|---|---|
| Kill a map worker mid-task | Task reassigned; output correct |
| Kill a reduce worker mid-write | No partial output; task reassigned |
| Kill 50% of workers | Job completes, slower |
| Kill the coordinator | Job resumes from checkpoint |
| Pause a worker (SIGSTOP) | Speculative execution covers it |
| Slow disk on one worker | Detected as a straggler, not as a failure |
| Network partition worker↔coordinator | Worker's tasks reassigned; the partitioned worker must not commit output when it returns |
| Duplicate task completion messages | Idempotent |
| Corrupt an intermediate file | Detected by checksum; task re-run |
| Disk full on a mapper | Clean failure; task rescheduled elsewhere |
The partitioned-worker case is the important one: a worker that loses contact, continues working, and then reappears must not overwrite the output of the replacement task. The atomic rename plus a task-attempt id in the filename is the standard fix, and you should construct the interleaving deliberately to prove yours works.
Expected Difficulties
- The shuffle is 60% of the work. Budget accordingly; milestones 3–5 are the core.
- "Distributed" on one laptop needs discipline. Use real processes and real sockets, throttle bandwidth deliberately (E9), and never let a shortcut assume shared memory.
- Speculative execution can make things worse and you will see it (E5). That is the result, not a bug.
- Exactly-once output is subtler than it looks. The atomic rename must be paired with attempt ids, or a resurrected worker clobbers good output.
- Skew experiments need realistic data. Synthetic uniform keys teach nothing; use real text or a Zipfian generator with a stated α.
- The coordinator is a single point of failure and making it not one is a whole subproject. Milestone 10 is checkpoint-and-restart, not a replicated coordinator. Say so in the report.
Scope Boundaries
In scope: batch jobs, map/shuffle/reduce, task-level fault tolerance, speculative execution, coordinator checkpointing, local-filesystem intermediate storage.
Out of scope: a distributed filesystem (use local disks and simulate locality); a general DAG engine (extension); SQL or a query optimiser; resource negotiation à la YARN; streaming (P07); iterative-job optimisation; a web UI beyond a status endpoint.
Deliverables
mapreduce/— Go framework with the five benchmark jobsREPORT.mdwhose thesis is E12: why the restricted model wins- The worker-utilisation timeline plot — the most legible artifact this project produces, and the one that makes stragglers obvious to any audience
- Notebook entries for E4, E5, E12
- Reusable: the straggler simulator, and the skew-aware partitioner
Exit Criteria
- All five benchmark jobs produce output identical to single-process
- Job completes correctly with 50% of workers killed mid-run
- Coordinator failure recovery works from checkpoint
- E4 complete: speculative execution measured across the straggler matrix, compared against the simulated prediction
- E5 complete: naive speculation shown to hurt on a heterogeneous cluster, quantified
- E3 + E11 complete: skew measured and at least two mitigations compared
- E12 complete: the hand-written comparison, with lines of code and fault behaviour
- The partitioned-worker output-clobbering interleaving is tested explicitly
-
REPORT.mdwritten with a falsified prediction
Extension Ideas
- Lineage-based recovery (RDD-style): recompute only the lost partition instead of re-running from input. Measure recovery cost on an iterative job — that is the argument of the Spark paper and you can reproduce it.
- General DAG execution with pipelined stages.
- Adaptive task sizing: split slow tasks at runtime rather than duplicating them. A research direction.
- In-memory shuffle with spill-to-disk, and the resulting memory/speed frontier.
Connections
Backward: P05 supplies the fault injector, heartbeats, and failure detection. P04 supplies intermediate storage patterns and the sorted-file merge.
Forward:
- → P07 (Streaming): the scheduler and worker pool generalise to long-running operators; the shuffle becomes a network partitioner
- → P08/P09: batch embedding generation and offline evaluation are natural MapReduce jobs, and running them on your own framework is a good integration check
- → P15: the batch layer
References
- Dean, J., Ghemawat, S. MapReduce: Simplified Data Processing on Large Clusters. OSDI 2004.
- Ghemawat, S., Gobioff, H., Leung, S.-T. The Google File System. SOSP 2003.
- Zaharia, M. et al. Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing. NSDI 2012.
- Zaharia, M., Konwinski, A., Joseph, A. D., Katz, R., Stoica, I. Improving MapReduce Performance in Heterogeneous Environments. OSDI 2008.
- Dean, J., Barroso, L. A. The Tail at Scale. CACM 56(2), 2013.
- Isard, M., Budiu, M., Yu, Y., Birrell, A., Fetterly, D. Dryad: Distributed Data-Parallel Programs from Sequential Building Blocks. EuroSys 2007.
- Verma, A. et al. Large-scale cluster management at Google with Borg. EuroSys 2015.
- Vavilapalli, V. K. et al. Apache Hadoop YARN: Yet Another Resource Negotiator. SoCC 2013.
- Kwon, Y. et al. SkewTune: Mitigating Skew in MapReduce Applications. SIGMOD 2012.
- O'Malley, O. TeraByte Sort on Apache Hadoop. 2008. Range partitioning by sampling.