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.
Before: one knife, one blender (p. 779)
After: "A parallel version of the process" (p. 786)
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":
- "Read a lot of data"
- "Map: extract something you care about from each record"
- "Shuffle and Sort"
- "Reduce: aggregate, summarize, filter, or transform"
- "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.
| Reducer input | Reducer output | Printed 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 keys | 12 pairs, counts sum to 21 | 11 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":
- \(\langle k_i, v_i\rangle\)
- one input pair, one record:
Q1and 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.
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.
| Part | The 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:
- \(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.
Before: "Transfer peta-scale data through network" (p. 805)
After: "MapReduce on HDFS" (p. 807)
"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.
Fork. The user program starts copies of itself on the cluster; one becomes the Master, the others workers. Someone must coordinate; everyone else computes.
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.
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.
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.
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.
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.
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".
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.
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.
mapred(.map)? means: with .map for the map output, without it for the job output.| What | Property | Choices |
|---|---|---|
| job output | mapred.output.compress | true |
| map output | mapred.compress.map.output | true |
| type | mapred(.map)?.output.compression.type | "block": "Group of keys and values are compressed together" — "almost always best"; "record": "Each value is compressed individually" |
| codec | mapred(.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".
| Capacity scheduler | Fair 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.
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?
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.
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?
With R = 2 reduce tasks, where do the four <store, 1> pairs of the slide's sentence go?
Which reduce functions can also serve, unchanged, as combiners? Select all that apply.
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?
A job slows down at the end because one key holds half of all intermediate values. Will speculative execution help?
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?
Why does p. 828 call disk locality "irrelevant" as a new trend?
Key take-aways
- MapReduce is "a programming model and an associated implementation for processing and generating large data sets" (p. 783): you write map and reduce over key-value pairs, the framework takes care of parallelization, fault tolerance, data distribution and load balancing — and you scale out, not up.
- Map emits pairs from each record without aggregating; shuffle and sort brings all the values of a key to one reducer, in key order; reduce aggregates them. In the slide's word count
<store, [1, 1, 1, 1]>becomes<store, 4>, not the printed 3: the counts must add up to the number of map pairs. - A job's parts are the input splitter (64 MB chunks on the slide, 128 MB today), the mapper, the optional combiner (same types in and out, no side effects, run zero or more times), the partitioner (hash(key) mod R), the reducer, the output committer and the Master; it needs at least six Writables.
- On a cluster the code moves, not the data: the Master asks the HDFS NameNode where the chunks are and runs each map task on a node that stores its split, or near it; only the shuffle crosses the network. The system sets M from the splits, the user sets R, and each reduce task writes one output file.
- A dead worker's in-progress and completed map tasks are re-run, its completed reduce tasks are not, because map output sits on the worker's local disk and reduce output in the replicated file system; slow tasks get back-up copies near the end, and the first copy to finish wins; a record that crashes more than one worker is skipped. Back-ups cure slow machines, not skewed keys.
- Compression, counters and zero reduces are the other tools; FIFO is the default job scheduler, Capacity (Yahoo) and Fair (Facebook) the alternatives; and faster networks make disk locality less relevant — the next step is memory locality.
What to watch and read
-
The MapReduce Computational Model
Mining of Massive Datasets (Leskovec, Rajaraman, Ullman)22 min
The model drawn on a whiteboard at the level of this lesson — map tasks, grouping by key, reduce tasks, the word count — a calm first contact before the slides' juice factory.
-
MapReduce Explained: Interactive Simulator
Rahul Kumar
Step a word count through map, shuffle and reduce and watch the pairs move; open it next to the first widget. Use the simulator and its WordCount; skip the interview-preparation text around it.
-
mrjob: Writing jobs
the mrjob project
A word count with mapper, combiner and reducer in a dozen lines of Python that runs on your laptop without Hadoop. The combiner and the reducer are the same two lines, as p. 815 says.
-
MapReduce: Simplified Data Processing on Large Clusters
Jeffrey Dean, Sanjay Ghemawat, OSDI 2004
The source of most of these slides: its sections 3 and 4 hold the execution figure of p. 812, fault tolerance, locality, task granularity, backup tasks and the refinements, in the same order and with the same headings.
-
Lecture 1: Introduction (MapReduce)
Robert Morris, MIT 6.824 Distributed Systems
The lecturer draws the word count, then asks what happens when a worker dies, when the master dies, when a task is slow — the questions of section 5, answered aloud.