The Reducer

The reducer is where the answer for each key is computed and written. This lesson shows exactly how it is called, how keys are dealt to reducers, what the number of reducers does to your output, and the one Java habit (copying values) that every Hadoop programmer learns the hard way, all with real runs on the Module 8 cluster.

Module 8 · MapReduce fundamentals⏱ 26 minIntermediatePartitions reproduced in PythonObject-reuse bug caught liveRuns in your browser (Python)8 practice problems
1 The story

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.

6 keys from the maps chai · dosa · idli poha · samosa · vada pav partition = hash(key) % 3 reducer 0nothing: part-r-00000 is 0 bytes reducer 1 (79 bytes)chai · dosa · samosa · vada pav reducer 2 (38 bytes)idli · poha first 3 ids, no copy: chai [113456, 113456, 113456] same object 3 times Real lab output. Keys are dealt to reducers by hash, not evenly; and the reducer hands you one reused value object. myphatshala.in
Riya

Sir, three reducers and one of them did nothing? Isn't Hadoop supposed to divide the work equally?

Arjun Sir

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.

Kabir

And the repeated IDs? I saved the values in a list, just like in any Java program.

Arjun Sir

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.

2 Definition

The Reducer

Formal definition (write this in exams)

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.

In simple words

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

Reduce taskOne reducer instance; handles one partition and writes one file.
PartitionThe share of keys a reduce task receives; numbered 0 … reduces − 1.
HashPartitionerDefault rule: (hash & MAX_VALUE) % reduces.
Reduce input groupOne key with its values: one call of reduce().
Object reuseThe framework fills the same key and value objects again and again.
part-r-NNNNNOutput file of reduce task NNNNN (part-m- for map-only jobs).
Output committerMoves a task's output from _temporary into place, writes _SUCCESS.
SkewSome reducers get far more data than others.

From our lab (Hadoop 3.5.0, 3 workers, canteen_orders.csv)

3 reducerspart-r-00000: 0 bytes · part-r-00001: chai, dosa, samosa, vada pav (79 bytes) · part-r-00002: idli, poha (38 bytes)
10 reducers10 files, 4 of them empty, one item per non-empty file; “Launched reduce tasks=11” (one speculative reduce attempt)
0 reducersMap-only: part-m-00000, 2,699,986 bytes of unsorted item amount lines, no shuffle at all
Object reuseCopying values: chai [104221, 150350, 113456] · keeping references: chai [113456, 113456, 113456]

The Reducer at three levels

Pick your level. New to this? Start at Beginner and climb.

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, then reduce(key, values, context) once per key, in sorted key order, then cleanup(context) once.
  • values is an Iterable over 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 Text or IntWritable object. 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, _SUCCESS is 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.

3 Types and ways to use it

Choosing the number of reducers

ReducersOutputShuffle and sortUse whenOur lab
0part-m-* (one per map)NoneFiltering, cleaning, format conversionpart-m-00000, 2.7 MB unsorted
1 (default)One sorted fileYesSmall results, a single globally sorted filepart-r-00000, 6 lines
A fewpart-r-00000 … n−1YesModerate output; each file sorted on its own3: sizes 0, 79, 38 bytes
≈ 0.95–1.75 × containersMany filesYesBig aggregations with many keys10: 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 common

Collect

Build a list per key: an inverted index, first N order IDs. Copy each value you keep.

watch memory

Combine two sides

Reduce-side join: records from two inputs meet under the same key (Lesson 9.5).

joins

Identity

Write values unchanged: used just to get the data partitioned and sorted by key.

sorting

The 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.

