Three desks, one of them empty
Riya's item revenue job works, but it has one reducer doing all the adding. “Let's use three,” says Kabir, and reruns it with -D mapreduce.job.reduces=3. Three output files appear. The first one is empty. The second holds four items, the third holds two. Then Riya writes a small reducer that lists the first three order IDs of each item, and every item shows the same ID three times. Two surprises, both caused by how a reducer really works.
Sir, three reducers and one of them did nothing? Isn't Hadoop supposed to divide the work equally?
Hadoop divides keys, not work. Each key goes to reducer hash(key) % 3. With only six keys, the dice fell four, two and zero. With crores of keys it evens out; with a few keys, or one huge key, it doesn't.
And the repeated IDs? I saved the values in a list, just like in any Java program.
The reducer gives you an Iterable that reads the values off a sorted stream, and it puts each one into the same Text object. You saved three references to one object, which ended up holding the last value. Copy what you keep. Today: how the reducer is called, how many to run, where its output goes, and these two traps.
The Reducer
In Hadoop MapReduce, a Reducer is the user class, extending org.apache.hadoop.mapreduce.Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>, whose reduce(key, Iterable<VALUEIN> values, Context context) method is called once for every distinct intermediate key in its partition, in sorted key order, with all the values for that key. A job has mapreduce.job.reduces reduce tasks (default 1); the Partitioner (by default HashPartitioner, (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks) decides which reduce task receives each key. Each reduce task runs setup() once, reduce() per key and cleanup() once, and writes its output through the job's OutputFormat to one file, part-r-NNNNN, which the output committer moves into the output directory when the task succeeds. The values are delivered as a single-pass iterator that reuses one value object, and their order within a key is not defined.
A reducer gets one key at a time, with every value for that key, and writes the answer for that key. The number of reducers decides how many output files you get, and the hash of each key decides which reducer it lands on.
Words you'll meet in this lesson
(hash & MAX_VALUE) % reduces.reduce()._temporary into place, writes _SUCCESS.From our lab (Hadoop 3.5.0, 3 workers, canteen_orders.csv)
| 3 reducers | part-r-00000: 0 bytes · part-r-00001: chai, dosa, samosa, vada pav (79 bytes) · part-r-00002: idli, poha (38 bytes) |
|---|---|
| 10 reducers | 10 files, 4 of them empty, one item per non-empty file; “Launched reduce tasks=11” (one speculative reduce attempt) |
| 0 reducers | Map-only: part-m-00000, 2,699,986 bytes of unsorted item amount lines, no shuffle at all |
| Object reuse | Copying values: chai [104221, 150350, 113456] · keeping references: chai [113456, 113456, 113456] |
The Reducer at three levels
Beginner Counting desks
- On counting day each desk is responsible for some candidates. A rule fixed in advance says which desk gets which candidate, so every slip for a candidate reaches the same desk.
- A desk takes one candidate's pile at a time, adds it up, writes one line, and moves to the next candidate in alphabetical order.
- With few candidates, some desks may get none. That desk still opens, and still files an empty result sheet.
Intermediate How reduce() is called
setup(context)runs once, thenreduce(key, values, context)once per key, in sorted key order, thencleanup(context)once.valuesis anIterableover a merged, sorted stream on disk and in memory. You can loop over it once; there is no going back.- Each step of the loop overwrites the same
TextorIntWritableobject. Use the value immediately, or copy it (new Text(v),v.get()) if you keep it. - The order of values inside one key is not guaranteed: our correct run listed chai's IDs as 104221, 150350, 113456. If you need them ordered, that is a secondary sort (Lesson 9.7).
Advanced How many reducers, and what they write
- Default is 1. Set it with
job.setNumReduceTasks(n)or-D mapreduce.job.reduces=n. Hadoop's own guideline: about 0.95 or 1.75 × (nodes × containers per node): 0.95 finishes in one wave, 1.75 lets fast nodes take a second wave for better balance. - Too few: each reducer is huge and slow. Too many: many tiny output files (Lesson 7.5) and task overhead.
- Partitioning is by key, so one hot key (say 60% of all orders are chai) goes to one reducer no matter how many you add. Fixes: combiners (9.1), custom partitioners (9.2), salting keys.
- Each reduce task writes to
_temporary/…/attempt_…; when it succeeds the FileOutputCommitter (algorithm version 2 in our logs) moves the file into the output folder. After all tasks,_SUCCESSis written. Output goes to HDFS with normal replication (3 here). - With 0 reducers there is no shuffle and no sort: map output is written straight to HDFS as
part-m-NNNNN.
Exam tip: For “What is the role of the Reducer?” write: receives each key with all its values (after shuffle and sort), called once per key in sorted order, output written to HDFS as part-r-NNNNN, number set by setNumReduceTasks (default 1), keys assigned by the Partitioner (hash % n). Mention that 0 reducers gives a map-only job.
Choosing the number of reducers
| Reducers | Output | Shuffle and sort | Use when | Our lab |
|---|---|---|---|---|
| 0 | part-m-* (one per map) | None | Filtering, cleaning, format conversion | part-m-00000, 2.7 MB unsorted |
| 1 (default) | One sorted file | Yes | Small results, a single globally sorted file | part-r-00000, 6 lines |
| A few | part-r-00000 … n−1 | Yes | Moderate output; each file sorted on its own | 3: sizes 0, 79, 38 bytes |
| ≈ 0.95–1.75 × containers | Many files | Yes | Big aggregations with many keys | 10: four files empty (only 6 keys) |
What a reducer typically does
Aggregate
Count, sum, min, max, average per key. Our ItemRevenue: orders and revenue per item.
most commonCollect
Build a list per key: an inverted index, first N order IDs. Copy each value you keep.
watch memoryCombine two sides
Reduce-side join: records from two inputs meet under the same key (Lesson 9.5).
joinsIdentity
Write values unchanged: used just to get the data partitioned and sorted by key.
sortingThe output of a job with n reducers is n files, each sorted, but not sorted across files: our 3-reducer run has chai in file 1 and idli in file 2. For one globally sorted result use 1 reducer, or TotalOrderPartitioner, which gives each reducer a key range instead of a hash.
Reducers on a real cluster
All runs use the Module 8 lab and reducer_lab.sh (in “Run it on your PC”). Input: canteen_orders.csv, 3 lakh orders, 6 items.
The reducer we are running
The four type parameters: input key Text, input value IntWritable (they must match the map output), output key Text, output value Text.public static class SumReducer extends Reducer<Text, IntWritable, Text, Text> { protected void reduce(Text item, Iterable<IntWritable> amounts, Context ctx) … { long orders = 0, revenue = 0; for (IntWritable a : amounts) { orders++; revenue += a.get(); } ctx.write(item, new Text(orders + "\t" + revenue)); } }a.get()copies the number out at once, so object reuse does no harm here.Run with 3 reducers
The default HashPartitioner sent chai, dosa, samosa and vada pav to partition 1, idli and poha to 2, and nothing to 0. Reducer 0 still started, found no keys and wrote an empty file. Each file is sorted on its own.hadoop jar mr.jar ItemRevenue -D mapreduce.job.reduces=3 /user/hadoop/data/canteen_orders.csv /out/red_3Launched reduce tasks=3 Reduce input groups=6 Reduce output records=6 part-r-00000 (0 bytes): part-r-00001 (79 bytes): chai dosa samosa vada pav part-r-00002 (38 bytes): idli pohaCheck the partitions yourself
Exactly the files Hadoop wrote. The partition depends only on the key and the number of reducers, so the same key always lands on the same reducer.def text_hash(s): # Hadoop Text.hashCode() h = 1 for b in s.encode(): h = (31 * h + b) & 0xffffffff return h for item in ["chai", "dosa", "idli", "poha", "samosa", "vada pav"]: print(item, (text_hash(item) & 0x7fffffff) % 3)chai 1 dosa 1 idli 2 poha 2 samosa 1 vada pav 1Run with 10 reducers
Ten tasks, ten files, four empty: you can never use more reducers than you have keys. “Launched reduce tasks=11” is one extra speculative attempt, the same mechanism we saw for maps in Lesson 8.1.hadoop jar mr.jar ItemRevenue -D mapreduce.job.reduces=10 /user/hadoop/data/canteen_orders.csv /out/red_10Launched reduce tasks=11 part-r-00000 (0 bytes): part-r-00001 (0 bytes): part-r-00002 (22 bytes): vada pav part-r-00003 (0 bytes): part-r-00004 (19 bytes): dosa part-r-00005 (19 bytes): poha part-r-00006 (18 bytes): chai part-r-00007 (20 bytes): samosa part-r-00008 (0 bytes): part-r-00009 (19 bytes): idliRun with 0 reducers
The reducer class is ignored. Map output goes straight to HDFS, in input order, one pair per order: the shuffle, sort and reduce simply don't happen.hadoop jar mr.jar ItemRevenue -D mapreduce.job.reduces=0 /user/hadoop/data/canteen_orders.csv /out/red_0 hdfs dfs -cat /out/red_0/part-m-00000 | head -3Launched map tasks=1 2699986 /out/red_0/part-m-00000 samosa 18 vada pav 20 poha 25The object-reuse trap
The iterator reuses onefor (Text id : ids) { first3.add(copy ? new Text(id) : id); // without a copy: the SAME object 3 times if (first3.size() == 3) break; }-- copy (correct) -- no copy (-D sample.copy=false) chai [104221, 150350, 113456] chai [113456, 113456, 113456] dosa [78778, 213441, 200658] dosa [200658, 200658, 200658] idli [101007, 50991, 101001] idli [101001, 101001, 101001]Text; after three steps it holds the third ID, and all three list entries point to it. Notice too that the correct IDs are not in order: values arrive in no defined order within a key.
Never collect all values of a key into a list unless you know the key is small. A hot key with crores of values will exhaust the reducer's heap (ours had 300 MB). Aggregate as you iterate, or keep only what you need.
Three reducers and one reused object
Six steps: keys are partitioned, each reducer is called per key, files are committed, then the object-reuse bug in slow motion.
- The job is submitted with three reduce tasks.
- All three reducers finish; together they saw 6 key groups.
- Three output files: one is empty.
- File 1 holds four items, sorted by key.
- The sampling reducer without copies prints the same ID three times per item.
- With a copy of each value, the three IDs are different (and in no particular order).
- Every map output pair gets a partition number from the hash of its key.
- Each reducer fetches only its own partition from every map: reducer 0's partition is empty.
- Each reducer calls reduce() once per key, in sorted key order.
- Each task writes one file under _temporary; the committer moves it into /out/red_3 and _SUCCESS is written.
- The iterator fills ONE Text object again and again; a list of references sees only the last value.
- Copying each value keeps three different objects.
Reducers at work
1. Sizing reducers for nightly aggregates
Teams set the reducer count from cluster capacity and output size, aiming for output files of a few hundred MB each, so the result is neither one giant file nor thousands of tiny ones.
2. Hunting skewed reducers
When a job sits at “reduce 99%” for an hour, one reducer usually has a hot key (a default city, a null customer ID). Engineers find it from the reducer counters and fix it with combiners, salting or a custom partitioner.
3. Map-only ETL
Cleaning, filtering and format conversion jobs set reducers to 0 and skip the whole shuffle, often halving the run time. DistCp is a map-only job too.
4. Code reviews catch object reuse
“Copy Writables you keep” is a standard review check in Hadoop teams; the same reuse appears in Hive UDFs and in Spark when reading Hadoop input formats.
Simplified; organisations differ in tools and policies.
Partition, group and reduce like Hadoop
This model deals keys to reducers with Hadoop's real Text hash, groups values per key, and calls a reduce function once per key in sorted order. Its partitions match the files our cluster wrote.
pairs = [("samosa", 18), ("vada pav", 20), ("poha", 25), ("samosa", 18), ("chai", 15),
("idli", 30), ("dosa", 45), ("chai", 15), ("poha", 25), ("idli", 30)]
def text_hash(s): # Hadoop's Text.hashCode()
h = 1
for b in s.encode():
h = (31 * h + b) & 0xffffffff
return h
def partition(key, n): # HashPartitioner
return (text_hash(key) & 0x7fffffff) % n
def reduce_fn(key, values):
return f"{key}\t{len(values)}\t{sum(values)}"
R = 3
parts = [{} for _ in range(R)]
for k, v in pairs:
parts[partition(k, R)].setdefault(k, []).append(v)
for r in range(R):
lines = [reduce_fn(k, parts[r][k]) for k in sorted(parts[r])]
print(f"part-r-{r:05d}:", lines if lines else "(empty)")return (text_hash(key) & 0x7fffffff) % nThe HashPartitioner: clear the sign bit, then modulo the number of reducers. Same key, same reducer, always.lines = [reduce_fn(k, parts[r][k]) for k in sorted(parts[r])]Each reducer handles its keys in sorted order, one reduce() call per key.print(f"part-r-{r:05d}:", lines if lines else "(empty)")Reducer 0 gets no keys but still writes a file: our part-r-00000 was 0 bytes.
Run it on your PC: number of reducers and the reuse trap
Use the Module 8 lab from Lesson 8.1, with canteen_orders.csv in HDFS and mr.jar built from ItemRevenue.java and SampleOrders.java. The script runs ItemRevenue with 3, 10 and 0 reducers, lists every output file with its size and keys, then runs SampleOrders with and without copying values. It takes about 3 minutes.
First three order IDs per item
// SampleOrders.java: for each item, list the first 3 order ids the reducer sees.
// Run with -D sample.copy=false to see the famous "object reuse" bug.
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
public class SampleOrders {
public static class M extends Mapper<LongWritable, Text, Text, Text> {
private final Text item = new Text(), id = new Text();
@Override
protected void map(LongWritable k, Text line, Context ctx) throws IOException, InterruptedException {
String[] f = line.toString().split(",");
if (f.length != 3 || f[0].equals("order_id")) return;
item.set(f[1]); id.set(f[0]);
ctx.write(item, id);
}
}
public static class R extends Reducer<Text, Text, Text, Text> {
@Override
protected void reduce(Text item, Iterable<Text> ids, Context ctx) throws IOException, InterruptedException {
boolean copy = ctx.getConfiguration().getBoolean("sample.copy", true);
List<Text> first3 = new ArrayList<>();
for (Text id : ids) {
first3.add(copy ? new Text(id) : id); // without a copy we keep the SAME object 3 times
if (first3.size() == 3) break;
}
ctx.write(item, new Text(first3.toString()));
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
String[] rest = new GenericOptionsParser(conf, args).getRemainingArgs();
Job job = Job.getInstance(conf, "sample orders");
job.setJarByClass(SampleOrders.class);
job.setMapperClass(M.class);
job.setReducerClass(R.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
FileInputFormat.addInputPath(job, new Path(rest[0]));
FileOutputFormat.setOutputPath(job, new Path(rest[1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
// Inside the namenode container, in /tmp/mr:
// javac -cp $(hadoop classpath) -d classes *.java && jar cf mr.jar -C classes .
// hadoop jar mr.jar SampleOrders /user/hadoop/data/canteen_orders.csv /out/sample_ok
// hadoop jar mr.jar SampleOrders -D sample.copy=false /user/hadoop/data/canteen_orders.csv /out/sample_bugReal output. In the buggy run every list holds the last ID that the reused object contained. Your IDs may differ (value order within a key is not defined), but the bug pattern is the same.
3, 10 and 0 reducers, and the reuse trap
# reducer_lab.sh: number of reducers = number of output files; which key goes where; the object-reuse trap
cd /tmp/mr
D=/user/hadoop/data/canteen_orders.csv
for r in 3 10; do
hdfs dfs -rm -r -f -skipTrash /out/red_$r > /dev/null 2>&1
hadoop jar mr.jar ItemRevenue -D mapreduce.job.reduces=$r $D /out/red_$r 2>&1 | grep -E "Launched reduce tasks|Reduce input groups|Reduce output records"
echo "== $r reducers: output files"
for f in $(hdfs dfs -ls /out/red_$r | grep part-r | awk '{print $8}'); do
echo "$(basename $f) ($(hdfs dfs -stat %b $f) bytes): $(hdfs dfs -cat $f | cut -f1 | tr '\n' ' ')"
done
done
echo "== 0 reducers (map-only): mapper output written directly, unsorted"
hdfs dfs -rm -r -f -skipTrash /out/red_0 > /dev/null 2>&1
hadoop jar mr.jar ItemRevenue -D mapreduce.job.reduces=0 $D /out/red_0 2>&1 | grep -E "Launched (map|reduce)"
hdfs dfs -ls /out/red_0 | grep part | awk '{print $5, $8}'
hdfs dfs -cat /out/red_0/part-m-00000 2>/dev/null | head -3
echo "== first 3 order ids per item, copying each value (correct)"
hdfs dfs -rm -r -f -skipTrash /out/sample_ok /out/sample_bug > /dev/null 2>&1
hadoop jar mr.jar SampleOrders $D /out/sample_ok > /dev/null 2>&1
hdfs dfs -cat /out/sample_ok/part-r-00000
echo "== the same job WITHOUT copying (-D sample.copy=false)"
hadoop jar mr.jar SampleOrders -D sample.copy=false $D /out/sample_bug > /dev/null 2>&1
hdfs dfs -cat /out/sample_bug/part-r-00000
# Copy into the namenode container and run:
# docker cp reducer_lab.sh lab8-namenode-1:/tmp/mr/
# docker exec lab8-namenode-1 bash /tmp/mr/reducer_lab.shReal output, unedited. The empty files and their positions follow directly from the hash of each item name; “Launched reduce tasks=11” for 10 reducers includes one speculative attempt.
How a reducer walks through its keys
A reducer's input is one sorted stream of (key, value) pairs. Grouping means: keep reading while the key stays the same, then call reduce for that key.
Worked examples: real problems, solved and traced in memory
Three reducer problems: choosing the count, predicting output files, and spotting skew.
How many reducers for this cluster?
Problem statement
A cluster has 20 workers, each able to run 6 reduce containers at once. Using Hadoop's guideline of 0.95 or 1.75 × total containers, what are the two candidate reducer counts?
20 nodes × 6 containers
one wave : 114 two waves: 210
Approach
- Total containers = nodes × containers per node.
- 0.95 × total: all reducers start in one wave.
- 1.75 × total: two waves, better load balancing.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Which file will each item land in?
Problem statement
Using Hadoop's Text hash and the HashPartitioner, predict the output file of each item for 4 reducers.
6 items · 4 reducers
part-r-00000 [] part-r-00001 ['idli', 'poha'] part-r-00002 ['chai', 'dosa', 'vada pav'] part-r-00003 ['samosa']
Approach
- Take each item's Text hash with the sign bit cleared (h = 31h + byte, 32-bit).
- Take the remainder by 4.
- Group the items by remainder.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Why does one reducer take an hour?
Problem statement
A job with 50 reducers shows 49 reducers finishing in 2 minutes and one running for 60. The reducer counters show “Reduce input records” of about 2 lakh for most tasks and 9 crore for the slow one. What is the cause, and roughly how much faster could the job be if the load were spread evenly?
49 × 2 lakh records · 1 × 9 crore records · 60 min
hot reducer has 450x the normal load even spread: 1,996,000 records each, about 1.3 min
Approach
- The slow reducer has about 450× more records: one hot key.
- Time is roughly proportional to records.
- Even spread = total records / 50 per reducer.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Check yourself
Quick revision
Lesson 8.5 in one screen
Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>: setup once, reduce(key, values, ctx) once per key in sorted order, cleanup once.- Keys go to reducers by HashPartitioner:
(hash & MAX_VALUE) % reduces; same key → same reducer. - Default 1 reducer; set with
setNumReduceTasksor-D mapreduce.job.reduces; guideline 0.95 or 1.75 × containers. - Lab: 3 reducers → 0, 79, 38 bytes; 10 reducers → 4 empty files; 0 reducers → part-m-00000, unsorted.
- Values are a one-pass iterator over a reused object: aggregate as you go, copy anything you keep.
- Value order within a key is undefined; each part file is sorted, not the whole output.
- Output: part-r-NNNNN via the output committer (_temporary → final), then _SUCCESS; replicated in HDFS.
- Skew: one hot key = one slow reducer; more reducers don't help.
Interview and viva questions
What does the Reducer do in Hadoop MapReduce?
After the shuffle and sort, each reduce task receives a partition of the intermediate keys. For each key, in sorted order, Hadoop calls reduce(key, values, context) with all the values for that key. The reducer typically aggregates them and writes output records, which go to one file per reduce task (part-r-NNNNN) in HDFS.
How do I set the number of reducers?
In the driver call job.setNumReduceTasks(n), or pass -D mapreduce.job.reduces=n on the command line when the driver uses GenericOptionsParser or ToolRunner. The default is 1. The Hadoop documentation suggests 0.95 or 1.75 times (nodes × maximum containers per node); in practice also aim for output files of reasonable size.
Why are some reducer output files empty?
Keys are assigned to reducers by the partitioner, by default (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks. If no key hashes to a given partition, that reducer receives nothing but still writes its (empty) part file. In our lab, 6 keys and 10 reducers left 4 files empty.
Why do I get the same value repeated when I store reducer values in a list?
Hadoop reuses a single Writable instance for the values of the Iterable and refills it at every step. Storing the object stores a reference to that one instance, which ends up holding the last value. Store a copy instead, for example new Text(value) or the primitive from value.get().
Can a MapReduce job have zero reducers?
Yes. With setNumReduceTasks(0) the job is map-only: there is no shuffle or sort, and each map task writes its output directly to the output directory as part-m-NNNNN. This is used for filtering, cleaning and conversion jobs.
Practice problems
Reducer exercises using the real files and counters from our lab.
Bytes per file
part-r-00001 (chai, dosa, samosa, vada pav) is 79 bytes and part-r-00002 (idli, poha) is 38. Check 79 by building the four lines.
4 output lines
79 bytes
Show solution and explanation
lines = ["chai\t50170\t752550", "dosa\t49948\t2247660", "samosa\t50021\t900378", "vada pav\t49986\t999720"]
print(sum(len(l) + 1 for l in lines), "bytes")How it works: 79: each line plus its newline character.
Empty files
With 6 keys and 10 reducers, how many files must be empty at the very least?
6 keys · 10 reducers
4 at least; more if two keys share a reducer
Show solution and explanation
print(10 - 6, "at least; more if two keys share a reducer")How it works: 4, which is exactly what we got: every item happened to land on a different reducer.
Reducer input
The reducer saw Reduce input records=300000 and Reduce input groups=6. How many values per reduce() call, on average?
300,000 / 6
50000.0
Show solution and explanation
print(300000 / 6)How it works: 50,000 values per call: each item has about 50,000 orders.
Single pass
Write a reducer that returns count, sum and max of a key's values in ONE loop over an iterator.
iterator of amounts
('dosa', 4, 175, 45)Show solution and explanation
def reduce_fn(key, values):
n = s = 0
m = None
for v in values:
n += 1; s += v
m = v if m is None or v > m else m
return key, n, s, m
print(reduce_fn("dosa", iter([45, 45, 40, 45])))How it works: One pass is all you get; keep running totals instead of a list.
Group a sorted stream
Given a sorted stream of (key, value), produce one line per key with the sum, without building a dictionary.
sorted pairs
chai 30 idli 30 poha 50
Show solution and explanation
from itertools import groupby
stream = [("chai", 15), ("chai", 15), ("idli", 30), ("poha", 25), ("poha", 25)]
for key, group in groupby(stream, key=lambda kv: kv[0]):
print(key, sum(v for _, v in group))How it works: This is how Hadoop groups: the input is sorted, so equal keys are next to each other.
Partition a new item
The canteen adds “upma”. With 3 reducers, which file will it go to?
“upma”, 3 reducers
part-r-00001
Show solution and explanation
h = 1
for b in "upma".encode():
h = (31 * h + b) & 0xffffffff
print("part-r-%05d" % ((h & 0x7fffffff) % 3))How it works: The hash decides; adding one key never moves the other items.
Files per day
A daily job uses 400 reducers and writes 8 GB. A team keeps the output for a year. How many files and what average size? Suggest a better reducer count for files of about 256 MB.
400 reducers · 8 GB/day · 365 days
146,000 files a year, 20 MB each better reducer count: 32
Show solution and explanation
files = 400 * 365
print(f"{files:,} files a year, {8 * 1024 / 400:.0f} MB each")
print("better reducer count:", 8 * 1024 // 256)How it works: 1,46,000 files of 20 MB a year; 32 reducers would give 256 MB files and about 11,700 files (Lesson 7.5).
Salting a hot key
chai has 9 lakh records and the other keys 50,000 each. Salt chai into 4 sub-keys (chai#0 … chai#3) and show the load per reducer drops, then combine the salted totals.
record counts per key
largest group before: 900000 after: 225000 chai recombined: 900000
Show solution and explanation
counts = {"chai": 900_000, "dosa": 50_000, "idli": 50_000, "poha": 50_000}
salted = {}
for k, c in counts.items():
parts = 4 if k == "chai" else 1
for i in range(parts):
key = f"{k}#{i}" if parts > 1 else k
salted[key] = c // parts
print("largest group before:", max(counts.values()), "after:", max(salted.values()))
total_chai = sum(v for k, v in salted.items() if k.startswith("chai#"))
print("chai recombined:", total_chai)How it works: The biggest reduce group drops from 9 lakh to 2.25 lakh records; a small second step adds chai#0 … chai#3 back to 9 lakh.
Real-world scenario questions (asked in company interviews)
Reducer questions test whether you have actually written MapReduce code: partitioning, reducer count and object reuse come up again and again.
“How does Hadoop decide which reducer gets a key?”
A standard follow-up after explaining word count.
The interviewer is checking: the Partitioner and its guarantee.
Show a strong answer
- The Partitioner; by default HashPartitioner: (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks.
- Guarantee: all values of one key go to the same reducer, so reduce sees the complete group.
- Side effect: load depends on key distribution; few keys or a hot key give uneven reducers (we saw files of 0, 79 and 38 bytes).
A reducer collects values into a list and all entries are identical
Code review of a reducer that builds a list of the last 10 transactions per customer.
The interviewer is checking: knowledge of Writable reuse.
Show a strong answer
- Hadoop reuses one key and one value object while iterating; the list stores references to the same instance.
- Fix: add a copy (new Text(v), WritableUtils.clone, or extract primitives).
- Also check memory: collecting all values of a hot key can exhaust the heap; keep only what is needed.
The job is stuck at “reduce 99%” for an hour
All but one reducer finished quickly.
The interviewer is checking: skew diagnosis and fixes.
Show a strong answer
- Compare “Reduce input records” and shuffle bytes per reduce task; one task with far more input means a hot key.
- Find the key (sample the map output or count keys in a quick job); often null or a default value.
- Fix: filter or handle the junk key, use a combiner, salt the hot key and merge in a second step, or a custom partitioner. More reducers alone won't help.
“I need one sorted output file. Do I set 1 reducer?”
An analyst wants a single report file sorted by customer.
The interviewer is checking: trade-offs between one reducer and total order.
Show a strong answer
- 1 reducer works for small outputs, but all data passes through one task: slow for big outputs.
- For large data use TotalOrderPartitioner (sampled key ranges), so files 0 … n−1 are in order and can be concatenated.
- Or keep many reducers and sort/merge downstream (getmerge only concatenates, it does not sort).