MapReduce: the juice factory

After this lesson you will run a word count by hand — map output, shuffle and sort, reduce output — name the parts of a MapReduce job and what each does, and explain how the framework survives dead machines and slow ones.

The lecture opens with a juice-making business. Cutting one apple and blending the slices is easy; a juice giant that receives a whole container of mixed fruit, and must bottle every kind separately, has a different problem (pp. 777–780). Page 781 puts Google in the same position: indexing and recommendations are as "conceptually simple" as making juice, "But, the data to process is HUGE!!!" — "Google processes over 50 PB of data every day".

Start smaller. Page 792 counts the words of one short user query, three sentences long: "The teacher went to the store. The store was closed; the store opens in the morning. The store opens at 9am." Count the occurrences of store. The reducer output printed on pp. 793 and 797 says <store, 3>.

If your number differs, keep it, and ask: could any part of a MapReduce job running on a cluster turn your count into the slide's 3 — a mapper that dies, a combiner, a second reducer? The last section answers.

From one apple to a container of fruit

The NoSQL lessons spread data and queries over many machines. The first slide of this lecture names what is still missing: "Scalability of computation, not only data and queries" (p. 775). MapReduce is the course's answer, told as a story about juice.

The prototype needs two tools: a knife cuts an apple into slices, a blender turns the slices into juice (p. 778). "Next Day" the recipe meets "all the fruits in the fruit basket" — still one knife, one blender, one glass of mixed juice (p. 779). Then "Industrialization": a giant with a "whole container of fruits" that wants "juice of different fruits separately" (p. 780), one bottle per kind. The slide draws the knife and the blender tiny next to the container: the tools are fine, the scale is the problem. Page 784 names them: the knife is Map, the blender is Reduce.

basket map the knife pieces reduce the blender one mixed glass

Before: one knife, one blender (p. 779)

container map map map map map shuffle reduce reduce reduce apple orange pineapple

After: "A parallel version of the process" (p. 786)

Fig. 1. The two juice slides as a before / after pair. Circle, square and triangle are the three kinds of fruit. Left, one knife and one blender handle the whole basket and make one glass of mixed juice. Right, the container is cut into five baskets that five knives cut at once; every piece keeps its kind, and the fifteen lines carry each kind from every basket to the one blender for that kind, which fills one bottle.

Read the right panel as the whole model. The container is the input data. The five baskets are the splits, one per map task. Each knife is a mapper: it cuts whatever fruit it gets and leaves every piece tagged with its kind — the key. The criss-crossing lines are the shuffle: every apple piece, from every basket, travels to the one apple pile. Each blender is a reducer: it sees all the pieces of one kind and fills one bottle, an output file.

One machine is too slow, parallel code is hard

Page 781 lists the simple operations — "Making juice", "Indexing", "Recommendations etc" — and gives the verdict on the volume: "Sequential execution just won't scale up". Many machines help, since "Parallel execution achieves greater efficiency", "But, parallel programming is hard" (p. 782): parallelization (with its "Race Conditions" and its "Debugging"), "Fault Tolerance", "Data Distribution", "Load Balancing".

MapReduce — "a programming model and an associated implementation for processing and generating large data sets" (p. 783). The programming model gives "Abstractions to express simple computations": you write a map function and a reduce function. The implementation "Takes care of the gory stuff: Parallelization, Fault Tolerance, Data Distribution and Load Balancing".

Those are the four hard things of p. 782, absorbed by the framework; an exam answer names both halves of the definition. Page 785 lists them as advantages — "Automatic parallelization, distribution", "I/O scheduling" (load balancing, network and data transfer optimization), "Fault tolerance" ("Handling of machine failures") — and ends with "Need more power: Scale out, not up!": add machines rather than buy a bigger one, the horizontal scaling of lesson 3.

MapReduce is a programming model — you write only map and reduce, over key-value pairs — plus an implementation that runs them on many machines and handles distribution, failures and load balancing.

The word count, by hand and by machine

MapReduce has one data structure: "All data is in terms of key-value pairs" (p. 788). "Mappers read in data from the filesystem, and output (typically) modified data"; "Reducers collect all of the mappers output on the keys, and output (typically) reduced data". Page 789 gives the shape of a "Typical problem solved by MapReduce":

  1. "Read a lot of data"
  2. "Map: extract something you care about from each record"
  3. "Shuffle and Sort"
  4. "Reduce: aggregate, summarize, filter, or transform"
  5. "Write the results"

You write steps 2 and 4. Step 3 is the library's: it brings all the values of one key together, sorted by key.

The slide's word count

The mapper of p. 792 counts "number of each word in user queries". Its input pair is <queryID, QueryText> — the key Q1, the value the text of the hook — and it emits <word, 1> for every word, punctuation removed: 21 pairs, <The, 1> <teacher, 1> <went, 1> …. A mapper counts nothing: it sees only its own input and cannot know how often a word occurs elsewhere. The table follows the 21 pairs through shuffle and sort to the reducer.

The word count of pp. 792–793 done right, keys lower-cased: the reducer input after shuffle and sort, the output, and what p. 793 prints. Highlighted: the rows where the slide's counts differ.
Reducer inputReducer outputPrinted on p. 793
<9am, [1]><9am, 1><9am, 1>
<at, [1]><at, 1><at, 1>
<closed, [1]><closed, 1><closed, 1>
<in, [1]><in, 1>missing
<morning, [1]><morning, 1><morning, 1>
<opens, [1, 1]><opens, 2><opens, 1>
<store, [1, 1, 1, 1]><store, 4><store, 3>
<teacher, [1]><teacher, 1><teacher, 1>
<the, [1, 1, 1, 1, 1, 1]><the, 6><The, 6>
<to, [1]><to, 1><to, 1>
<was, [1]><was, 1><was, 1>
<went, [1]><went, 1><went, 1>
21 values, 12 keys12 pairs, counts sum to 2111 pairs, counts sum to 18