4 Step by step

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.

  1. The reducer we are running

    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));
      }
    }
    The four type parameters: input key Text, input value IntWritable (they must match the map output), output key Text, output value Text. a.get() copies the number out at once, so object reuse does no harm here.
  2. Run with 3 reducers

    hadoop jar mr.jar ItemRevenue -D mapreduce.job.reduces=3 /user/hadoop/data/canteen_orders.csv /out/red_3
    Launched 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 poha
    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.
  3. Check the partitions yourself

    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 1
    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.
  4. Run with 10 reducers

    hadoop jar mr.jar ItemRevenue -D mapreduce.job.reduces=10 /user/hadoop/data/canteen_orders.csv /out/red_10
    Launched 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): idli
    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.
  5. Run with 0 reducers

    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 -3
    Launched map tasks=1 2699986 /out/red_0/part-m-00000 samosa 18 vada pav 20 poha 25
    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.
  6. The object-reuse trap

    for (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]
    The iterator reuses one 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.

5 See it happen

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.

Inside the reduce phase 6 steps
What you see hadoop@namenode : /tmp/mr $ hadoop jar mr.jar ItemRevenue \ -D mapreduce.job.reduces=3 …Launched reduce tasks=3 map 100% reduce 100%Reduce input groups=6Reduce output records=6 $ hdfs dfs -ls /out/red_3_SUCCESSpart-r-00000 0 bytespart-r-00001 79 bytespart-r-00002 38 bytes $ hdfs dfs -cat /out/red_3/part-r-00001chai 50170 752550dosa 49948 2247660samosa 50021 900378vada pav 49986 999720 $ … SampleOrders -D sample.copy=falsechai [113456, 113456, 113456]dosa [200658, 200658, 200658] $ … SampleOrders (copies each value)chai [104221, 150350, 113456]dosa [78778, 213441, 200658] myphatshala.in
Behind the screen reducer 0 reducer 1 reducer 2 map output: 300,000 pairs, 6 keyspartition = (hash & MAX) % 3each pair is tagged with a partition number map output, sorted per partitionchai, dosa,samosa, vada pavidli, poha(no keys)the shuffle: each reducer fetches only its partition reduce(chai, …)reduce(dosa, …) …reduce(idli, …)reduce(poha, …)0 callssetup → reduce() once per key, in sorted order → cleanup part-r-000000 Bpart-r-0000179 Bpart-r-0000238 BHDFS /out/red_3_temporary → committed → _SUCCESSeach task's file is committed, replicated 3 times one Text object"113456"first3 = [ref, ref, ref]all point to the same objecteach loop step overwrote the object: 104221 → 150350 → 113456 new Text("104221")new Text("150350")new Text("113456")three separate copiescopy what you keep; values come in no fixed order myphatshala.in
6 Real-world use

Reducers at work

reduces = 200≈ 0.95 × slots

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.

Output size→reduces ≈ size / 256 MB→Healthy files

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.

One slow reducer→Reduce input records per task→Find the hot key
0 reducersno shuffle

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.

Raw files→map-only job→part-m-* files
[x, x, x][a, b, c]

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.

list.add(value)→review→list.add(new Text(value))

Simplified; organisations differ in tools and policies.

7 Run the code

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)")
part-r-00000: (empty) part-r-00001: ['chai\t2\t30', 'dosa\t1\t45', 'samosa\t2\t36', 'vada pav\t1\t20'] part-r-00002: ['idli\t2\t60', 'poha\t2\t50']
  • 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_bug
$ hdfs dfs -cat /out/sample_ok/part-r-00000 chai [104221, 150350, 113456] dosa [78778, 213441, 200658] idli [101007, 50991, 101001] poha [199796, 104049, 99562] samosa [151000, 300992, 300987] vada pav [101030, 268025, 50984] $ hdfs dfs -cat /out/sample_bug/part-r-00000 chai [113456, 113456, 113456] dosa [200658, 200658, 200658] idli [101001, 101001, 101001] poha [99562, 99562, 99562] samosa [300987, 300987, 300987] vada pav [50984, 50984, 50984]

