Unit 3 · 5 hrs · CSUE301 Big Data Analytics

Parallel &
Distributed Processing

Chapter 2 asked where a byte lives. This chapter asks who does the work on it. The same three hub servers that stored CityCourier's blocks now also compute on them — and a job has to be split into tasks, scheduled onto machines, and put back together, all without one bad task taking the whole thing down.

One job, three map tasks, three reduce tasks

"Count orders per restaurant" isn't one step

It's map, then shuffle, then reduce — on machines, not just on paper.

Chapter 1 did this arithmetic by hand. This chapter asks which machine runs which piece, in what order, and what happens if one of them doesn't finish.

Colour contract for this chapter Hub N's server Hub S's server Hub E's server What the scheduler had to decide
3.1 · CLUSTER RESOURCE MANAGEMENT

Who Gets to Run Next?

Three machines, one job, and every machine is also running someone else's job at the same time. Something has to decide who runs where, right now, without asking a human.

Section 2.1 split storage into a single coordinator (the NameNode, metadata only) and many workers (DataNodes, holding actual blocks). Scheduling computation across a cluster uses the identical shape, for the identical reason: one place has to hold the global view, and it can't also be doing the heavy lifting, or it becomes the bottleneck it was supposed to prevent. The standard answer — YARN (Yet Another Resource Negotiator) — names four roles instead of two.

YARN's roles ResourceManager one per cluster. Tracks every machine's free CPU/memory and decides which job gets resources next. Never runs a task itself. NodeManager one per machine (here: one on each of Hub N, Hub S, Hub E). Reports its free resources upward and launches containers when told to. ApplicationMaster one per SUBMITTED JOB, not one per cluster. Negotiates containers from the ResourceManager and tracks that specific job's tasks to completion. Container a bounded slice of CPU + memory on one machine, granted to one task at a time. Where work actually executes.
why a per-job ApplicationMaster, and not one global scheduler doing everything?

Early Hadoop (MapReduce 1) used exactly one central scheduler for both cluster bookkeeping and every job's task tracking, and it became a scaling bottleneck as clusters grew. YARN split the job-specific bookkeeping (which task of this job succeeded, which needs retrying) out into a fresh ApplicationMaster per job, leaving the ResourceManager with only the cluster-wide question — who gets a container next — and nothing job-specific to track at all.

Worked Example · Launching "Count Orders Per Restaurant"
From submission to running containers
3 map tasks · 3 NodeManagers
CityCourierDB submits a job to count orders by restaurant. Trace it from submission to the first task actually running.
1 · submitThe client asks the ResourceManager for a container to run this job's ApplicationMaster. The RM picks any NodeManager with room — say Hub S — and launches the AM there.
2 · negotiateThe new AM inspects the job: 3 map tasks (one per Section 2.1's blocks) and, say, 3 reduce tasks. It asks the ResourceManager for 6 containers total, stating a preference: each map task's container should run on whichever NodeManager already holds that block.
3 · place, honouring data localityBlock 1 lives on {Hub N, Hub S} — the RM can satisfy the AM's preference by granting a container on either. Same for Blocks 2 and 3. No map task ships its input block across the network; every one runs where its data already sits.
4 · execute and reportEach NodeManager launches its container, runs the map task, and reports progress back to the AM (not the ResourceManager — the RM never learns task-level detail). Once all three map tasks finish, the AM requests reduce containers and repeats the pattern.
Takeaway: the ResourceManager touched this job exactly twice — granting the AM's container, then granting task containers on request. Every task-level decision after that was the job's own AM, not the cluster-wide scheduler.
The Roles, Live
pitfalls
  • "The ResourceManager is just a renamed NameNode." Same architectural pattern (one coordinator, many workers), completely different job: the NameNode answers "where is this block," the ResourceManager answers "who gets a container next." A cluster typically runs both, side by side, solving unrelated problems.
  • "One ApplicationMaster runs the whole cluster's jobs." Every submitted job gets its own fresh AM instance, in its own container, tracking only its own tasks. Ten simultaneous jobs means ten ApplicationMasters, not one AM juggling ten job's bookkeeping.
  • "Data locality is automatic; the scheduler always finds a local slot." The AM can only request a data-local container. If every NodeManager holding a needed block is already fully busy, the RM grants a container somewhere else, and that task has to ship its input over the network after all — exactly the cost Section 1.6 measured, paid because of resource contention, not because locality wasn't tried.
Practice
  1. A cluster runs 4 simultaneous jobs. How many ApplicationMasters exist right now, and how many ResourceManagers?
    Solution

    4 ApplicationMasters (one per running job) and exactly 1 ResourceManager (one per cluster, regardless of how many jobs are running).

  2. Block 2 (Section 2.1) lives on {Hub S, Hub E}. Both are fully busy with other containers when this job's AM asks for a data-local map container. What are the AM's realistic options?
    Solution

    Wait for a container to free up on Hub S or Hub E (preserving locality, at the cost of time), or accept a container on Hub N and have that map task fetch Block 2's data over the network (preserving speed-of-start, at the cost of Section 1.6's data-movement penalty). Real schedulers typically wait a short, configurable period before falling back to the second option.

  3. The ApplicationMaster's own container dies partway through the job. Does the ResourceManager know which of the job's 3 map tasks had already finished?
    Solution

    No — the ResourceManager never tracked task-level state at all; only the AM did. This is precisely why AM failure is handled separately (the RM restarts the AM in a fresh container), and the restarted AM typically has to rediscover job progress from durable job-history logs, not from anything the RM was keeping.

  4. Connect this to Section 2.4: quorum replication kept the storage layer working through a node failure by having no single point of failure among data holders. Does YARN's design give the same guarantee to the ResourceManager itself?
    Solution

    Not by the basic architecture above — a single ResourceManager is a single point of failure for scheduling new work, exactly the concern Section 2.1 raised about a single NameNode. Production YARN deployments address this with a standby ResourceManager that can take over, which is the scheduling-layer equivalent of Section 2.1's replication fix for the NameNode, not a difference in kind.