Page 810 writes the same job in pseudo-code, run on a document "Sam Apple Sam Mom Sam Mom":

// emits one <word, "1"> per word
map(String input_key, String input_value):
    // input_key: document name
    // input_value: document contents
    for each word w in input_value:
      EmitIntermediate(w, "1");
// emits the count of one word
reduce(String output_key, Iterator intermediate_values):
    // output_key: a word
    // output_values: a list of counts
    int result = 0;
    for each v in intermediate_values:
      result += ParseInt(v);
    Emit(AsString(result));

The map emits one pair per word — <"Sam", "1">, <"Apple", "1">, <"Sam", "1">, <"Mom", "1">, <"Sam", "1">, <"Mom", "1">; the library groups them into <"Sam", ["1","1","1"]>, <"Apple", ["1"]>, <"Mom", ["1", "1"]>; the three reduce calls emit "3", "1" and "2", each the count of the key of its call. The values travel as strings, hence ParseInt and AsString; Hadoop gives them real types (section 3).

The model in symbols

The "Programming Model" of p. 791: "To generate a set of output key-value pairs from a set of input key-value pairs", \(\{\langle k_i, v_i\rangle\} \to \{\langle k_o, v_o\rangle\}\), "Expressed using two abstractions":

\[ \begin{aligned} &\text{Map task} \\ &\quad \langle k_i, v_i\rangle \to \{\langle k_{int}, v_{int}\rangle\} \\[4pt] &\text{Reduce task} \\ &\quad \langle k_{int}, \{v_{int}\}\rangle \to \langle k_o, v_o\rangle \end{aligned} \]
\(\langle k_i, v_i\rangle\)
one input pair, one record: Q1 and its text
\(\langle k_{int}, v_{int}\rangle\)
intermediate pairs, any number per input pair: <store, 1>
\(\{v_{int}\}\)
all the intermediate values of one key, gathered by the library: [1, 1, 1, 1]
\(\langle k_o, v_o\rangle\)
the output pair: <store, 4>

Between the two, the "Library" "aggregates all the all intermediate values associated with the same intermediate key" (sic) and "passes the intermediate key-value pairs to reduce function". One map call may emit no pair, one or many. The slide's reduce emits one pair per key — exact for the word count, though a Hadoop reducer may emit any number.

The word-count conveyor

A MapReduce word count on the text in the box, one stage per step: splits, map output, combiner output, the shuffle (one line from every map task to every reduce task it sends pairs to, thicker for more pairs), the grouped reducer input and the output files. Press Next or Play; click any pair to follow its key. The one thing to see: the counting happens only after the shuffle has brought every copy of a key to one reducer — and the combiner changes how many pairs travel, never the result.

  1. Keep the slide's text, M = 3, R = 1, combiner off. Before pressing Next, predict how many pairs the three mappers emit together, and what the reducer writes for store, opens and in. Then step to the end.
    What you should see

    21 pairs, 6 + 10 + 5: one per word, none counted. The reducer writes <store, 4>, <opens, 2>, <in, 1> and <the, 6>: 12 pairs whose counts add up to 21. The slide's <store, 3> cannot come out of these pairs.

  2. Set R = 2 and click a <store, 1>. Predict: can the four store pairs end up on different reducers?
    What you should see

    No. All four go to reduce task 1, because hash(store) mod 2 is 1 for every copy: the partitioner computes the reducer from the key alone. Reduce task 1 receives 17 pairs, task 0 only 4 (closed, morning, teacher, went): the hash spreads keys, not values, and the and store happen to share a reducer.

  3. Back to R = 1. Switch the combiner on and predict the pairs sent in the shuffle for M = 3, then for M = 1. Does the output change?
    What you should see

    21 without the combiner; with it 17 for M = 3 (5 + 7 + 5: each mapper merges only its own repeats) and 12 for M = 1 (one mapper sees every repeat). The output files are identical. On a 64 MB split of real text, with thousands of <the, 1>, the saving is enormous.

  4. Choose "at white space only", the tokenizer of the Java job of p. 811, and step to the end. Predict which keys change.
    What you should see

    14 keys instead of 12: The and the are two keys with 3 each, store. (1) is not store (3), and closed;, morning., 9am. keep their punctuation. This program really would write <store, 3> — next to <store., 1>.

Two invariants carry the model. "All inputs with the same key must go to the same reducer!" (p. 797): if the four <store, 1> reached two reducers, two files would hold partial counts and no file the total. And "Input is typically sorted, output is output exactly as is" (p. 797): every reducer meets its keys in order, so every output file is sorted — each on its own.

Map tags the pieces of each record with a key, the library gathers all the values of a key in one place, reduce aggregates them: counting happens only after the shuffle.

Seven parts and six types

"MR Key Components" (pp. 794–803) takes the job apart. Fig. 2 puts the parts in the order a pair meets them; the table gives each part in the slides' words.

