Partitioner

Between map and reduce, something must decide which reducer receives each key. That is the partitioner. This lesson runs the default hash partitioner and a custom one on the same 5 lakh orders, measures every reducer's load, reproduces Hadoop's hash in Python, and breaks a job on purpose with an invalid partition number.

Module 9 · MapReduce in depth⏱ 24 minIntermediateReal reducer loads measuredHadoop's hash reproduced in PythonRuns in your browser (Python)8 practice problems
1 The story

Four counters, four reducers, one empty

The canteen has four counters: A for drinks, B for snacks, C for breakfast and D for meals. Riya runs the item-revenue job on 5 lakh orders with 4 reducers, hoping for one output file per counter. She gets four files, but one is completely empty, one holds four items from three different counters, and one does more than three times the work of another. Nobody told Hadoop about counters. It sent each item wherever its hash number pointed.

Default: HashPartitioner r0empty r1160,766 · coffee idli poha thali r2269,350 · chai dosa vada pav r369,884 · samosa Custom: CounterPartitioner r0190,033 · A r1139,638 · B r290,110 · C r380,219 · D Real reduce input records from our lab. The partitioner decides which reducer gets each key. myphatshala.in
Riya

Sir, I asked for 4 reducers. Why does one get nothing?

Arjun Sir

Because only 8 different keys exist, and the default HashPartitioner sends each key to hash(key) mod 4. With 8 keys and 4 buckets, it is quite possible that no key lands in bucket 0. It's like 8 students choosing 4 rows by their roll number: nobody planned it to be fair.

Kabir

And chai alone is 30% of all orders, so whichever reducer gets chai is always the busiest.

Arjun Sir

Today we write our own Partitioner: one reducer per canteen counter. We'll also see what happens when a partitioner returns a number that doesn't exist: every map task fails.

2 Definition

Partitioner

Formal definition (write this in exams)