3.2 · A MAPREDUCE JOB, START TO FINISH

Map, Shuffle, Reduce — on Real Machines

Section 1.3 defined MapReduce's three phases in the abstract. Now run them on the three actual blocks from Section 2.1, and watch where the network gets involved.

The job: count how many orders each restaurant received. Three map tasks, one per block, placed exactly as Section 3.1 described — each on a NodeManager that already holds its input.

Worked Example · Every Pair, Every Hop
Map locally, shuffle by key, reduce per partition
16 orders · 5 restaurants · 3 reducers
Run the job on Blocks 1–3 and follow every (restaurant, count) pair to its reducer.
map (local, no network)Block 1's map task emits: Curry Point×2, Momo Hut×1, Wrap City×1, Sushi Go×1, Pizza Barn×1. Block 2 and Block 3 each emit one of every restaurant. Nothing has crossed a network link yet — every map task only touched its own local block.
shuffle (the only phase that crosses the network)All pairs sharing a key must land on the same reducer. With 3 reducers over 5 restaurant names, one reasonable assignment: Hub N handles {Curry Point, Momo Hut}, Hub S handles {Pizza Barn, Sushi Go}, Hub E handles {Wrap City}.
reduce (local again, once shuffle delivers)Hub N sums 7 shuffled records down to 2 output rows (Curry Point:4, Momo Hut:3). Hub S sums 6 records to 2 rows (Pizza Barn:3, Sushi Go:3). Hub E sums 3 records to 1 row (Wrap City:3).
Answer: Curry Point 4, Momo Hut 3, Wrap City 3, Sushi Go 3, Pizza Barn 3 — matching Section 1.3's original count exactly, now with every hop traced to a specific machine.
Step Through It
Map → shuffle → reduce, one stage at a time
3 nodes · 16 records · 5 keys
Stage 1 of 3
Map: each node processes only its own block. Nothing has crossed the network yet.
the load imbalance was a choice, not an accident

Hub N ends up with 7 shuffled records, Hub S with 6, Hub E with just 3 — an uneven split from an assignment that put two busy restaurants together on one reducer. A different key-to-reducer assignment (say, distributing by a hash of the restaurant name rather than alphabetically) would balance this more evenly across three reducers, exactly Section 1.5's "balance beats headcount" lesson, now showing up at the shuffle-partitioning layer instead of the node-count layer.

pitfalls
  • "The map phase is the expensive one." Map tasks here touched only local data — the cheapest phase in the whole job by Section 1.6's own logic. Shuffle is the one phase guaranteed to cross the network, which is why Sections 3.3 and 3.4 both target it.
  • "Reduce tasks all start once ALL map tasks finish." A reducer can start pulling shuffle data for keys whose map output has already landed, overlapping with still-running map tasks elsewhere — real schedulers exploit this, though the reduce computation itself can't finish until every relevant map task has contributed its share.
  • "Five distinct keys means five reducers is the natural choice." Reducer count is a cluster-sizing decision independent of key count — three reducers over five keys (as here) or five hundred reducers over five keys (wasteful, most sitting idle) are both valid; the job just has to choose an assignment of keys to whatever count it picks.