map worker input splitter K1, V1 mapper K2, V2 combiner K2, V2 partitioner network reduce worker shuffle & sort K2, {V2} reducer K3, V3 output committer six types: K1 V1, K2 V2, K3 V3
Fig. 2. The way of a key-value pair through a job, with the types it carries. The combiner is dashed because it is optional; the partitioner decides which reduce worker a pair goes to, and only then does it cross the network. The Master, not drawn, schedules all of it. Input and output have two types each, the intermediate pairs two more: the "six writables" of p. 803.
The key components in the slides' words, in the order of Fig. 2, and what each one is for.
PartThe slide says — and what for
Input splitter (p. 795)"Is responsible for splitting your input into multiple chunks", "used as input for your mappers"; "Splits on logical boundaries. The default is 64MB per chunk" — a record is never cut in half
Mapper (p. 796)"Reads in input pair <K,V> (a section as split by the input splitter)", "Outputs a pair <K', V'>" — called once per record of its split
Combiner (p. 799)"Essentially an intermediate reducer", "Is optional", "Reduces output from each mapper, reducing bandwidth and sorting"
Partitioner, or shuffler (p. 798)"Decides which pairs are sent to which reducer" — same key, same reducer
Reducer (p. 797)"Accepts the Mapper output, and collects values on the key" — one call per key, keys in order
Output committer (p. 800)"Is responsible for taking the reduce output, and committing it to a file"; "needs a corresponding input splitter (so that another job can read the input)" — jobs chain
Master (pp. 801–802)"Responsible for scheduling & managing jobs"; "Scheduled computation should be close to the data if possible", since "Bandwidth is expensive! (and slow)"; "handled by the framework, no user code is necessary" — section 4

Two numbers to keep. The slide's 64 MB per chunk ("You can change it", p. 795 adds) is the Hadoop 1 value, equal to the HDFS block size of the time (lesson 31); Hadoop 2 and later use 128 MB. Give 64 MB as the slide's figure. And one split feeds one map task, while the map function runs once per record of the split: once per line in the Java job below.

The partitioner

"Default is simply: Key.hashCode() % numOfReducers" (p. 798); p. 825 writes it as the default partitioning function:

\[ \text{partition}(k) = \text{hash}(k) \bmod R \]
\(k\)
the intermediate key
\(R\)
the number of reduce tasks; the result is one of \(0, \dots, R-1\)

Equal keys have equal hashes, so they always reach the same reducer, while different keys spread roughly evenly. "Custom partitioning is often required": "to produce a total order in the output" — keys a–m to reducer 0 and n–z to reducer 1, so the sorted files concatenate into one sorted list; to "Provide (more) uniform distribution of load between reducers"; because "Some values might need to be sent to the same reducer" — "To compute the relative frequency of a pair of words <W1, W2> you would need to make sure all of word W1 are sent to the same reducer", since count(W1, W2) is divided by the count of all pairs that start with W1; and for "Binning of results". A custom one "Should implement Partitioner interface", "Set by calling conf.setPartitionerClass(MyPart.class)".

The combiner

The combiner runs on the map side, on one mapper's output, before the shuffle: three <store, 1> of one split leave as one <store, 3>. It "Cannot change the type of its input" — "Input types must be the same as output types" (p. 799) — because the reducer must accept combined and raw pairs alike. Page 815 says when it pays — "When maps produce many repeated keys", with the "Goal" "to decrease size of the transient data" — and that "Combiners have the same interface as Reduces, and often are the same class": "In WordCount, conf.setCombinerClass(Reduce.class);". Page 826: a "Mini-reduce phase before the intermediate data is sent to reduce" that "Saves network bandwidth".

"Combiners must not side effects, because they run an intermdiate number of times" (p. 815). Read: combiners must not have side effects, because the framework runs them an indeterminate number of times — zero, once or several times on the same data. So the result must not depend on whether, or how often, the combiner ran. A sum, a count of ones, a maximum or a minimum pass that test; an average fails, as you know from statistics — the mean of 10 and 20 is 15, the mean of 15 and 60 is 37.5, but the mean of 10, 20 and 60 is 30. Carry (sum, count) pairs instead and divide in the reducer. If a question quotes the printed sentence, this is what it means.

Writables: the six types

Keys and values cross machines and are written to disk, so their types "can be serialized / deserialized to a stream": the Writables of p. 803. There are defaults "for basic values, like Strings, Integers, Longs" and for structures such as arrays and maps, and "User can implement this interface". "Your application needs at least six writables": "2 for your input", "2 for your intermediate values (Map <-> Reduce)", "2 for your output" — K1, V1, K2, V2, K3, V3 of Fig. 2. A combiner adds none.

The complete job

Page 811 prints Hadoop's WordCount, inside public class WordCount { … }, in the Java API of the time. Match the type parameters with Fig. 2.

// the Mapper: <offset, line> in, <word, 1> out
public static class Map extends MapReduceBase implements
    Mapper<LongWritable, Text, Text, IntWritable> {
  private final static IntWritable one = new IntWritable(1);
  private Text word = new Text();

  public void map(LongWritable key, Text value,
      OutputCollector<Text, IntWritable> output,
      Reporter reporter) throws IOException {
    String line = value.toString();
    StringTokenizer tokenizer = new StringTokenizer(line);
    while (tokenizer.hasMoreTokens()) {
      word.set(tokenizer.nextToken());
      output.collect(word, one);
}}}

The mapper's input pair is one line of a text file: key its byte offset (LongWritable), value the line (Text). It emits <word, one> with output.collect, the EmitIntermediate of p. 810. StringTokenizer splits at white space only, so "store." and "store" are two different keys for this program.

