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.
Sir, I asked for 4 reducers. Why does one get nothing?
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.
And chai alone is 30% of all orders, so whichever reducer gets chai is always the busiest.
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.
Partitioner
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.
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
From our lab (Hadoop 3.5.0, 5 lakh orders, 4 reducers)
| HashPartitioner | reducer 0: 0 records (empty file) · r1: 160,766 (coffee, idli, poha, thali) · r2: 269,350 (chai, dosa, vada pav) · r3: 69,884 (samosa) |
|---|---|
| CounterPartitioner | r0: 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 average | hash: 269,350 vs 125,000 (2.2×) · counter: 190,033 vs 125,000 (1.5×) |
| Bad partitioner | returned 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 reducer | Hadoop skips the partitioner entirely: everything is partition 0 (Lesson 8.5) |
Partitioners at three levels
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_VALUEclears the sign bit, because Java's%of a negative number is negative. - For
Text, hashCode starts at 1 and doesh = 31 × h + byteover 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.
Partitioners Hadoop ships, and when to write one
| Partitioner | How it decides | Use it for | Watch out |
|---|---|---|---|
| HashPartitioner (default) | key hash mod n | Many distinct keys | Few keys: empty or overloaded reducers |
Custom (extends Partitioner) | Your rule | Grouping related keys, one file per category, balancing known sizes | Must return 0 … n−1 for every key |
| KeyFieldBasedPartitioner | Hash of chosen fields of a text key | Streaming jobs, keys like student\ttime | Field options syntax (-k1,1) |
| TotalOrderPartitioner | Key ranges from a partition file | Globally sorted output across all part files | Needs a sample (InputSampler) and the same key type |
| BinaryPartitioner | Hash of a byte range of a BinaryComparable key | Binary composite keys | Rarely 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.
Known heavy keys
Put the biggest keys on separate reducers instead of trusting the hash.
balanceComposite keys
Partition by the natural key only, so a secondary sort works (Lesson 9.7).
correctnessRange output
Dates or IDs in ranges, so every reducer's file covers one range and the files concatenate in order.
sortingBefore 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.
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.
Run with the default partitioner
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.hadoop jar mr9.jar ItemPartition -D part.mode=hash student_orders.csv /out/part_hashpart-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)Measure each reducer
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.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 1Write the counter partitioner
Two safety habits: a fixed answer for unexpected keys, andpublic 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);% numReduceTasksso the same code still works if someone runs it with 2 or 3 reducers.Run with the counter partitioner
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.hadoop jar mr9.jar ItemPartition -D part.mode=counter student_orders.csv /out/part_counterpart-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 2Return a partition that doesn't exist
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.return item.toString().length(); // "vada pav" -> 8, but there are only 4 reducersError: 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
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.
Stamping a reducer number on every pair
Six steps: one map task's output passing through the default partitioner, then the custom one.
- The job starts with 3 map tasks and 4 reducers, using the default partitioner.
- Each map task sorts its output into 4 partitions, one per reducer.
- The default result: one empty file and an uneven split.
- With the counter partitioner, each file holds one canteen counter.
- Every reducer now gets two items and real work.
- A partitioner that returns 4 for 4 reducers fails every map task.
- The map task produces (item, amount) pairs. Each one must be given a reducer number.
- HashPartitioner computes the number from the key's hash: chai 2, samosa 3, coffee 1, dosa 2, idli 1.
- All 8 items land in buckets 1, 2 and 3. Reducer 0 receives nothing.
- The custom partitioner looks up each item's counter instead.
- Every reducer gets work, but counter A with chai is still the biggest.
- A partitioner returning numbers 4 and above crashes the map task with “Illegal partition”.
Partitioners at work
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.
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.
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.
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.
Simplified; organisations differ in tools and policies.
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]}")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 .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
}
}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>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.shReal output, unedited. Your job IDs will differ; the record counts are the same because the data is generated with a fixed seed.
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.
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?
6 keys · 6 reducers
every reducer busy: 1.5%, at least one empty: 98.5%
Approach
- The only way every reducer gets a key is a perfect one-to-one spread.
- Count those arrangements: 6! out of 6⁶.
- 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.
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”.
8 items with lab counts
{'chai': 0, 'samosa': 1, 'vada pav': 2, 'dosa': 3, 'poha': 3, 'idli': 2, 'coffee': 1, 'thali': 3}
[149669, 110248, 114528, 125555]Approach
- Sort items by size, biggest first.
- Give each to the reducer with the least work so far.
- 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.
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.
20 sample chai order numbers
step 1: {'chai#1': 4, 'chai#0': 5, 'chai#2': 5, 'chai#3': 6}
step 2: {'chai': 20}Approach
- Salt: key = item + "#" + (order_id % 4).
- Step 1 counts each salted key separately.
- 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.
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.
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?
4 loads
53.9%
Show solution and explanation
loads = [0, 160766, 269350, 69884]
print(f"{max(loads) / sum(loads):.1%}")How it works: 53.9%: more than half the reduce work on one of four reducers.
Skew factor
Compute busiest ÷ average for the hash and the counter runs.
two sets of loads
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))How it works: 2.15 vs 1.52. 1.0 would be perfectly even.
Valid partition numbers
Which of these return values are legal with 4 reducers: -1, 0, 3, 4?
4 values
[0, 3]
Show solution and explanation
print([p for p in [-1, 0, 3, 4] if 0 <= p < 4])How it works: Only 0 and 3. Java's % can give -1 for a negative hash, which is why HashPartitioner masks the sign bit.
Java's negative modulo
Java computes -7 % 4 as -3. Show why (h & 0x7FFFFFFF) % 4 is needed for a negative 32-bit hash.
h = -7
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)How it works: -3 would be an illegal partition; masking gives a valid 1.
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?
8 items
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))How it works: A range partitioner: the 3 files, read in order, are sorted by item. That is the idea behind TotalOrderPartitioner.
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.
3 orders of S0001
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])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).
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?
269,350 vs 190,033 records
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")How it works: About 28 minutes, saving roughly 72 hours of waiting a year, for a 10-line class (illustrative).
Sample-based range partitioner
Like InputSampler + TotalOrderPartitioner: sample student IDs, pick cut points for 4 reducers, then route keys by range.
a sample of 12 student IDs
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))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.
“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.
“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.
“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.
“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.