Real 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.sh
Launched reduce tasks=3 Reduce input groups=6 Reduce output records=6 == 3 reducers: output files part-r-00000 (0 bytes): part-r-00001 (79 bytes): chai dosa samosa vada pav part-r-00002 (38 bytes): idli poha Launched reduce tasks=11 Reduce input groups=6 Reduce output records=6 == 10 reducers: output files 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): idli == 0 reducers (map-only): mapper output written directly, unsorted Launched map tasks=1 2699986 /out/red_0/part-m-00000 samosa 18 vada pav 20 poha 25 == first 3 order ids per item, copying each value (correct) chai [104221, 150350, 113456] dosa [78778, 213441, 200658] idli [101007, 50991, 101001] poha [199796, 104049, 99562] samosa [151000, 300992, 300987] vada pav [101030, 268025, 50984] == the same job WITHOUT copying (-D sample.copy=false) chai [113456, 113456, 113456] dosa [200658, 200658, 200658] idli [101001, 101001, 101001] poha [99562, 99562, 99562] samosa [300987, 300987, 300987] vada pav [50984, 50984, 50984]

Real 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.

8 Line by line

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.

Example 1BeginnerSizing · count

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?

Input
20 nodes × 6 containers
Expected output
one wave : 114
two waves: 210

Approach

  1. Total containers = nodes × containers per node.
  2. 0.95 × total: all reducers start in one wave.
  3. 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.

Example 2IntermediatePrediction · files

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.

Input
6 items · 4 reducers
Expected output
part-r-00000 []
part-r-00001 ['idli', 'poha']
part-r-00002 ['chai', 'dosa', 'vada pav']
part-r-00003 ['samosa']

Approach

  1. Take each item's Text hash with the sign bit cleared (h = 31h + byte, 32-bit).
  2. Take the remainder by 4.
  3. 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.

Example 3AdvancedSkew · diagnosis

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?

Input
49 × 2 lakh records · 1 × 9 crore records · 60 min
Expected output
hot reducer has 450x the normal load
even spread: 1,996,000 records each, about 1.3 min

Approach

  1. The slow reducer has about 450× more records: one hot key.
  2. Time is roughly proportional to records.
  3. 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.

9 Quiz and revision

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 setNumReduceTasks or -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.

P1Easy

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.

Input
4 output lines
Expected output
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")
79 bytes

How it works: 79: each line plus its newline character.

P2Easy

Empty files

With 6 keys and 10 reducers, how many files must be empty at the very least?

Input
6 keys · 10 reducers
Expected output
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")
4 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.

P3Easy

Reducer input

The reducer saw Reduce input records=300000 and Reduce input groups=6. How many values per reduce() call, on average?

Input
300,000 / 6
Expected output
50000.0
Show solution and explanation
print(300000 / 6)
50000.0

How it works: 50,000 values per call: each item has about 50,000 orders.

P4Medium

Single pass

Write a reducer that returns count, sum and max of a key's values in ONE loop over an iterator.

Input
iterator of amounts
Expected output
('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])))
('dosa', 4, 175, 45)

How it works: One pass is all you get; keep running totals instead of a list.

P5Medium

Group a sorted stream

Given a sorted stream of (key, value), produce one line per key with the sum, without building a dictionary.

Input
sorted pairs
Expected output
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))
chai 30 idli 30 poha 50

How it works: This is how Hadoop groups: the input is sorted, so equal keys are next to each other.

P6Medium

Partition a new item

The canteen adds “upma”. With 3 reducers, which file will it go to?

Input
“upma”, 3 reducers
Expected output
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))
part-r-00001

How it works: The hash decides; adding one key never moves the other items.

P7Business problem

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.

Input
400 reducers · 8 GB/day · 365 days
Expected output
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)
146,000 files a year, 20 MB each better reducer count: 32

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).

P8Advanced

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.

Input
record counts per key
Expected output
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)
largest group before: 900000 after: 225000 chai recombined: 900000

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.

Concepts · partitioningFresher

“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).
Coding · bugExperienced

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.
Performance · skewExperienced

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.
Design · outputFresher

“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).