// the Reducer: <word, [1, 1, …]> in, <word, sum> out
public static class Reduce extends MapReduceBase implements
    Reducer<Text, IntWritable, Text, IntWritable> {
  public void reduce(Text key, Iterator<IntWritable> values,
      OutputCollector<Text, IntWritable> output,
      Reporter reporter) throws IOException {
    int sum = 0;
    while (values.hasNext()) { sum += values.next().get(); }
    output.collect(key, new IntWritable(sum));
}}
// the driver: configures the job and runs it
public static void main(String[] args) throws Exception {
  JobConf conf = new JobConf(WordCount.class);
  conf.setJobName("wordcount");
  conf.setOutputKeyClass(Text.class);
  conf.setOutputValueClass(IntWritable.class);
  conf.setMapperClass(Map.class);
  conf.setCombinerClass(Reduce.class);
  conf.setReducerClass(Reduce.class);
  conf.setInputFormat(TextInputFormat.class);
  conf.setOutputFormat(TextOutputFormat.class);
  FileInputFormat.setInputPaths(conf, new Path(args[0]));
  FileOutputFormat.setOutputPath(conf, new Path(args[1]));

  JobClient.runJob(conf);
}

In main the input format plays the input splitter and the output format the output committer; setCombinerClass(Reduce.class) reuses the reducer as combiner — allowed, because a sum of sums is a sum and the types match. The slide's arrow "Run this program as a MapReduce job" points at JobClient.runJob(conf). Not set: the partitioner, so the default hash is used, and R, so the job runs Hadoop's default of one reduce task.

Splitter, mapper, combiner, partitioner, shuffle and sort, reducer, committer — and a Master that schedules them: you write the mapper and the reducer and choose the rest.

On the cluster: move the code, not the data

"MR: How it works" (p. 804) puts the job on machines, in the figure of pp. 790 and 805: a "Hadoop Program" forks a Master and workers; map workers "read" their splits and "local write" their output, already cut into one region per reducer; reduce workers "remote read, sort" their regions and "write" the output files.

Page 805 pastes a green warning over the "read" arrows: "Transfer peta-scale data through network". If the splits lie on some machines and the map workers run on others, reading moves the whole input across the network. Page 806 answers in six words: "Move the code, not the data". A map function is a few kilobytes; its input is terabytes.

Master the input travels input 0 1 2 map reduce

Before: "Transfer peta-scale data through network" (p. 805)

where are the chunks? locations NameNode Master read from local disk map 0 1 2 reduce

After: "MapReduce on HDFS" (p. 807)

Fig. 3. The same job twice. Squares 0, 1, 2 are the input splits; dashed outlines mark the two servers that coordinate. Left, the splits sit on machines that do not run the map tasks, and each one crosses the network. Right, the Master asks the NameNode "Where are the chunks of input data?", receives the "Location of the chunks of input data" and runs every map task on a machine that stores its split. In both panels the shuffle still crosses the network.

"MapReduce on HDFS" (p. 807) shows how. The Master asks the HDFS NameNode — the server that knows where every block of every file is stored, lesson 31 — "Where are the chunks of input data?", and gives each map task to a worker that stores its split: "Read from local disk". "Disk Locality" (p. 822) adds the fallback: "Map tasks are scheduled close to data", "on nodes that have input data", "if not, on nodes that are nearer to input data" — "Ex. Same network switch" — which "Conserves network bandwidth". The shuffle still crosses the network, since grouping by key mixes every mapper's output; but it carries intermediate pairs, which a combiner shrinks.

How many tasks

"Execution" (p. 808): the "System determines" M, the "no. of map tasks", as "input is partitioned into M splits"; the "User specifies" R, the "no. of reduce tasks"; "intermediate keys are partitioned (using partitioning function) to be processed by desired reduce task". A 1 GB file in 64 MB splits gives M = 16. "Task Granularity" (p. 823) wants "No. of map tasks > no. of worker nodes", for "Better load balancing" — a fast worker simply takes more tasks — and "Better recovery" — a dead worker's small tasks are re-run quickly, on many machines; "But, increases load on Master": "More scheduling decisions", "More states to be saved". "M could be chosen w.r.t to block size of the file system" "to effectively leverage locality"; "R is usually specified by users", and "Each reduce task produces one output file". The figure of p. 809 adds that map worker and reduce worker are roles: the same three nodes run the "Mapping process" and then the "Reducing process".

The execution flow

Page 812 is the figure of the 2004 Google paper by Dean and Ghemawat (the slide does not say so), its steps numbered.

user program Master split 0 split 1 split 2 split 3 split 4 file 0 file 1 1 2 3 4 5 6 inputfiles mapphase localdisks reducephase outputfiles
Fig. 4. "Execution Flow" (p. 812), redrawn: five splits, three map workers, two reduce workers and two output files, R = 2. Dashed arrows are control (fork and assign), blue arrows and lines are data. Each intermediate file has two regions, one per reduce task. The numbers are the steps below.
  1. Fork. The user program starts copies of itself on the cluster; one becomes the Master, the others workers. Someone must coordinate; everyone else computes.

  2. Assign. The Master gives each idle worker a map task or a reduce task. A worker that finishes gets the next task, so fast workers end up doing more.

  3. Read. A map worker reads its split — from its own disk when it can — and calls map on each record. The input stays where it is stored.

  4. Local write. The map output goes to the worker's local disk, cut into R regions by the partitioner, and the Master records their size and location. The data is temporary: copying it into the distributed file system would cost more than computing it again.

  5. Remote read. Each reduce worker fetches its region from every map worker and sorts it by key. This is where the intermediate data crosses the network: grouping by key needs pieces from every map worker.

  6. Write. Each reduce worker calls reduce on every key, in order, and writes one output file to the distributed file system, so the result survives any one machine.

The Master's bookkeeping is small (p. 813): "For each task", its "State { idle, in-progress, completed }" and the "Identity of the worker machine"; "For each completed map task", the "Size and location of intermediate data" — the addresses step 5 needs.

Hadoop 1 has its own names for the roles (p. 832, Fig. 5): the JobTracker, one per cluster, is the Master; a TaskTracker on every slave node runs the tasks. The slide never defines them; lesson 31 shows what replaced them.