Practice
  1. If the job used only 2 reducers instead of 3, and Hub N took {Curry Point, Momo Hut, Wrap City} while Hub S took {Sushi Go, Pizza Barn}, how many shuffled records would each reducer handle?
    Solution

    Hub N: 4+3+3=10 records. Hub S: 3+3=6 records. Total 16, matching the job's full record count, just split differently — and more unevenly than the 3-reducer version in the worked example.

  2. Which phase would a network outage between Hub N and Hub S actually block: map, shuffle, or reduce?
    Solution

    Shuffle specifically — it's the only phase that requires inter-node communication. Map tasks already running would finish untouched (purely local), and reduce tasks could still process any shuffle data that had already arrived before the outage.

  3. Section 2.6 measured that a hub-selective query costs 1 network hop under hub-partitioning versus 3 under hash-partitioning. Does the reducer assignment in this section's worked example (alphabetical, not hash-based) change that lesson?
    Solution

    No — it's a different partitioning question entirely. Section 2.6 partitioned stored rows by a query-relevant key; this section partitions shuffle keys (restaurant names) across reducers for a computation already in flight. Both are partitioning decisions, but one is about where data rests and the other is about where a specific job's intermediate results land.

  4. Hub E's reducer, handling only Wrap City, finishes well before Hub N's. Is Hub E's NodeManager now free to pick up unrelated work from a different job?
    Solution

    Yes — once its container's task completes, the NodeManager reports the freed resources to the ResourceManager (Section 3.1), which can immediately grant that capacity to any other job waiting for containers. An idle reducer doesn't sit reserved for its original job "just in case."

3.3 · SHRINKING THE SHUFFLE: COMBINERS

Add Before You Ship

Block 1's map task emits two separate (Curry Point, 1) pairs, from orders #1 and #5, and ships both across the network separately. Why would it ship two, when it could add them first and ship one?

A combiner runs the reduce function's logic locally, before shuffle, on whatever a single map task already produced. It's not a new phase — it's the reduce logic borrowed early, applied to one map task's own output, on that same machine, with no network involved.

Worked Example · Block 1, With and Without
Without a combiner

Block 1 emits one pair per row: (Curry Point,1), (Momo Hut,1), (Wrap City,1), (Sushi Go,1), (Curry Point,1), (Pizza Barn,1). 6 pairs cross the shuffle from this one block, even though Curry Point's two pairs are about to be added together anyway.

With a combiner

The same map task's output is pre-summed locally first: (Curry Point,2), (Momo Hut,1), (Wrap City,1), (Sushi Go,1), (Pizza Barn,1). 5 pairs cross the shuffle — the two Curry Point pairs became one, for free, before the network was involved at all.

One pair saved, from sixteen rows, looks unimpressive. It isn't the point at this scale — it's the mechanism, and the mechanism's payoff is proportional to how many repeats of the same key land on the same map task, which grows directly with data volume.

The Same Mechanism, at Citywide Volume
per NodeManager, 46,080 orders/day ÷ 3 nodes 15,360 orders processed locally by one map task, per day without combiner: 15,360 (restaurant, 1) pairs cross the shuffle with combiner: at most 1,200 pairs — one per distinct restaurant citywide, however many orders each contributed reduction: ~12.8× fewer records crossing the network, per node, per day

The combiner didn't change the final answer by one order — Section 3.2's reduce step still adds up to the identical totals either way. It changed how many bytes had to move to get there, which is exactly Section 1.6's "moving computation to data" principle, applied one level deeper: instead of moving the whole computation, move just enough of it — the associative part — to happen before the expensive hop.

why this only works for some operations

Combining works because counting is associative and commutative: (2+1)+1+1+1 gives the same answer as 1+1+1+1+1+1 regrouped any way you like. Sum, count, min and max all share this property. Computing an average naively does not — combining partial averages isn't the same as averaging everything at once — which is why a correct average-combiner has to carry a (sum, count) pair forward instead of a single averaged number, and divide only once, at the very end.