A Partitioner decides, for every intermediate key-value pair produced by a map task, which reduce task will receive it. It implements int getPartition(KEY key, VALUE value, int numReduceTasks), which must return a number from 0 to numReduceTasks − 1 and must return the same number for equal keys, so that all values of a key reach one reducer. It runs inside the map task (and before the combiner), on every map output record, and is only used when the job has more than one reducer. The default, HashPartitioner, computes (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks. A custom partitioner is set with job.setPartitionerClass(), for example to group related keys, to balance load, or (with TotalOrderPartitioner) to produce globally sorted output.

In simple words

The partitioner is the sorting clerk at the end of every map task: for each pair it writes a reducer number on the envelope. By default the number comes from the key's hash. Write your own when you want to choose which keys travel together.

Words you'll meet in this lesson

PartitionThe share of map output for one reducer; one partition = one reduce task = one part-r file.
getPartition()Returns the reducer number (0 … n−1) for one key-value pair.
HashPartitionerThe default: hash of the key, made non-negative, modulo the number of reducers.
Hot keyA key with far more records than others (chai: 30% of orders).
SkewUneven work across reducers; the slowest reducer decides when the job ends.
SaltingAdding a random suffix to a hot key so it spreads over several reducers.
TotalOrderPartitionerSends key ranges to reducers so all output files together are sorted.
Illegal partitionThe error when getPartition() returns a number outside 0 … n−1.

From our lab (Hadoop 3.5.0, 5 lakh orders, 4 reducers)

HashPartitionerreducer 0: 0 records (empty file) · r1: 160,766 (coffee, idli, poha, thali) · r2: 269,350 (chai, dosa, vada pav) · r3: 69,884 (samosa)
CounterPartitionerr0: 190,033 (chai, coffee) · r1: 139,638 (samosa, vada pav) · r2: 90,110 (poha, idli) · r3: 80,219 (dosa, thali); 2 keys each
Busiest vs averagehash: 269,350 vs 125,000 (2.2×) · counter: 190,033 vs 125,000 (1.5×)
Bad partitionerreturned the item's length (4 to 8) for 4 reducers: “java.io.IOException: Illegal partition for chai (4)”, Failed map tasks=10, job FAILED
1 reducerHadoop skips the partitioner entirely: everything is partition 0 (Lesson 8.5)

Partitioners at three levels

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

Beginner The letter sorter

  • A post office has 4 delivery vans. A clerk stamps each letter with a van number before it leaves the counter. That clerk is the partitioner.
  • The default clerk uses a formula on the PIN code. Letters with the same PIN always go in the same van, but some vans may get many more letters, and one may get none.
  • A smarter clerk can be told: “all letters for Kharghar go in van 2”. That is a custom partitioner.

Intermediate How the default works

  • For each map output pair, the map task calls getPartition(key, value, 4) and stores the pair in that partition of its sort buffer (Lesson 8.4).
  • HashPartitioner: (key.hashCode() & Integer.MAX_VALUE) % 4. The & Integer.MAX_VALUE clears the sign bit, because Java's % of a negative number is negative.
  • For Text, hashCode starts at 1 and does h = 31 × h + byte over the UTF-8 bytes. “chai” → 2, “samosa” → 3, “coffee” → 1. No item gives 0.
  • With millions of different keys (student IDs, words) hashing spreads load well. With a handful of keys it is a lottery.

Advanced Designing partitioners

  • Correctness: equal keys must always get the same number, from every map task. Never use randomness or the value alone unless that's the plan (salting changes the key itself).
  • Balance: the job ends when the slowest reducer ends. Estimate key sizes (counters or a sample) and pack big keys apart.
  • Hot keys: one key can't be split by any partitioner, because all its values must meet in one reduce call. Split it in the key: chai#0 … chai#3, then add the four partial results in a second step.
  • Composite keys: with a (student, time) key, partition on the student only, or one student's records scatter over reducers (Lesson 9.7).
  • Global sort: TotalOrderPartitioner with a sampled partition file gives reducer 0 the smallest keys, reducer 1 the next range and so on.

Exam tip: For “What is a Partitioner? Write a custom one”, give the signature getPartition(key, value, numReduceTasks), the default formula (hashCode & Integer.MAX_VALUE) % numReduceTasks, the rule “0 to n−1, same key → same number”, and a 6-line example that maps a category to a reducer number with % numReduceTasks.

3 Types and ways to use it

Partitioners Hadoop ships, and when to write one

PartitionerHow it decidesUse it forWatch out
HashPartitioner (default)key hash mod nMany distinct keysFew keys: empty or overloaded reducers
Custom (extends Partitioner)Your ruleGrouping related keys, one file per category, balancing known sizesMust return 0 … n−1 for every key
KeyFieldBasedPartitionerHash of chosen fields of a text keyStreaming jobs, keys like student\ttimeField options syntax (-k1,1)
TotalOrderPartitionerKey ranges from a partition fileGlobally sorted output across all part filesNeeds a sample (InputSampler) and the same key type
BinaryPartitionerHash of a byte range of a BinaryComparable keyBinary composite keysRarely needed

Why teams write custom partitioners

One file per group

All items of counter A in part-r-00000, B in 00001: downstream readers pick their file.

layout

Known heavy keys

Put the biggest keys on separate reducers instead of trusting the hash.

balance

Composite keys

Partition by the natural key only, so a secondary sort works (Lesson 9.7).

correctness

Range output

Dates or IDs in ranges, so every reducer's file covers one range and the files concatenate in order.

sorting

Before writing a partitioner, look at the reduce tasks of a real run. Our reduce_tasks.sh reads each reduce task's REDUCE_INPUT_RECORDS from the JobHistory Server; the same numbers are on its web UI (port 19888) under each task's counters.

4 Step by step

Hash vs counter partitioner on 5 lakh orders

Run on the Module 8 lab cluster with partitioner_lab.sh (in “Run it on your PC”). The input is student_orders.csv (Lesson 9.1), stored in 3 blocks.

  1. Run with the default partitioner

    hadoop jar mr9.jar ItemPartition -D part.mode=hash student_orders.csv /out/part_hash
    part-r-00000: part-r-00001: coffee(40364) idli(44774) poha(45336) thali(30292) part-r-00002: chai(149669) dosa(49927) vada pav(69754) part-r-00003: samosa(69884)
    The job asked for 4 reducers and got 4 files, but the hash of no item came out as 0. Reducer 0 started a container, fetched nothing and wrote an empty file.
  2. Measure each reducer

    bash reduce_tasks.sh job_1792643020992_…
    r_000000 reduce input records 0 groups 0 r_000001 reduce input records 160766 groups 4 r_000002 reduce input records 269350 groups 3 r_000003 reduce input records 69884 groups 1
    Reducer 2 gets chai (149,669 orders on its own) plus two more items: 54% of all records. In a big job, this reducer alone would decide how long you wait.
  3. Write the counter partitioner

    public static class CounterPartitioner extends Partitioner<Text, IntWritable> {
      // A: chai, coffee  B: samosa, vada pav  C: poha, idli  D: dosa, thali
      public int getPartition(Text item, IntWritable amount, int numReduceTasks) {
        Integer c = COUNTER.get(item.toString());
        if (c == null) return 0;                // unknown items go to the first reducer
        return c % numReduceTasks;              // always 0 .. numReduceTasks-1
      }
    }
    job.setPartitionerClass(CounterPartitioner.class);
    Two safety habits: a fixed answer for unexpected keys, and % numReduceTasks so the same code still works if someone runs it with 2 or 3 reducers.
  4. Run with the counter partitioner

    hadoop jar mr9.jar ItemPartition -D part.mode=counter student_orders.csv /out/part_counter
    part-r-00000: chai(149669) coffee(40364) part-r-00001: samosa(69884) vada pav(69754) part-r-00002: idli(44774) poha(45336) part-r-00003: dosa(49927) thali(30292) r_000000 reduce input records 190033 groups 2 r_000001 reduce input records 139638 groups 2 r_000002 reduce input records 90110 groups 2 r_000003 reduce input records 80219 groups 2
    One file per counter, every reducer busy. Still not perfectly even: counter A carries chai. The partitioner can choose where chai goes, but never split it.
  5. Return a partition that doesn't exist

    return item.toString().length();   // "vada pav" -> 8, but there are only 4 reducers
    Error: java.io.IOException: Illegal partition for chai (4) Error: java.io.IOException: Illegal partition for dosa (4) Error: java.io.IOException: Illegal partition for idli (4) Failed map tasks=10 Job … failed with state FAILED due to: Task failed task_…_m_000001
    The check happens in the map task, on the first bad record. Every attempt failed, Hadoop retried each map up to 4 times, then gave up: 10 failed attempts before the job was killed. Even 4 (“chai”) is illegal: valid numbers are 0 to 3.

Changing the number of reducers changes where every key goes with HashPartitioner. Don't build anything that assumes “samosa is always in part-r-00003”; read the files by content, or use a partitioner that defines the layout.

5 See it happen

Stamping a reducer number on every pair

Six steps: one map task's output passing through the default partitioner, then the custom one.

Which reducer? 6 steps
What you see hadoop@namenode : /tmp/mr9 $ hadoop jar mr9.jar ItemPartition -D part.mode=hash …number of splits:3 map 100% reduce 0%(each map wrote 4 partitions) part-r-00000:part-r-00001: coffee idli poha thalipart-r-00002: chai dosa vada pavpart-r-00003: samosa $ … -D part.mode=counter …part-r-00000: chai coffeepart-r-00001: samosa vada pavpart-r-00002: idli pohapart-r-00003: dosa thali r_000000 190033 groups 2r_000001 139638 groups 2r_000002 90110 groups 2r_000003 80219 groups 2 $ … -D part.mode=bad …Illegal partition for chai (4)Failed map tasks=10failed with state FAILED myphatshala.in
Behind the screen map task output reducer 0 reducer 1 reducer 2 reducer 3 (chai, 15)(samosa, 18)(coffee, 20)(dosa, 45)(idli, 30)every map output pair needs a reducer number (chai, 15) → 2(samosa, 18) → 3(coffee, 20) → 1(dosa, 45) → 2(idli, 30) → 1(hash & MAX_VALUE) % 4, computed in the map task (nothing)coffee idli poha thalichai dosa vada pavsamosa8 keys, 4 buckets: no key hashed to 0 (chai, 15) → 0(samosa, 18) → 1(coffee, 20) → 0(dosa, 45) → 3(idli, 30) → 2A: chai coffeeB: samosa vada pavC: idli pohaD: dosa thaliCounterPartitioner: a lookup table, % numReduceTasks chai is 30% ofall ordersbalanced by counter, but a hot key stays in one place (chai, 15) → 4 ✗(samosa, 18) → 6 ✗(vada pav,20) → 8 ✗IOExceptionmap attempt failsvalid numbers are 0 … 3; retried, then the job fails myphatshala.in
6 Real-world use

Partitioners at work

a–mn–z

1. Globally sorted output (TeraSort)

The famous TeraSort benchmark samples the input and uses a total-order partitioner, so reducer 0 gets the smallest keys and the part files read in order form one sorted data set.

Sample keys→Range partitioner→Sorted part files
region=northpart-r-00002

2. One output file per business unit

Reports split by region, store or product line: a custom partitioner sends each unit's keys to a fixed reducer so each team reads only its own file.

Sales records→Partition by region→File per region

3. Taming hot keys

Click logs where one page or one big customer dominates: teams salt the hot key into several sub-keys, spread them with the partitioner, then merge the partial results.

Hot key→key#0 … key#9→Merge partials
bucket = hash % 32same in Hive, Spark

4. Bucketed tables

Hive and Spark bucketing is the same idea made permanent: rows are written into a fixed number of files by hash of a column, so later joins on that column can skip a shuffle.

CLUSTERED BY (student)→hash % buckets→Bucket files

Simplified; organisations differ in tools and policies.

7 Run the code

Reproduce Hadoop's HashPartitioner

This is exactly what Text.hashCode() and HashPartitioner compute, written in Python with 32-bit arithmetic. Run it and compare with the real files from the lab: chai, dosa and vada pav in part-r-00002, samosa alone in part-r-00003, nothing in part-r-00000.

def text_hash(s):                        # org.apache.hadoop.io.Text.hashCode()
    h = 1
    for b in s.encode("utf-8"):
        h = (31 * h + (b if b < 128 else b - 256)) & 0xFFFFFFFF   # Java int: wraps at 32 bits
    return h - (1 << 32) if h >= (1 << 31) else h              # back to a signed int

def hash_partition(key, n):              # HashPartitioner.getPartition()
    return (text_hash(key) & 0x7FFFFFFF) % n

items = ["chai", "coffee", "dosa", "idli", "poha", "samosa", "thali", "vada pav"]
n = 4
files = {p: [] for p in range(n)}
for item in items:
    files[hash_partition(item, n)].append(item)
for p in range(n):
    print(f"part-r-0000{p}: {files[p]}")
part-r-00000: [] part-r-00001: ['coffee', 'idli', 'poha', 'thali'] part-r-00002: ['chai', 'dosa', 'vada pav'] part-r-00003: ['samosa']
  • h = (31 * h + (b if b < 128 else b - 256)) & 0xFFFFFFFF # Java int: wraps at 32 bitsJava bytes are signed and ints overflow at 32 bits; Python integers don't, so we mask.
  • return (text_hash(key) & 0x7FFFFFFF) % n& Integer.MAX_VALUE clears the sign bit, so the result of % is never negative.
  • files[hash_partition(item, n)].append(item)This call happens in the map task, once for every map output record (5 lakh times in our job).

Run it on your PC: hash vs custom partitioner

Use the Module 8 lab cluster (Lesson 8.1). Copy the files below and student_orders.csv (made by make_student_orders.py in Lesson 9.1) into the namenode container's /tmp/mr9 folder, compile and run the lab script. It runs three jobs, about 2 minutes in total.

Item revenue with a choice of partitioner

// ItemPartition.java: orders and revenue per item with 4 reducers.
// -D part.mode=hash      default HashPartitioner
// -D part.mode=counter   CounterPartitioner: every item of one canteen counter (A–D) goes to the same reducer
// -D part.mode=bad       BadPartitioner (BadPartitioner.java): returns an invalid partition number
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
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.Partitioner;
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 ItemPartition {
  public static class M extends Mapper<LongWritable, Text, Text, IntWritable> {
    private final Text item = new Text();
    private final IntWritable amount = new IntWritable();
    @Override
    protected void map(LongWritable k, Text line, Context ctx) throws IOException, InterruptedException {
      String[] f = line.toString().split(",");          // order_id,student_id,time,item,amount
      item.set(f[3]); amount.set(Integer.parseInt(f[4]));
      ctx.write(item, amount);
    }
  }

  // Counter A: chai, coffee   B: samosa, vada pav   C: poha, idli   D: dosa, thali
  public static class CounterPartitioner extends Partitioner<Text, IntWritable> {
    private static final Map<String, Integer> COUNTER = new HashMap<>();
    static {
      COUNTER.put("chai", 0); COUNTER.put("coffee", 0); COUNTER.put("samosa", 1); COUNTER.put("vada pav", 1);
      COUNTER.put("poha", 2); COUNTER.put("idli", 2); COUNTER.put("dosa", 3); COUNTER.put("thali", 3);
    }
    @Override
    public int getPartition(Text item, IntWritable amount, int numReduceTasks) {
      Integer c = COUNTER.get(item.toString());
      if (c == null) return 0;                           // unknown items go to the first reducer
      return c % numReduceTasks;                         // must always be in 0 .. numReduceTasks-1
    }
  }

  public static class R extends Reducer<Text, IntWritable, Text, Text> {
    @Override
    protected void reduce(Text item, Iterable<IntWritable> amounts, Context ctx) throws IOException, InterruptedException {
      long n = 0, sum = 0;
      for (IntWritable a : amounts) { n++; sum += a.get(); }
      ctx.write(item, new Text(n + "\t" + sum));
    }
  }

  public static void main(String[] args) throws Exception {
    Configuration conf = new Configuration();
    String[] rest = new GenericOptionsParser(conf, args).getRemainingArgs();
    String mode = conf.get("part.mode", "hash");
    Job job = Job.getInstance(conf, "item partition " + mode);
    job.setJarByClass(ItemPartition.class);
    job.setMapperClass(M.class);
    job.setReducerClass(R.class);
    if (mode.equals("counter")) job.setPartitionerClass(CounterPartitioner.class);
    if (mode.equals("bad")) job.setPartitionerClass(BadPartitioner.class);
    job.setNumReduceTasks(conf.getInt("part.reducers", 4));           // -D part.reducers=N to change
    job.setMapOutputKeyClass(Text.class);
    job.setMapOutputValueClass(IntWritable.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);
  }
}

// In the mr-lab folder:
//   docker exec lab8-namenode-1 mkdir -p /tmp/mr9
//   docker cp ItemPartition.java lab8-namenode-1:/tmp/mr9/
//   docker cp BadPartitioner.java lab8-namenode-1:/tmp/mr9/
//   docker exec -it lab8-namenode-1 bash
// Inside the container:
//   cd /tmp/mr9 && mkdir -p classes
//   javac -cp $(hadoop classpath) -d classes *.java && jar cf mr9.jar -C classes .
(compiles silently; the jobs are run by partitioner_lab.sh below)

The partitioner runs in the map task, so its class must be inside the job jar like the Mapper and Reducer.

A partitioner that breaks the job

// BadPartitioner.java: a partitioner that returns a number outside 0..numReduceTasks-1 for one item
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;

public class BadPartitioner extends Partitioner<Text, IntWritable> {
  @Override
  public int getPartition(Text item, IntWritable amount, int numReduceTasks) {
    return item.toString().length();                     // "vada pav" -> 8, but there are only 4 reducers
  }
}
(used by ItemPartition with -D part.mode=bad)

Every item name is at least 4 characters long, so every record gets an illegal partition.

Per-reduce-task records from the JobHistory Server

# reduce_tasks.sh JOB_ID : for each reduce task, where it ran, how long it took and how many records it got
J=$1; ID=${J#job_}; H=http://historyserver:19888/ws/v1/history/mapreduce/jobs/$J/tasks
for t in $(curl -s $H | grep -o "task_${ID}_r_[0-9]*" | sort -u); do
  curl -s $H/$t/counters | python3 -c '
import sys, json
t = sys.argv[1]
groups = json.load(sys.stdin)["jobTaskCounters"]["taskCounterGroup"]
groups = groups if isinstance(groups, list) else [groups]
c = {}
for g in groups:
    cs = g["counter"] if isinstance(g["counter"], list) else [g["counter"]]
    c.update({x["name"]: x["value"] for x in cs})
print("%s  reduce input records %9d  groups %d" % (t[-8:], c["REDUCE_INPUT_RECORDS"], c["REDUCE_INPUT_GROUPS"]))' $t
done

# Inside the namenode container:
#   bash reduce_tasks.sh job_<your job id>
r_000000 reduce input records 0 groups 0 r_000001 reduce input records 160766 groups 4 r_000002 reduce input records 269350 groups 3 r_000003 reduce input records 69884 groups 1

Real output for the hash run. The same counters are on the JobHistory web UI, under each reduce task.

The whole lab

# partitioner_lab.sh: who decides which reducer gets which key? Default hash vs a custom partitioner.
cd /tmp/mr9
D=/user/hadoop/data/student_orders.csv
for mode in hash counter; do
  hdfs dfs -rm -r -f -skipTrash /out/part_$mode > /dev/null 2>&1
  hadoop jar mr9.jar ItemPartition -D part.mode=$mode $D /out/part_$mode > /tmp/part_$mode.log 2>&1
  echo "== part.mode=$mode, 4 reducers"
  for f in $(hdfs dfs -ls /out/part_$mode | grep part-r | awk '{print $8}'); do
    echo "$(basename $f): $(hdfs dfs -cat $f | awk -F'\t' '{printf "%s(%s) ", $1, $2}')"
  done
  sleep 3
  bash reduce_tasks.sh $(grep -o "job_[0-9_]*" /tmp/part_$mode.log | head -1)
done
echo "== a partitioner that returns 8 for 4 reducers"
hdfs dfs -rm -r -f -skipTrash /out/part_bad > /dev/null 2>&1
hadoop jar mr9.jar ItemPartition -D part.mode=bad $D /out/part_bad 2>&1 | grep -E "Illegal partition|Task Id|failed with state|Job .* failed|Failed map tasks" | sed -E 's/^[0-9-]+ [0-9:]+ INFO  //' | cut -c1-160 | sort -u | head -6

# Inside the namenode container:
#   bash /tmp/mr9/partitioner_lab.sh
== part.mode=hash, 4 reducers part-r-00000: part-r-00001: coffee(40364) idli(44774) poha(45336) thali(30292) part-r-00002: chai(149669) dosa(49927) vada pav(69754) part-r-00003: samosa(69884) r_000000 reduce input records 0 groups 0 r_000001 reduce input records 160766 groups 4 r_000002 reduce input records 269350 groups 3 r_000003 reduce input records 69884 groups 1 == part.mode=counter, 4 reducers part-r-00000: chai(149669) coffee(40364) part-r-00001: samosa(69884) vada pav(69754) part-r-00002: idli(44774) poha(45336) part-r-00003: dosa(49927) thali(30292) r_000000 reduce input records 190033 groups 2 r_000001 reduce input records 139638 groups 2 r_000002 reduce input records 90110 groups 2 r_000003 reduce input records 80219 groups 2 == a partitioner that returns 8 for 4 reducers Error: java.io.IOException: Illegal partition for chai (4) Error: java.io.IOException: Illegal partition for dosa (4) Error: java.io.IOException: Illegal partition for idli (4) Failed map tasks=10 Job:1767 - Job job_1792643020992_0030 failed with state FAILED due to: Task failed task_1792643020992_0030_m_000001 Job:1793 - Task Id : attempt_1792643020992_0030_m_000000_0, Status : FAILED

Real output, unedited. Your job IDs will differ; the record counts are the same because the data is generated with a fixed seed.

8 Line by line

Text.hashCode() for one key, byte by byte

Follow how “chai” becomes reducer 2. Watch h grow by ×31 plus each byte.

Worked examples: real problems, solved and traced in memory

Three partitioner problems: an empty reducer, a balanced design, and a hot key that no partitioner can fix.

Example 1BeginnerConcept · empty reducer

Why did one reducer get nothing?

Problem statement

A job has 6 distinct keys and 6 reducers. Assume the hash spreads keys at random. What is the chance that at least one reducer gets no key at all?

Input
6 keys · 6 reducers
Expected output
every reducer busy: 1.5%, at least one empty: 98.5%

Approach

  1. The only way every reducer gets a key is a perfect one-to-one spread.
  2. Count those arrangements: 6! out of 6⁶.
  3. Subtract from 1.

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 2IntermediateDesign · balance

Pack known key sizes onto reducers

Problem statement

You know the order counts per item from last month. Assign the 8 items to 4 reducers so the busiest one is as light as possible, using “biggest item first, onto the emptiest reducer”.

Input
8 items with lab counts
Expected output
{'chai': 0, 'samosa': 1, 'vada pav': 2, 'dosa': 3, 'poha': 3, 'idli': 2, 'coffee': 1, 'thali': 3}
[149669, 110248, 114528, 125555]

Approach

  1. Sort items by size, biggest first.
  2. Give each to the reducer with the least work so far.
  3. Turn the result into a lookup table for getPartition().

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 · salting

Count chai orders on 4 reducers at once

Problem statement

Chai has 149,669 orders. Show the two-step salting plan: step 1 uses keys chai#0 … chai#3 (order number mod 4) and computes partial counts on 4 reducers; step 2 adds the partials under the key chai.

Input
20 sample chai order numbers
Expected output
step 1: {'chai#1': 4, 'chai#0': 5, 'chai#2': 5, 'chai#3': 6}
step 2: {'chai': 20}

Approach

  1. Salt: key = item + "#" + (order_id % 4).
  2. Step 1 counts each salted key separately.
  3. Step 2 strips the salt and adds.

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 9.2 in one screen

  • getPartition(key, value, numReduceTasks) → a number 0 … n−1; equal keys must always get the same number.
  • Runs in the map task on every output record; skipped when there is 1 reducer.
  • Default HashPartitioner: (hashCode & Integer.MAX_VALUE) % n; Text hash: h = 1, h = 31h + byte.
  • Lab, 8 items on 4 reducers: hash → 0 / 160,766 / 269,350 / 69,884 (one empty); counter partitioner → 190,033 / 139,638 / 90,110 / 80,219.
  • Out-of-range number → “Illegal partition”, map attempts fail, job fails (10 failed attempts in our run).
  • A partitioner can't split a hot key; salt the key and combine partials in a second step.
  • Others: KeyFieldBasedPartitioner (fields of text keys), TotalOrderPartitioner + InputSampler (global sort).

Interview and viva questions

What is a Partitioner in MapReduce?

A Partitioner decides which reduce task receives each intermediate key-value pair. Its getPartition(key, value, numReduceTasks) method returns a number between 0 and numReduceTasks - 1, and must return the same number for equal keys so that all values of a key are processed by one reducer. The default is HashPartitioner.

How does HashPartitioner work?

It computes (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks. The bitwise AND removes the sign so the result is never negative. For Text keys the hash is computed over the UTF-8 bytes as h = 31 * h + byte, starting from 1.

When should I write a custom partitioner?

When the default layout or balance is wrong for you: to send related keys to the same reducer (for example one output file per region), to balance known key sizes, to partition a composite key by its natural part for a secondary sort, or to produce range-partitioned, globally sorted output.

What happens if getPartition returns an invalid number?

The map task throws java.io.IOException: Illegal partition for <key> (<number>). The attempt fails, is retried, and after the maximum number of attempts the job fails. Always return a value modulo numReduceTasks and handle unexpected keys.

How do I handle data skew in MapReduce?

Find it first with per-reduce-task counters. If several keys share an overloaded reducer, a custom partitioner can spread them. If a single key is too big, change the key: add a salt (key#0 … key#n) so it spreads across reducers, then merge the partial results in a second step, and use a combiner to shrink what is shuffled.

Practice problems

Partitioner exercises using the real loads from the lab.

P1Easy

Share of the busiest reducer

Hash run: reducer loads 0, 160,766, 269,350 and 69,884. What share of all records did the busiest reducer get?

Input
4 loads
Expected output
53.9%
Show solution and explanation
loads = [0, 160766, 269350, 69884]
print(f"{max(loads) / sum(loads):.1%}")
53.9%

How it works: 53.9%: more than half the reduce work on one of four reducers.

P2Easy

Skew factor

Compute busiest ÷ average for the hash and the counter runs.

Input
two sets of loads
Expected output
hash 2.15
counter 1.52
Show solution and explanation
for name, loads in [("hash", [0, 160766, 269350, 69884]), ("counter", [190033, 139638, 90110, 80219])]:
    print(name, round(max(loads) / (sum(loads) / len(loads)), 2))
hash 2.15 counter 1.52

How it works: 2.15 vs 1.52. 1.0 would be perfectly even.

P3Easy

Valid partition numbers

Which of these return values are legal with 4 reducers: -1, 0, 3, 4?

Input
4 values
Expected output
[0, 3]
Show solution and explanation
print([p for p in [-1, 0, 3, 4] if 0 <= p < 4])
[0, 3]

How it works: Only 0 and 3. Java's % can give -1 for a negative hash, which is why HashPartitioner masks the sign bit.

P4Medium

Java's negative modulo

Java computes -7 % 4 as -3. Show why (h & 0x7FFFFFFF) % 4 is needed for a negative 32-bit hash.

Input
h = -7
Expected output
java % : -3  masked: 1
Show solution and explanation
import math
h = -7
java_mod = int(math.fmod(h, 4))            # Java's % keeps the sign of h
masked = ((h & 0xFFFFFFFF) & 0x7FFFFFFF) % 4
print("java % :", java_mod, " masked:", masked)
java % : -3 masked: 1

How it works: -3 would be an illegal partition; masking gives a valid 1.

P5Medium

Partition by first letter

Write a partitioner for 3 reducers: items starting a–h → 0, i–p → 1, others → 2. Where does each item go?

Input
8 items
Expected output
chai 0
coffee 0
dosa 0
idli 1
poha 1
samosa 2
thali 2
vada pav 2
Show solution and explanation
def get_partition(item, n=3):
    c = item[0]
    return 0 if c <= "h" else 1 if c <= "p" else 2
for i in ["chai", "coffee", "dosa", "idli", "poha", "samosa", "thali", "vada pav"]:
    print(i, get_partition(i))
chai 0 coffee 0 dosa 0 idli 1 poha 1 samosa 2 thali 2 vada pav 2

How it works: A range partitioner: the 3 files, read in order, are sorted by item. That is the idea behind TotalOrderPartitioner.

P6Medium

Composite key, natural partition

Keys are (student, time). Show that hashing the whole key scatters one student over reducers, but hashing only the student keeps them together.

Input
3 orders of S0001
Expected output
whole key : [0, 3, 1]
student   : [3, 3, 3]
Show solution and explanation
def h(s):
    x = 1
    for b in s.encode():
        x = (31 * x + b) & 0xFFFFFFFF
    return (x & 0x7FFFFFFF) % 4
keys = [("S0001", "08:09"), ("S0001", "08:15"), ("S0001", "13:40")]
print("whole key :", [h(s + t) for s, t in keys])
print("student   :", [h(s) for s, t in keys])
whole key : [0, 3, 1] student : [3, 3, 3]

How it works: The whole-key hash sends one student's orders to different reducers; partitioning on the student keeps them in one place, which secondary sort needs (Lesson 9.7).

P7Business problem

Is the custom partitioner worth it?

A nightly job's reduce phase takes as long as its busiest reducer. Reduce time is proportional to records; the hash layout's busiest reducer takes 40 minutes. How long would the counter layout take, and how many minutes a year are saved?

Input
269,350 vs 190,033 records
Expected output
28 min, saves 72 hours a year
Show solution and explanation
hash_min = 40
counter_min = 40 * 190033 / 269350
print(f"{counter_min:.0f} min, saves {(hash_min - counter_min) * 365 / 60:.0f} hours a year")
28 min, saves 72 hours a year

How it works: About 28 minutes, saving roughly 72 hours of waiting a year, for a 10-line class (illustrative).

P8Advanced

Sample-based range partitioner

Like InputSampler + TotalOrderPartitioner: sample student IDs, pick cut points for 4 reducers, then route keys by range.

Input
a sample of 12 student IDs
Expected output
cut points: ['S0450', 'S0999', 'S1500']
S0001 -> reducer 0
S0500 -> reducer 1
S1413 -> reducer 2
S2000 -> reducer 3
Show solution and explanation
import bisect
sample = sorted(["S0004", "S0150", "S0333", "S0450", "S0701", "S0802", "S0999",
                 "S1105", "S1260", "S1500", "S1777", "S1990"])
cuts = [sample[len(sample) * i // 4] for i in range(1, 4)]
print("cut points:", cuts)
for s in ["S0001", "S0500", "S1413", "S2000"]:
    print(s, "-> reducer", bisect.bisect_right(cuts, s))
cut points: ['S0450', 'S0999', 'S1500'] S0001 -> reducer 0 S0500 -> reducer 1 S1413 -> reducer 2 S2000 -> reducer 3

How it works: Each reducer gets one ID range, so part-r-00000 … 00003 concatenated are in order. A bad sample gives uneven ranges, so sample enough keys.

Real-world scenario questions (asked in company interviews)

Partitioner questions test whether you understand where keys go and how to fix skew.

Concepts · basicsFresher

“What is a partitioner and what is the default?”

Asked right after the shuffle question.

The interviewer is checking: where it runs and the formula.

Show a strong answer
  • It assigns each map output pair to a reduce task: getPartition(key, value, numReduceTasks) returns 0 … n−1.
  • It runs in the map task; the default HashPartitioner uses (hashCode & Integer.MAX_VALUE) % n.
  • Same key, same partition, so all values of a key meet in one reducer.
Performance · skewExperienced

“99 reducers finish in 2 minutes, one takes 50 minutes”

A daily job keeps missing its deadline.

The interviewer is checking: diagnosing and fixing skew.

Show a strong answer
  • Check that reducer's input records and groups: one huge key, or many big keys on one partition?
  • Many keys: a custom partitioner (or more reducers) spreads them. One key: salt it (key#0..key#n), aggregate, then merge partials.
  • Add a combiner to cut shuffled records, and filter useless keys (nulls, “unknown”) in the map.
Design · outputFresher

“We need one output file per region”

Five regions, and each regional team wants only its own file.

The interviewer is checking: custom partitioner vs MultipleOutputs.

Show a strong answer
  • Set 5 reducers and a partitioner mapping each region to 0–4; each part-r file then holds one region.
  • Handle unknown regions explicitly and use % numReduceTasks to stay in range.
  • Alternative: MultipleOutputs to write named files per region from any reducer, without tying layout to reducer count.
Sorting · designExperienced

“Sort 1 TB of records by key across 200 reducers”

The output must be one sorted data set when the files are read in order.

The interviewer is checking: TotalOrderPartitioner.

Show a strong answer
  • Each reducer's output is sorted, but HashPartitioner mixes ranges, so the files aren't globally ordered.
  • Use InputSampler to sample keys and write a partition file of 199 cut points; TotalOrderPartitioner routes key ranges.
  • Sample enough keys to avoid uneven ranges; this is how TeraSort works.