MapReduce layer HDFS layer master node JobTracker NameNode slave node TaskTracker DataNode assigns tasks manages 1..*
Fig. 5. The Hadoop 1 cluster of p. 832. The slide labels the boxes "Name" and "Data" and the right half "HFDS layer" (read HDFS), and also draws a TaskTracker and a DataNode inside the master node: the master machine may work as a slave too. The stacked band stands for the "1..*" slave nodes.

The Master asks the NameNode where the input blocks are and runs each map task where its split is stored: the code travels, the input stays, and only the shuffle crosses the network.

Dead workers, slow workers, bad records

On a cluster of commodity machines something is always broken. "Fault Tolerance" (p. 821) gives the rules. "Worker failure – handled via re-execution": a dead worker is "Identified by no response to heartbeat messages"; "In-progress and Completed map tasks are re-scheduled"; "Workers executing reduce tasks are notified of re-scheduling"; "Completed reduce tasks are not re-scheduled".

B's map task, re-run on D A B C D X Y completed, kept still reading file 0, in HDFS
Fig. 6. Worker B misses its heartbeats. Its completed map task is scheduled again, on D, because its output — the intermediate file under it — was on B's local disk and is gone; the reduce worker Y, still reading, fetches the lost region from D instead. Reduce worker X had already fetched B's region and finished: its output file is in HDFS, in three copies, and nothing of it is re-run — as for a reduce task that B itself had completed.

Why re-run map tasks that had finished? Their output lay on the local disk of the dead machine (step 4) and died with it. A finished reduce task wrote its file to the distributed file system (step 6), which keeps copies elsewhere. A reduce task the dead worker was still running starts again elsewhere too: the slide lists only the map side, but in the Google paper every in-progress task of a failed worker goes back to idle. Re-running is safe only if a task gives the same output twice: "our nodes are (or at least should be) deterministic", "Nodes should have no side effects!" (p. 802) — node means a task there. A task that also inserted rows into an outside database would insert them twice. A silent task is treated the same way: "If a task fails to report progress … it is assumed to be stuck, and is killed, and the step is re-launched (with the same input)" (p. 801).

"Master failure" is "Rare"; it "Can be recovered from checkpoints", or "All tasks abort" (p. 821): two designs. A Master that saves checkpoints of its tables restarts from the last one; one that saves none aborts the job, and the client starts it again.

"Achieves fault-tolerance through replication" (p. 833, the conclusion). MapReduce's own mechanism is re-execution (p. 821); replication is what HDFS does to the input and to the final output, and what makes re-execution possible. An exam answer names both.

Stragglers and back-up tasks

A job ends when its last task ends. "Slow workers delay completion time" (p. 824), for reasons as dull as "Bad disks with soft errors", "Other tasks eating up resources" or "Strange reasons like processor cache being disabled". The remedy: "Start back-up tasks as the job nears completion"; "First task to complete is considered". Hadoop calls it speculative execution (p. 818): "The framework can run multiple instances of slow tasks", "Output from instance that finishes first is used", switched by mapred.speculative.execution; it "Can dramatically bring in long tails on jobs". The losing copy is killed — "First one to complete wins, then any other runs are killed" (p. 802) — and the committer lets only the winner's file through.

One slow worker and the whole job

A Gantt chart of the tasks of one phase, map or reduce: one row per worker, one bar per copy of a task, from its start to its end. The Master hands the next idle task to whichever worker is free first. With speculative execution, a worker that finds no idle task left starts a back-up copy of the running task that will end last — once that task has run longer than a normal one — and the first copy to finish wins. The times are computed by this model, in units of one normal task; none is printed on the slides. The one thing to see: the job ends when its last task ends.

  1. Keep 12 tasks, 4 workers, the last worker 5 times slower, back-ups off. Predict when the job ends, then read the chart.
    What you should see

    At 5. The three fast workers finish 11 tasks by time 4, while worker 4 is still on its first task, T4, which takes 5 units. One slow machine decides the end of the whole job.

  2. Switch on speculative execution. Predict the new completion time, and the price.
    What you should see

    4. At time 3 no idle task is left, so the free worker 3 starts a back-up copy of T4; it finishes at 4 and wins, and the slow copy is killed. The price: 1 unit of extra machine time, 8 % of the job's 12 units of work.

  3. With back-ups on, set the slowdown to 10, then to 1. Predict both completion times.
    What you should see

    With 10, still 4 — without back-ups it would be 10: the longer the tail, the more a back-up saves. With 1, the job ends at 3 and no back-up starts, because no task runs longer than a normal one.

  4. Choose "task T1: s times more data", slowdown 5, back-ups on. Predict whether the back-up helps.
    What you should see

    No: the job still ends at 5. The back-up of T1 has the same 5 units of data to process, so it cannot overtake the first copy, and it wastes 2 units before being killed. Speculation cures slow machines, not skewed data — such as a reduce task that received a huge key.

Back-ups cure slow machines, not heavy tasks. A reducer that receives far more values than the others is slow on any machine, and so is its copy. One cause is the partitioning function: "hash(key) mod R" by default, it must be "well-balanced" — "If not, reduce tasks may delay completion time" (p. 825) — because it may pile several big keys on one reducer. Another is one huge key, which no partitioning function can split, since all its values must reach the same reducer; there a combiner, when the reduce function allows one, shrinks the key on the map side.

Skipping bad records

Some crashes repeat: "Map/Reduce tasks may fail on certain records due to bugs" (p. 827). "Ideally, debug and fix" — "Not possible if third-party code is buggy". So "When worker dies, Master is notified of the record", and "If more than one worker dies on the same record", the "Master re-schedules the task and asks to skip the record". One record lost out of billions is fine for statistics, not for a bank balance.