pitfalls
  • "A combiner replaces the reducer." It runs the reduce logic early and locally, on one map task's partial output. The real reducer still runs afterward, on the shuffle results from every map task combined, and must produce the same answer whether or not any combiner ran.
  • "Combiners always run, if you write one." The framework may skip a combiner if there's nothing to gain (say, a map task that emits every key exactly once) — a combiner is a hint the framework is free to use, not a guaranteed extra pass.
  • "If sum can be combined, so can everything." Non-associative or order-dependent computations (median, or anything needing every raw value present at once) generally can't be partially pre-aggregated this way at all.
Practice
  1. Block 2's local counts (Section 3.2) are Momo Hut:1, Wrap City:1, Curry Point:1, Pizza Barn:1, Sushi Go:1. Does a combiner save any pairs here? Why or why not?
    Solution

    No savings — every restaurant in Block 2 appears exactly once, so pre-summing changes nothing (each pair is already a "sum of one"). Combiners only save shuffle traffic when the same key repeats within one map task's own output, which happened for Curry Point in Block 1 but not for anything in Block 2.

  2. Could a combiner be written for "find the maximum order amount per restaurant"? Sketch why or why not.
    Solution

    Yes — max is associative and commutative exactly like sum: max(max(a,b),c) = max(a,b,c) regrouped any way. A map task seeing two Curry Point orders locally (₹340 and ₹310) can combine them to a single (Curry Point, 340) pair before shuffle, since 340 already IS the local maximum.

  3. A job computes "average order amount per restaurant" with a naive (broken) combiner that averages locally and ships (restaurant, local_average) pairs. Curry Point's four orders are ₹340 and ₹310 in Block 1, ₹355 in Block 2, and ₹330 in Block 3. Show the naive combiner gives the wrong final answer.
    Solution

    True average across all four: (340+310+355+330)/4 = 1335/4 = ₹333.75. The naive combiner ships three local averages instead — Block 1's (340+310)/2=325, Block 2's 355, Block 3's 330 — and the reducer averages THOSE three numbers: (325+355+330)/3 = ₹336.67. Wrong, because it silently treated Block 1's two real orders as worth the same as Block 2's and Block 3's one each, overweighting the smaller blocks.

  4. Redo the same computation with the correct (sum, count) combiner instead. Confirm it recovers the true ₹333.75.
    Solution

    Block 1 ships (650, 2), Block 2 ships (355, 1), Block 3 ships (330, 1). The reducer sums both fields separately: sum = 650+355+330 = 1335, count = 2+1+1 = 4, then divides exactly once: 1335/4 = ₹333.75 — matching the true average, because carrying the count forward preserves how many real orders each partial sum actually represents.

3.4 · DATA SKEW

When One Reducer Does All the Work

Three reducers, evenly provisioned, identical hardware. One of them still takes ten times longer than the other two — not because it's slower, but because of which keys landed on it.

Section 3.2's reducer assignment already split unevenly — 7 shuffled records to Hub N, 6 to Hub S, only 3 to Hub E — simply because two busier restaurants happened to land on the same reducer. That was a mild version of a problem called data skew: when the real-world distribution of a key isn't uniform, no partitioning scheme that just spreads keys evenly can guarantee it spreads work evenly, because some keys carry far more records than others.

Worked Example · A Chain Restaurant, at Citywide Scale

Sixteen rows can't show this convincingly — the whole point of skew is that it hides at small scale and bites at large scale. Citywide, CityCourier serves roughly 1,200 distinct restaurants across 46,080 orders/day.

uniform vs. real-world key distribution if every restaurant got an equal share: 46,080 ÷ 1,200 = 38.4 orders/day, average but Curry Point is a 40-location chain, not one kitchen — suppose it captures 12% of ALL citywide orders: 46,080 × 0.12 = 5,530 orders/day, all under ONE reducer key skew ratio: 5,530 ÷ 38.4 ≈ 144× the average restaurant's load

Every other reducer in the job finishes in the time it takes to process a few dozen records. Curry Point's reducer is still working through 5,530 — and because a MapReduce job doesn't finish until its last task finishes, the entire job's runtime is now set by that one straggler, no matter how fast the other 1,199 reducers were.

more reducers doesn't fix this