Failures are handled by re-running deterministic tasks — finished map tasks too, since their output was on the dead machine's disk — and slow tasks get a back-up copy near the end: the first copy to finish wins.

Tools, schedulers, and the end of disk locality

Page 814 lists "Some further handy tools": "Combiners", "Compression", "Counters", "Speculation". Two you have met. Compression (p. 816): "Compressing the outputs and intermediate data will often yield huge performance gains", and it "Can be specified via a configuration file or set programmatically". Compressing the map output shrinks exactly what the shuffle moves.

The compression settings of p. 816 (Hadoop 1 property names). The slide's mapred(.map)? means: with .map for the map output, without it for the job output.
WhatPropertyChoices
job outputmapred.output.compresstrue
map outputmapred.compress.map.outputtrue
typemapred(.map)?.output.compression.type"block": "Group of keys and values are compressed together" — "almost always best"; "record": "Each value is compressed individually"
codecmapred(.map)?.output.compression.codec"Default (zlib) - slower, but more compression"; "LZO - faster, but less compression"

Counters (p. 817): "Often Map/Reduce applications have countable events"; the "framework counts records in to and out of Mapper and Reducer", and you can add your own — malformed lines, skipped records — through the Reporter that map and reduce receive, with "nice names in a MyClass_Counter.properties file". Each task counts locally; the framework adds up the totals.

// declare two user counters, then add 1 to the first
static enum Counter {EVENT1, EVENT2};
reporter.incrCounter(Counter.EVENT1, 1);

Zero reduces (p. 820): "Frequently, we only need to run a filter on the input data", with "No sorting or shuffling required by the job". "Set the number of reduces to 0", and the "Output from maps will go directly to OutputFormat and disk": one file per map task, no shuffle at all.

Whose job runs first

A shared cluster runs many jobs, and something decides whose tasks get the free slots. "By default, Hadoop uses FIFO to schedule jobs" (p. 829): first submitted, first served, so a long job holds up every short one behind it. "Alternate scheduler options: capacity and fair".

The two alternatives to FIFO, in the words of pp. 830–831.
Capacity schedulerFair scheduler
by"Developed by Yahoo""Developed by Facebook"
jobs go to"queues"; "Jobs can be prioritized""Pools"; uncategorized jobs to "a default pool"
guarantee"Queues are allocated a fraction of the total resource capacity""Each pool assigned a guaranteed minimum share"
spare capacity"Free resources are allocated to queues beyond their total capacity""Excess capacity split between jobs"
also"No preemption once a job is running""Provides fast response times for small jobs"; pools set minimum map slots, reduce slots and a limit on running jobs

Capacity gives each organisation a slice of the cluster and lends it while idle, but a borrowed slot is not taken back while its task runs. Fair gives every running job its share over time, so a small job submitted behind a big one gets slots as soon as running tasks free them, instead of waiting for the big job to end.

The end of disk locality

"New Trend: Disk-locality Irrelevant" (p. 828). Moving the code to the disk "Assumes disk bandwidth exceeds network bandwidth". But "Network speeds fast improving", "Disk speeds have stagnated": a remote disk now costs about as much as a local one. "Next step: attain memory-locality" — keep the data in RAM between steps, the idea of Spark in lesson 38. The "Conclusion" (p. 833) sums up: an "Easy to use scalable programming model for large-scale data processing on clusters" that "Hides issues of parallelization, fault tolerance, data partitioning & load balancing" and "Achieves efficiency through disk-locality".

Back to <store, 3>

Could a cluster turn the four occurrences of store into 3? Not by losing a pair: a dead mapper's pairs are produced again by re-execution. Not with a combiner: <store, 2> and two <store, 1> still add up to 4. Not with two reducers: the partitioner sends every store pair to the same one. The framework only moves and groups what the mappers emit; its one exception, a bad record skipped after it crashed more than one worker (p. 827), drops a whole record, not one word. So the count can change only in your own code — and the Java mapper of p. 811 does change it: StringTokenizer keeps the full stop, and the job writes <store, 3> next to <store., 1>, as the first widget shows. The slide's own mapper output has four <store, 1>, so its 3 is a slip of the pen.

Exam-style questions

The 2026/27 rules do not list MapReduce among the topics of the practical part (Neo4j, MongoDB, Elasticsearch, Redis and SparkQL), so expect it in the theory part: open answers, true/false and multiple choice, where a wrong answer may cost points. No past paper has a MapReduce exercise, but the exam-guideline deck lists "map-reduce basics (be able to exemplify it)" and "Hadoop simple examples": the first two questions are of that kind, on the ER diagram of the exam of 12 Sep 2025.

Consider the scenario of the exam of 12 Sep 2025, which tracks farmers and their sales at local markets: Product (Product ID, Name, Quantity, Expiration Date) is sold at Market (Name, Location, Opening Time, Closing Time, Size) through the relationship Sell, with cardinality (1, N) on both sides. Each market stall logs the name of every product it sells, one word per sale, in a text file. Two stalls produced the files "milk cheese milk" and "cheese milk eggs", and each file is one input split.

Exemplify MapReduce with a word count over these files: give the output of each mapper, the input of the reducer after shuffle and sort (one reducer), and the output of the job. Then say how many pairs cross the network with and without a combiner.

Show model answer

Mapper 1 (split "milk cheese milk") emits one pair per word: <milk, 1> <cheese, 1> <milk, 1>. Mapper 2 (split "cheese milk eggs"): <cheese, 1> <milk, 1> <eggs, 1>.

Shuffle and sort: all pairs with the same key go to the same reducer, which receives them sorted by key and grouped: <cheese, [1, 1]> <eggs, [1]> <milk, [1, 1, 1]>.

Reduce (sum of the values of each key): <cheese, 2> <eggs, 1> <milk, 3>. Check: the counts add up to 6, the number of words, and there is one output pair per distinct word.

Without a combiner, 6 pairs cross the network. With the reducer used as combiner, mapper 1 sends <milk, 2> <cheese, 1> and mapper 2 sends <cheese, 1> <milk, 1> <eggs, 1>: 5 pairs. The reducer then receives <milk, [2, 1]> and still outputs <milk, 3>.

A complete answer shows three stages, each in its own form: one <word, 1> per occurrence (no counting in the mapper), groups <word, [values]> after shuffle and sort, one sum per key. Writing <milk, 2> as a map output is the classic mistake — it is a combiner's output.

Consider the scenario of the exam of 12 Sep 2025: People (Personal ID, Name, Surname, Age, Address, State) buy Products (Product ID, Name, Quantity, Expiration Date) through the relationship Buy, with attributes Buy Date and Amount. The purchases are exported as a large text file with one line per purchase: "PersonalID,ProductID,BuyDate,Amount".

Using the pseudo-code notation of the lecture, write map and reduce functions that compute, for each product, the total amount bought and the average amount per purchase. Can the reduce function also be used as the combiner? If not, write a combiner.

Show model answer
map(String key, String line):
    // key: offset of the line
    // line: "PersonalID,ProductID,BuyDate,Amount"
    (person, product, date, amount) = split(line, ",")
    EmitIntermediate(product, (ParseFloat(amount), 1));

combine(String product, Iterator pairs):
    // pairs: (partial sum, partial count)
    s = 0; c = 0
    for each (a, n) in pairs:
      s += a; c += n
    EmitIntermediate(product, (s, c));

reduce(String product, Iterator pairs):
    s = 0; c = 0
    for each (a, n) in pairs:
      s += a; c += n
    Emit(product, (s, s / c));

The map emits, for every purchase, the product as key and the pair (amount, 1) as value. The reduce adds up amounts and counts and emits the total and the average. The reduce cannot be the combiner: its output (total, average) has another meaning than its input (sum, count), and an average of partial averages is wrong when the parts differ in size — the average of 10 and 20 is 15, the average of 15 and 60 is 37.5, while the average of 10, 20 and 60 is 30. The combiner above emits partial (sum, count) pairs of the same type as the map output, so the framework may run it zero, one or several times without changing the result.

A "Hadoop simple example" in the style of p. 810: the key is the grouping attribute, the value carries what the reducer needs. The combiner trap — averages do not combine — is the point of the question; for the total alone the reducer itself could be the combiner.

True or false: when a worker fails, the map tasks it had already completed must be executed again, while the reduce tasks it had already completed are not re-executed.

  • True Completed map output lives on the failed worker's local disk and is lost; completed reduce output is already in the distributed file system (p. 821).
  • False The asymmetry is real: "In-progress and Completed map tasks are re-scheduled", "Completed reduce tasks are not re-scheduled" (p. 821).
Show model answer

True. A failure is detected by missing heartbeats. The completed map tasks of the failed worker stored their output on its local disk, which is no longer reachable, so they are re-scheduled together with the in-progress ones, and the workers executing reduce tasks are notified to read from the new location. Completed reduce tasks wrote their output to the distributed file system, so they are not re-executed.

The "why" is where the output lives: local disk for map output, the replicated file system for reduce output. A true/false question invites a careless "false" from anyone who reasons "completed means safe".

Which component of a MapReduce job decides to which reducer an intermediate key-value pair is sent?

  • The combiner The combiner pre-aggregates one mapper's output; it does not choose a reducer.
  • The input splitter It cuts the input into chunks for the mappers, before any intermediate pair exists.
  • The partitioner It "Decides which pairs are sent to which reducer", by default with Key.hashCode() % numOfReducers, that is hash(key) mod R (pp. 798, 825).
  • The output committer It commits the reduce output to a file, after the reducers have run.
Show model answer

The partitioner (also called the shuffler): by default it computes hash(key) mod R, so all pairs with the same key reach the same reducer. It can be replaced, for example to obtain a total order of the output or a more uniform load.

Each wrong option is a real component in the wrong place of the pipeline: know the order splitter, mapper, combiner, partitioner, reducer, committer.

What is a combiner in MapReduce? Why does it save network bandwidth, and which conditions must it satisfy to be used safely? Give one reduce function that can serve as a combiner and one that cannot.

Show model answer

A combiner is an optional "intermediate reducer" that runs on the output of each mapper, on the map side, before the shuffle. When the map output contains many repeated keys — in a word count, thousands of <the, 1> — it merges the values of each key locally, so far fewer pairs are written, sorted and sent over the network to the reducers. Conditions: its input and output types must be the same as the map output types, because the reducer must accept both combined and uncombined pairs; it must have no side effects, because the framework may run it zero, one or several times; and the aggregation must give the same final result however the values are grouped, as for sums, counts of ones, maximum and minimum. The word-count reducer can be the combiner (conf.setCombinerClass(Reduce.class)); a reducer that computes an average cannot, unless the values travel as (sum, count) pairs and the division happens only in the reducer.

Three conditions — same types, no side effects, an aggregation that does not care about grouping — and the two examples. The slide's own wording "intermdiate number of times" means an indeterminate number: write "zero, one or more times".

Explain the principle "move the code, not the data" in MapReduce. How does the Master use HDFS to schedule map tasks, and which data still crosses the network?

Show model answer