Adding reducers helps keys that were sharing a partition by coincidence (Section 3.2's Curry-Point-and-Momo-Hut pairing). It does nothing for Curry Point itself: however many reducers exist, every one of Curry Point's 5,530 daily orders shares the same key, and standard hash-partitioning sends every instance of one key to exactly one partition. A thousand reducers with one skewed key still has one reducer doing 5,530 orders' worth of work while the rest sit idle.

Two Real Fixes
salting
  • Split the hot key artificially: relabel Curry Point's rows as Curry Point#1 through Curry Point#10 (round-robin), spreading its 5,530 orders across 10 reducer partitions instead of 1.
  • Needs a second pass to re-combine the 10 partial counts back into one true Curry Point total — extra work, but far less than one reducer eating 5,530 records alone.
pre-aggregation
  • Section 3.3's combiner already shrank Curry Point's contribution from every map task down to one partial sum per task before shuffle — the skew is still there, but the sheer record count hitting the reducer drops sharply.
  • Doesn't fully solve extreme skew alone: 3 map tasks each pre-summing a third of 5,530 orders still ships 3 large partial sums to the same one reducer, which is better than 5,530 tiny ones but still lopsided.
pitfalls
  • "Skew means the data is wrong." Curry Point genuinely receiving more orders than a small independent isn't a Section 1.1 veracity problem — it's an accurate reflection of the real world. Skew is a processing challenge, not a data-quality one.
  • "Salting is free." It trades one problem for another: extra shuffle traffic (the same records now travel as 10 smaller groups instead of 1 big one) and a mandatory second aggregation pass. It's a worthwhile trade against a severe straggler, not a strictly better default for every job.
  • "Skew only affects reduce." Anywhere a key groups records — a join key, a partition key for storage (Section 2.3's ring can suffer this too, if one key is queried far more often than others) — can concentrate load onto one machine, not just a MapReduce reducer specifically.
Practice
  1. If Curry Point's share were 25% instead of 12%, recompute its daily order count and the skew ratio against the 38.4/day average.
    Solution

    46,080 × 0.25 = 11,520 orders/day. Skew ratio: 11,520 ÷ 38.4 = 300× the average restaurant's load — roughly double the worked example's 144×, since the share itself roughly doubled.

  2. With salting into 10 sub-keys, roughly how many orders does each of Curry Point's 10 reducer partitions handle, and how does that compare to the citywide average restaurant's 38.4/day?
    Solution

    5,530 ÷ 10 ≈ 553 orders per sub-key — still about 14× the average restaurant's load (553/38.4), far better than 144× unsalted, but not perfectly even. Salting reduces skew; it doesn't guarantee eliminating it, since the choice of how many sub-keys to split into is itself a tuning decision.

  3. A job joins CityCourier's orders against a restaurants table, keyed by restaurant name. Explain why Curry Point being a skewed key is now a problem for the join, not just for aggregation.
    Solution

    A join has to bring together every order row AND the matching restaurant row for the same key on one machine to actually perform the join. Since every one of Curry Point's 5,530 daily order rows shares that same key, all 5,530 land on whichever machine is handling the Curry Point side of the join — identical mechanism to the reduce-side skew above, just triggered by a join key instead of a group-by key.

  4. Section 1.5 showed a 2-node split (11 vs 5) being less efficient than a balanced 3-node split, due to hardware imbalance. How is Curry Point's skew fundamentally different from that earlier imbalance?
    Solution

    Section 1.5's imbalance was a scheduling choice — the same 16 orders could have been split more evenly across nodes, and a better split would have fixed it entirely. Curry Point's skew is a property of the data itself: no matter how the job assigns keys to reducers, every one of Curry Point's orders must still end up together on one reducer for a correct count, so re-splitting the assignment can spread OTHER keys better but can never split Curry Point's own total across multiple reducers without an extra mechanism like salting.

3.5 · SPARK'S DAG EXECUTION MODEL

What If the Job Isn't Just One Map-Shuffle-Reduce?

Count orders per restaurant, then keep only restaurants with 4 or more. That's two steps. MapReduce doesn't have a "two-step job" — it has two jobs.

MapReduce's contract is rigid on purpose: one map phase, one shuffle, one reduce, done. A computation with two logically dependent steps has to run as two separate MapReduce jobs, the first writing its output to HDFS, the second reading that output back in. Spark's answer is to represent the whole computation — however many steps — as one DAG (directed acyclic graph) of operations, and let the scheduler decide how much of it can run without ever touching disk in between.

the vocabulary RDD Resilient Distributed Dataset — not stored data, but a RECIPE: the sequence of operations needed to compute a set of partitions from durable source data. Transformation map, filter, reduceByKey, etc. LAZY — building the DAG, not running anything yet. Action collect, count, save. The only thing that actually triggers execution of the DAG built so far. Narrow dependency each output partition depends on exactly one input partition (map, filter). No shuffle needed — can pipeline with neighbours. Wide dependency an output partition depends on MANY input partitions (reduceByKey, groupByKey, join). Forces a shuffle, and a new stage.
Worked Example · One DAG vs Two Jobs

Reuse Section 3.2's job — count per restaurant — then add the second step: keep only restaurants with a count of 4 or more. Only Curry Point (count 4) survives; the other four restaurants (all count 3) are dropped.

Chained MapReduce

Job 1: map + shuffle + reduce → writes 5 rows to HDFS.
Job 2: reads those 5 rows back in, map + shuffle + reduce (trivial, but still a full job) → filters to 1 row.
Total: 2 shuffles, 1 full HDFS write-then-read of the intermediate result.

Spark, one DAG

Stage 0: map + local combine (narrow, pipelined).
shuffle — the only one
Stage 1: reduce (wide dependency's other side) then filter (narrow — pipelines right into Stage 1, no new shuffle needed).
Total: 1 shuffle, zero intermediate disk writes.

See the Graph
lazy evaluation's real trap

Nothing runs until an action is called — not map, not filter, not reduceByKey, however many of them are chained. This is efficient (the whole DAG can be optimised before anything executes) but has a sharp edge: calling two separate actions on the same chain of transformations, without caching anything, reruns the entire DAG from the original source twice. Spark doesn't remember "I already computed this" unless told to, with an explicit .cache() or .persist() — Chapter 4 covers exactly when that's worth doing.

pitfalls
  • "An RDD is a dataset sitting in memory somewhere." It's a lineage — a recipe for producing partitions from durable source data. Whether any of it is actually materialized in memory at a given moment depends entirely on caching decisions, which are separate from the RDD's definition.
  • "More transformations means more stages." Stage boundaries are created only by wide dependencies (shuffles). A chain of ten map calls in a row is still one stage — they all pipeline together with no shuffle between any of them.
  • "Spark eliminates shuffles." It eliminates the unnecessary repeated ones that chained MapReduce jobs forced. Any wide-dependency operation — grouping, joining — still needs exactly one shuffle to gather matching keys together; Spark just avoids scheduling more shuffles than the computation actually requires.
Practice
  1. Add a third step: after filtering to count≥4, sort the surviving restaurants by name. Does this add a new stage to the Spark DAG?
    Solution

    Sorting by key generally requires a shuffle (records need to be redistributed by sort order across partitions), so yes — it adds a second shuffle and therefore a new stage boundary, giving 3 stages total instead of 2. Sorting is a wide-dependency operation just like grouping and joining.

  2. Would a chain of filter → map → filter, with no grouping or joining anywhere, ever force a shuffle?
    Solution

    No — all three are narrow-dependency operations, each output partition depending on exactly one input partition. The entire chain pipelines into a single stage with zero shuffles, regardless of how many such operations are chained together.

  3. A Spark job calls .count() on a chain of transformations, then calls .collect() on the exact same chain again, without caching anything in between. What actually happens, cost-wise?
    Solution

    The entire DAG — every transformation, including any shuffle — runs twice, once triggered by each action. Nothing from the first run is reused, exactly the "lazy evaluation's real trap" above; the fix is calling .cache() on the chain before either action, so the second action reuses the first run's materialized result instead of recomputing from source.

  4. Section 3.1's ApplicationMaster tracked one job's tasks for the ResourceManager. In a Spark job with 2 stages, is that still one ApplicationMaster, or one per stage?
    Solution

    Still one — the ApplicationMaster is per job (or per Spark application, which can itself run many actions/DAGs across its lifetime), not per stage. The same AM negotiates containers for Stage 0's tasks, then for Stage 1's tasks once the shuffle they depend on is ready, exactly the same role Section 3.1 described, just coordinating a multi-stage plan instead of a single map-then-reduce one.

3.6 · FAULT TOLERANCE

A Task Dies. Does the Job Restart?

Hub N's reducer, mid-way through summing its 7 shuffled records, crashes. Section 2.1 taught the storage-layer version of this question. Here's the processing-layer version.

Both MapReduce and Spark answer the same way in spirit: retry a bounded unit of work, not the whole job. They differ in what that unit is anchored to, and that difference has real cost consequences.

Worked Example · Redoing Only What Was Lost
Hub N's reduce task fails partway through
7 of 16 shuffled records at risk
The ApplicationMaster notices Hub N's task has stopped reporting. What gets redone?
MapReduce's answerThe AM marks only that one task FAILED and requests a fresh container — on Hub N again, or elsewhere if Hub N itself is the problem. The new task re-reads its assigned shuffle partition (Curry Point and Momo Hut's records, already sitting durably on disk from the completed map tasks) and re-sums from scratch.
what that costs7 of the job's 16 shuffled records get reprocessed — 43.8% of the total, not 100%. Hub S's and Hub E's completed reduce tasks are untouched; their output is simply kept.
Answer: the retry unit is one task. A failure costs exactly that task's share of the work, bounded and predictable, because the map stage's output was already durably persisted before the failure happened.
The Same Question for Spark

Spark's retry unit is a lost RDD partition, recomputed by replaying its lineage — the recorded chain of transformations that built it. If Hub N's reduce-side partition is lost, Spark needs the shuffle data the map stage already wrote to disk (same durable artifact MapReduce relies on) to recompute it — often just as cheap as the MapReduce retry above.

the case where lineage costs more, not less

If Hub N was also holding a cached, in-memory-only intermediate result (Chapter 4's territory) that was never written to disk, and that same machine's failure took the cache with it, Spark's lineage has to reach further back — potentially replaying the map stage itself, not just the reduce. A long, uncached lineage chain can cost more to replay than a single bounded MapReduce task retry would. Lineage isn't a strictly cheaper mechanism; it's a more flexible one, whose actual cost depends entirely on how much of the chain has to be redone.

pitfalls
  • "A task failure restarts the whole job." Both systems bound the damage to the failed task (MapReduce) or lost partition (Spark). A job with a thousand tasks and one failure redoes roughly one task's worth of work, not a thousand.
  • "Spark's lineage is always cheaper than MapReduce's retry." Only when the lineage chain to replay is short or already cached. An uncached, many-stage lineage can mean replaying more work than a single MapReduce task ever would have to.
  • "Retrying a task risks double-counting its output." Real systems use an output-commit protocol so only one attempt's result for a given task is ever considered final, even if a slow original attempt eventually limps across the finish line after a retry already started (a pattern called speculative execution) — the framework, not the task itself, guarantees exactly-once output per task.
Practice
  1. If Hub E's reduce task (handling only Wrap City, 3 records) fails instead of Hub N's, what fraction of the job gets redone?
    Solution

    3 of 16 records, or 18.75% — smaller than Hub N's 43.8%, simply because Hub E's reducer was handling fewer keys' worth of shuffled data. This is exactly Section 3.2's load-imbalance note showing up again: an uneven reducer assignment means failures aren't equally expensive to retry either.

  2. A map task (not a reduce task) fails, before any of its output has been shuffled anywhere. What has to be redone, and does it touch the network?
    Solution

    Just that one map task, re-reading its own local block (Section 2.1) and re-emitting its pairs — no network involved, since map tasks only ever touch local data. This is the cheapest possible failure: nothing had been shuffled yet, so nothing downstream needs to change.

  3. Chapter 2's replication factor 2 meant a DataNode failure never lost a block outright. Does an equivalent guarantee exist for a failed MAP task's output, once it's been shuffled to a reducer?
    Solution

    Generally yes in practice: shuffle output is written to local disk (not held only in the map task's memory) precisely so a downstream reduce failure doesn't force map tasks to rerun. But this is weaker than Section 2.1's replication — that shuffle file usually exists on ONE machine's local disk, not replicated 2× across machines, so losing that specific machine before the reducer reads it would force the map task to rerun after all.

  4. Tie this section back to Section 3.1's ApplicationMaster. What specifically does the AM need to know to decide a task has failed, and where does that information come from?
    Solution

    Missing heartbeats or progress reports from the task's container, over some timeout window — the exact same signal Section 2.1's NameNode uses to detect a dead DataNode, just scoped to one running task instead of one storage node. Neither system needs a human to notice; both rely on the absence of an expected periodic signal.

CLOSING · CHEAT SHEET

Chapter 3, One Page

3.1 · Cluster Resource Management

ResourceManager one per cluster · NodeManager one per machine

ApplicationMaster one per JOB, tracks its own tasks only

Data locality: AM requests containers where blocks already sit

3.2 · A MapReduce Job

Map = local, free · Shuffle = the only networked phase · Reduce = local again

Key-to-reducer assignment controls load balance, independent of key count

3.3 · Combiners

Pre-aggregate locally, before shuffle — same reduce logic, run early

Only valid for associative/commutative ops; average needs (sum,count), not a pre-divided value

Payoff scales with data volume: negligible at 16 rows, ~13× at citywide scale

3.4 · Data Skew

Uniform key spread ≠ uniform work spread, if real-world frequency is skewed

More reducers doesn't split one hot key's load — salting or pre-aggregation does

Job finishes only when its slowest task does

3.5 · Spark's DAG Model

RDD = lineage, not stored data · transformations lazy, actions trigger execution

Narrow deps pipeline for free · wide deps (shuffle) create new stages

No caching = every action reruns the whole DAG from source

3.6 · Fault Tolerance

Retry unit = one task (MapReduce) or one lost partition via lineage (Spark)

Both bounded, not job-wide — but uncached long lineage chains can cost more to replay

Shuffle output on local disk is what makes reduce-side retries cheap in both systems

CLOSING · SELF-TEST

Mixed Review

Eight questions, deliberately out of section order.

  1. A cluster's ResourceManager crashes. Can already-running tasks keep executing?
    Solution

    Yes — the RM's job is granting NEW containers, not supervising tasks already running (that's each job's own ApplicationMaster). Already-running containers keep executing; only the ability to start new work stalls until the RM (or a standby) is back.

  2. A map task's output never gets used by any reducer, because the job is killed before shuffle starts. Was running that map task wasted work?
    Solution

    In terms of final output, yes — nothing downstream consumed it. But it cost nothing beyond that one task's local compute time; no network resources were spent, since shuffle (the networked phase) never began.

  3. Explain, in one sentence, why "average" needs a different combiner strategy than "sum" or "count."
    Solution

    Sum and count are directly additive across partial results, but a partial average discards the count that produced it, so combining partial averages naively double-weights whichever partition had fewer records — the fix is carrying (sum, count) forward and dividing only once, at the end.

  4. A hash-partitioned job sends every instance of key "Curry Point" to reducer #2 by design. Is that a bug?
    Solution

    No — it's required for correctness. Every record for the same key MUST land on the same reducer for a group-by or join to produce a right answer. The problem in Section 3.4 wasn't that Curry Point's records were grouped together; it's that the volume behind that one key was disproportionate.

  5. A Spark job chains 6 map calls, then one reduceByKey, then 2 more map calls. How many stages does this DAG have?
    Solution

    2 stages. All narrow-dependency operations (every map) pipeline together with whatever's adjacent; only the single reduceByKey forces a shuffle, creating exactly one stage boundary regardless of how many map calls sit on either side of it.

  6. Which is cheaper to retry after a failure: a map task that hasn't shuffled yet, or a reduce task that has already pulled its shuffle data?
    Solution

    Both retries are bounded to their own task, but the map task's retry is purely local (re-read its block, no network); the reduce task's retry still needs to re-pull its shuffle partition from wherever the map outputs are stored, which may or may not involve the network depending on where that data sits relative to the retried task's new container.

  7. Why can't naive hash-partitioning fix Curry Point's skew just by using more reducers?
    Solution

    Because standard hash-partitioning sends every occurrence of one key to exactly one partition, however many partitions exist. Adding reducers spreads OTHER keys thinner but does nothing to split Curry Point's own single-key load, which is why Section 3.4 needed salting (splitting the key itself) rather than just adding capacity.

  8. A cached Spark result and a Section 2.1 replicated HDFS block both exist so something doesn't have to be recomputed or refetched from scratch. What's the key difference in what happens if the machine holding each one dies?
    Solution

    A replicated block (RF≥2) survives the failure outright — another copy already exists elsewhere. A cache with no replication is simply gone, and Spark must fall back to recomputing it via lineage. Caching speeds up repeated access; it isn't, by itself, a durability mechanism the way replication is.

FURTHER READING

If You Want the Long Version

  • Hadoop: The Definitive Guide — Tom White. Covers YARN's ResourceManager/NodeManager/ApplicationMaster split (Section 3.1) and MapReduce's job execution internals (Section 3.2) in full implementation detail.
  • Spark: The Definitive Guide — Chambers & Zaharia. The authoritative source on RDDs, the DAG scheduler, narrow versus wide dependencies, and lineage-based fault tolerance behind Sections 3.5 and 3.6 — essential before Chapter 4's deeper dive into in-memory processing.
  • Mining of Massive Datasets — Leskovec, Rajaraman & Ullman. Its treatment of combiners and skewed-key handling in MapReduce (Sections 3.3–3.4) predates Spark but the underlying shuffle-cost reasoning is identical.

Chapter 3 of 7 · CSUE301 Big Data Analytics · builds on Chapter 2's NameNode/DataNode split (Section 2.1) and cost trade-offs (Section 1.6).
Next → Chapter 4, In-Memory Data Processing.