Transferring a peta-scale input from where it is stored to the machines that run the map function would saturate the network, while the map code is tiny; so the framework sends the code to the machines that store the data. The Master asks the HDFS NameNode where the chunks (blocks) of the input are and assigns each map task to a worker that holds a copy of its split, which then reads from its local disk; if no such worker is free, it uses one close to the data, for example on the same network switch. The number of map tasks follows the block size, one split per block, to exploit this locality. The input then does not cross the network, but the intermediate data still does, in the shuffle: every reducer reads its partition from every map worker. Combiners and compression of the map output reduce this traffic.

A full answer gives the mechanism (the Master asks the NameNode for block locations), the fallback (same switch) and the honest limit (the shuffle); stopping at the slogan leaves all three out.

Which of the following statements about MapReduce agree with the lecture? Select all that apply.

  • The number of map tasks M is specified by the user, while the number of reduce tasks R is determined by the system. The other way round: the system determines M from the splits, the user specifies R (p. 808).
  • Speculative execution runs extra copies of slow tasks near the end of a job, and the output of the copy that finishes first is used. pp. 818 and 824: back-up tasks as the job nears completion, first to complete wins.
  • By default, Hadoop schedules jobs with the Fair scheduler, developed by Facebook. The default is FIFO (p. 829); Fair and Capacity are the alternatives.
  • Each reduce task produces one output file. p. 823, and the reason why R also fixes the number of output files.
  • A combiner may change the types of the mapper output, since the reducer converts them back. "Cannot change the type of its input": its input and output types must be equal (p. 799).
Show model answer

The second and the fourth statements.

A select-all question where a wrong tick may cost points. The false options swap two roles (M and R), name the wrong default (FIFO, not Fair) and break the combiner's type rule.

Check yourself

A word-count mapper receives the line "the store the store". What does it emit?

  • <the, 2> <store, 2> That is a combiner's or a reducer's output: the mapper does not add anything up.
  • <the, 1> <store, 1> <the, 1> <store, 1> One pair per occurrence, in text order: the mapper only tags (p. 792).
  • <store, 2> Every word is emitted, the included; and counting is not the mapper's job.
  • Nothing until a reducer asks for it The mapper emits as it reads and writes to its local disk; reducers fetch the output later.

With R = 2 reduce tasks, where do the four <store, 1> pairs of the slide's sentence go?

  • Two to each reducer, to balance the load Then two files would hold partial counts and no file the 4.
  • To the reducer on the machine of the mapper that emitted them The partitioner looks only at the key, never at where the pair was produced.
  • All four to reduce task hash(store) mod 2 The partition depends on the key alone: "All inputs with the same key must go to the same reducer!" (p. 797).
  • To whichever reducer is free first Then one key could end up in two places; the partitioning function fixes the reducer in advance.

Which reduce functions can also serve, unchanged, as combiners? Select all that apply.

  • The sum of the values A sum of partial sums is the sum, and the types stay the same.
  • The maximum of the values The maximum of partial maxima is the maximum.
  • The average of the values An average of partial averages is wrong when the parts differ in size: 37.5 instead of 30 for 10, 20 and 60 split as (10, 20) and (60).
  • The number of values received As a combiner it would turn three pairs into one value 3, and the reducer would count that 3 as a single value. Emit 1s and sum them instead, as the word count does.

The test: applying the function to groups of values, and again to the results, must give the same answer — with the same types.

Worker W had completed map task 3 and reduce task 1 when it stopped answering heartbeats. What does the Master run again?

  • Map task 3 only; the reducers that still need its output are told where to fetch it Its output was on W's local disk; reduce task 1's file is already in the distributed file system (p. 821).
  • Nothing: both tasks had completed A completed map task's output dies with the machine's disk.
  • Both tasks Completed reduce tasks are not re-scheduled: their output is safe in the distributed file system.
  • Reduce task 1 only The other way round.

A job slows down at the end because one key holds half of all intermediate values. Will speculative execution help?

  • Yes: a back-up copy on a fast machine finishes first The copy has the same half of the values to process: it is just as slow.
  • Yes, if the slow reducer gets two back-ups More copies of the same work make none of them shorter.
  • No, because back-up tasks exist only for map tasks Back-ups apply to any slow last task, map or reduce (p. 802: "If a node is the last step").
  • No: the slowness is in the data, not in the machine; a combiner that shrinks the key on the map side helps, a new partitioning function does not All the values of one key must reach one reducer (p. 797), whatever the partitioning function; a combiner, when the reduce function allows one, merges them before the shuffle. A well-balanced partitioning function (p. 825) helps when several big keys land on one reducer, not against one huge key.

A filter runs as a job with zero reduces and M = 40 map tasks. How many output files does it write, and how many pairs are shuffled?

  • 1 file, all the pairs With no reducer there is nothing that gathers the output into one file.
  • No files and no pairs The map output is the result: "Output from maps will go directly to OutputFormat and disk" (p. 820).
  • 40 files, no pairs One file per map task, and "No sorting or shuffling required by the job" (p. 820).
  • 40 files, all the pairs The map tasks write their files themselves; nothing is shuffled.

Why does p. 828 call disk locality "irrelevant" as a new trend?

  • Because HDFS now keeps a copy of every block on every node Replication stays at a few copies; what changed is the speed of the network.
  • Because networks got much faster while disks did not, so a remote disk is about as fast as a local one; the next step is memory locality The slide's lines: "Network speeds fast improving", "Disk speeds have stagnated", "Next step: attain memory-locality".
  • Because map tasks now read their input through the Master The Master never touches the data; it only schedules.
  • Because disks became faster than networks The opposite: locality paid off while disk bandwidth exceeded network bandwidth.

Key take-aways

What to watch and read