Counters

Counters are the job's built-in tally sheet: every task counts, Hadoop adds the counts up, and the driver reads the totals. This lesson uses them to answer questions without writing a single output file, turns one into a data-quality gate, and then breaks the 120-counter limit on purpose to see why counters are not for per-key data.

Module 9 · MapReduce in depth⏱ 24 minIntermediateAnswers from counters aloneLimit hit live: 121 max=120Runs in your browser (Python)8 practice problems
1 The story

Five lakh orders, no output file, all the answers

The canteen manager wants three quick numbers from September: how many orders per category, how busy each hour was, and how much money each category made. Riya starts writing a reducer. Arjun Sir stops her: “You don't need an output file for twenty numbers. Let the map tasks count, and read the counts in the driver.” The job finishes with no part files at all, and prints every answer. Then Kabir adds “one counter per student, just to see”, and the same job dies after a few seconds.

map tasks count map 0: DRINK +1, +1 … map 1: SNACK +1 … map 2: MEAL +1 … MRAppMaster adds them up driver: job.getCounters() DRINK190,033 SNACK139,638 BREAKFAST90,110 MEAL80,219 busiest hour: 14:00 (62,213) + one counter per student → Too many counters: 121 max=120 → FAILED Counters are for a few dozen numbers about the job, not for data with a key per student. myphatshala.in
Riya

Sir, so counters are just a free way to get totals?

Arjun Sir

Almost. Every task keeps its own little counters, sends them to the MRAppMaster with its heartbeat, and the framework adds them up. You already read 54 of them after your first job in Lesson 8.1, and you made your own in Lesson 8.3 to count bad records.

Kabir

Then why did my per-student version crash? It's the same code with one more line.

Arjun Sir

Because counters live in memory in every task, the MRAppMaster and the JobHistory Server. Hadoop allows only 120 of them per job. 2,000 students is a dataset, not a counter. Today we see what counters are good for, how to read them in the driver, how to make a job fail on bad data, and where the limit bites.

2 Definition

Counters

Formal definition (write this in exams)

A counter in Hadoop MapReduce is a named, job-wide long integer that tasks increment while they run and the framework aggregates. Counters are organised in groups. Hadoop maintains built-in counters (file system, job, task/framework, input and output format, shuffle errors); programs define custom counters either as Java enum constants (the enum class is the group) or as dynamic counters named by two strings, group and counter, at run time. Each task attempt keeps its counters locally and reports them to the ApplicationMaster with its progress updates; the job's final values are the sum over the successful attempt of every task, so counts from failed or killed attempts are not included. The driver can read the final values through Job.getCounters(). The number of counters per job is limited (mapreduce.job.counters.max, 120 by default), because every counter is kept in memory by tasks, the ApplicationMaster and the JobHistory Server.

In simple words

Counters are tally marks every task makes on its own sheet. Hadoop collects the sheets, adds them up, and gives you the totals at the end. Use them for a few dozen facts about the job (records read, bad rows, rows per category), never for one tally per customer.

Words you'll meet in this lesson

Counter groupA named set of counters, e.g. “Map-Reduce Framework” or your enum class.
Built-in counterMaintained by Hadoop: Map input records, HDFS bytes read, Spilled Records…
Enum counterctx.getCounter(Category.DRINK): names checked by the compiler.
Dynamic counterctx.getCounter("Orders by hour", "14"): names chosen at run time.
Job countersGroup of facts about tasks: launched, failed, killed, data-local maps.
Counter limitmapreduce.job.counters.max = 120 per job by default.
NullOutputFormatAn OutputFormat that writes nothing: handy when counters are the result.
Data quality gateThe driver checks counters and fails the pipeline if bad rows appear.

From our lab (Hadoop 3.5.0, 5 lakh student orders, 3 map tasks)

Enum countersDRINK 190,033 · SNACK 139,638 · BREAKFAST 90,110 · MEAL 80,219 (sum 5,00,000 = Map input records)
Dynamic counters13 “Orders by hour” counters, busiest 14:00 with 62,213 orders, quietest 20:00 with 18,724; 4 “Revenue (Rs)” counters, MEAL ₹46,70,075 highest
OutputNone: NullOutputFormat, map-only, exit code 0. Revenue matches the Python Streaming job of Lesson 9.8 to the rupee
Too manyOne counter per student (2,000): exit code 1, “LimitExceededException: Too many counters: 121 max=120”, job FAILED

Counters at three levels

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

Beginner Tally marks on many sheets

  • Each map task is a clerk with a tally sheet: one line per category, a mark for every order.
  • From time to time the clerk shows the sheet to the supervisor (the MRAppMaster), who keeps a running total.
  • At the end the supervisor hands you the totals. You never read the orders yourself.
  • A sheet for each of 2,000 students would not fit the supervisor's desk. Hadoop's desk holds 120 lines.

Intermediate Built-in, enum and dynamic

  • Built-in: 54 of them printed after our first job (Lesson 8.1). The most useful: Map input/output records, Reduce input groups, Spilled Records, HDFS bytes read/written, Launched/Failed/Killed tasks, Data-local map tasks.
  • Enum: enum Category { DRINK, SNACK, BREAKFAST, MEAL, UNKNOWN } then ctx.getCounter(Category.DRINK).increment(1). The group name is the enum's class name, OrderCounters$Category.
  • Dynamic: ctx.getCounter("Orders by hour", "14").increment(1). Flexible, but a typo silently makes a new counter, and the number of names must stay small.
  • Reading: after waitForCompletion, job.getCounters().findCounter(...) or loop over getGroup(name).

Advanced What the framework guarantees

  • Final counters come only from the successful attempt of each task. A map that failed half-way and was retried is not counted twice, and the killed copy from speculative execution adds nothing.
  • While the job runs, counters are approximate (they arrive with heartbeats). Decide on final values only.
  • Every counter costs memory in every task, the MRAppMaster and the JobHistory Server, and travels in every progress report. That is why mapreduce.job.counters.max is 120 and the number of groups is limited (50 by default).
  • Counters are not a side channel for results. Anything keyed by a value with many possibilities (student, item, URL) belongs in the job's output.

Exam tip: For “What are counters in MapReduce? Give an example”, name the two kinds of custom counters (enum and dynamic) with one line of code each, two built-in counters (Map input records, Reduce input groups), and one use: counting malformed records so the driver can fail the job.

3 Types and ways to use it

The counter groups Hadoop prints

GroupTypical countersWhat it tells you
File System CountersHDFS: Number of bytes read / written, FILE: bytes read / writtenInput size and how much intermediate data hit local disk
Job CountersLaunched / Failed / Killed map tasks, Data-local map tasks, total time and MB-msRetries, speculation, locality, resource use
Map-Reduce FrameworkMap input / output records, Combine input / output records, Reduce input groups, Spilled Records, GC time, CPU timeData flow through each phase (Lessons 8.3 – 9.1)
File Input / Output Format CountersBytes Read, Bytes WrittenWhat the InputFormat read and the OutputFormat wrote
Shuffle ErrorsBAD_ID, CONNECTION, IO_ERROR, WRONG_LENGTH, WRONG_MAP, WRONG_REDUCEProblems while reducers fetch map output
Your own groupsenum constants, or (group, name) stringsBusiness and data-quality facts

Good and bad uses of custom counters

Data quality

Malformed lines, missing fields, unknown items. Lesson 8.3: HEADER 42, WRONG_FIELDS 94, BAD_AMOUNT 107.

good

Small category totals

Orders per category, per hour, per status code: a bounded, small set of names.

good

Join diagnostics

Rows that found or did not find a partner (Lesson 9.5: ORDERS_WITHOUT_MENU 40,364).

good

One per key

Per student, per product, per URL: unbounded names hit the 120 limit or bloat memory. Write output instead.

bad

Counters are visible in many places: printed by waitForCompletion(true), in the ResourceManager UI while running (port 8088), in the JobHistory UI and REST API afterwards (port 19888), and from the shell with mapred job -counter <job-id> <group> <name>.

4 Step by step

A job whose only result is its counters

Run on the Module 8 lab cluster with counters_lab.sh (in “Run it on your PC”). The data is student_orders.csv: 5 lakh orders, 2,000 students, September 2026.

  1. Count in the mapper

    ctx.getCounter(c).increment(1);                                   // enum: Category.DRINK …
    ctx.getCounter("Orders by hour", f[2].substring(11, 13)).increment(1);   // dynamic
    ctx.getCounter("Revenue (Rs)", c.name()).increment(Long.parseLong(f[4]));
    A counter is a long, so it can add more than 1 at a time: the revenue counters add each order's amount. The mapper writes no key-value pairs at all.
  2. Write nothing

    job.setNumReduceTasks(0);
    job.setOutputFormatClass(NullOutputFormat.class);    // no output files at all
    boolean ok = job.waitForCompletion(false);           // false: don't print progress and counters
    No reducer, no output folder, no part files. With waitForCompletion(false) Hadoop doesn't print its own counter dump, so the driver prints exactly what it wants.
  3. Read the counters in the driver

    hadoop jar mr9.jar OrderCounters /user/hadoop/data/student_orders.csv
    exit code: 0 job SUCCEEDED, map input records = 500000 [OrderCounters$Category] BREAKFAST 90,110 DRINK 190,033 MEAL 80,219 SNACK 139,638
    findCounter(TaskCounter.MAP_INPUT_RECORDS) reads a built-in counter; getGroup(Category.class.getName()) lists our enum group. The four categories add up to exactly 5,00,000.
  4. Dynamic counters: hours and money

    [Orders by hour]
      08         55,852
      13         56,026
      14         62,213
      20         18,724
    [Revenue (Rs)]
      BREAKFAST  2,476,620
      DRINK      3,052,315
      MEAL       4,670,075
      SNACK      2,652,992
    The hour names (08 … 20) were invented while the job ran. Lunch at 14:00 is the busiest hour; drinks are the most orders but meals bring the most money.
  5. Use a counter as a quality gate

    if (cs.findCounter(Category.UNKNOWN).getValue() > 0) {
      System.out.println("unknown items found!");
      System.exit(2);                                  // the scheduler sees a failure
    }
    UNKNOWN was 0, so the exit code was 0. In a nightly pipeline (Oozie, Airflow) a non-zero exit stops the next step from loading bad data.
  6. One counter per student

    hadoop jar mr9.jar OrderCounters -D counters.perStudent=true /user/hadoop/data/student_orders.csv
    exit code: 1 job FAILED, map input records = 0 Diagnostics : Task failed task_1792643020992_0036_m_000002 Error: org.apache.hadoop.mapreduce.counters.LimitExceededException: Too many counters: 121 max=120
    Each map task met hundreds of students within seconds; at the 121st counter the task threw an exception. Every retry failed the same way; once one map task had failed 4 times, the job was failed and the other attempts killed. Because only successful attempts count, the driver read 0 map input records.

Don't raise mapreduce.job.counters.max to make per-key counters “work”. Counters are held in memory by every task, the MRAppMaster and the JobHistory Server, and sent with every heartbeat. Per-key numbers are the job's output: write them with a reducer.

5 See it happen

How counters travel

Six steps: tasks count locally, report with heartbeats, a failed attempt is thrown away, the driver reads the totals, and a per-student counter breaks the limit.

From task to driver 6 steps
What you see hadoop@namenode : /tmp/mr9 $ hadoop jar mr9.jar OrderCounters …number of splits:3(no progress printed: waitForCompletion(false)) job running …counters in the RM UI change every few seconds a map attempt fails and is retriedthe final totals don't change exit code: 0job SUCCEEDED, map input records = 500000[OrderCounters$Category] BREAKFAST 90,110 DRINK 190,033 MEAL 80,219 SNACK 139,638 [Orders by hour] 14 62,213[Revenue (Rs)] MEAL 4,670,075UNKNOWN = 0 → exit 0 $ … -D counters.perStudent=true …exit code: 1job FAILEDToo many counters: 121 max=120 myphatshala.in
Behind the screen map 0 map 1 map 2 MRAppMaster DRINK +1hour 08 +1SNACK +1hour 14 +1MEAL +1hour 13 +1each task keeps its own counters in memory running totalscounters ride along with progress updates map 1 attempt 0 ✗its counts are droppedonly the successful attempt of each task counts drivergetCounters()final totals go to the client and the JobHistory Server UNKNOWN= 0 → exit 0the driver turns a counter into a pass/fail decision S0001 S0002 …counter #121 ✗S0007 S0013 …counter #121 ✗S1413 S0973 …counter #121 ✗LimitExceededException in every task: job FAILED myphatshala.in
6 Real-world use

Counters at work

BAD_ROWS 0.2%limit 0.1% → stop

1. Quality gates in nightly pipelines

An ingestion job counts malformed rows; the driver compares them with the input count and exits non-zero above a threshold, so the scheduler doesn't publish a broken day.

Raw files→counters: GOOD / BAD→Publish or stop

2. Job monitoring and alerting

Operations teams scrape counters from the JobHistory REST API (records in and out, spilled records, GC time) and alert when a daily job suddenly reads half its usual input.

JobHistory REST→Compare with yesterday→Alert
joinno match40,364

3. Reconciliation

Joins and lookups count rows that found no partner, so finance teams can reconcile totals before reports go out.

Orders + master data→MATCHED / UNMATCHED→Reconciliation report
Sparkaccumulators

4. The same idea in Spark

Spark's accumulators play the role of counters, and Hive shows MapReduce or Tez counters for every query. The rules carry over: few names, read final values only.

Tasks→accumulators→Driver reads totals

Simplified; organisations differ in tools and policies.

7 Run the code

Simulate how task counters become job counters

Each map task attempt keeps its own counters. The job total is the sum over the successful attempt of each task. Here map 1's first attempt fails half-way and is retried.

attempts = [
    # (task, attempt, state, counters this attempt incremented)
    ("m_0", 0, "SUCCEEDED", {"DRINK": 64000, "SNACK": 46500, "MEAL": 26700}),
    ("m_1", 0, "FAILED",    {"DRINK": 30000, "SNACK": 21000, "MEAL": 12000}),   # died half-way
    ("m_1", 1, "SUCCEEDED", {"DRINK": 63500, "SNACK": 46800, "MEAL": 26900}),
    ("m_2", 0, "SUCCEEDED", {"DRINK": 62533, "SNACK": 46338, "MEAL": 26619}),
]

job = {}
for task, n, state, counters in attempts:
    if state != "SUCCEEDED":
        print(f"{task} attempt {n}: {state}, its counts are dropped")
        continue
    for name, value in counters.items():
        job[name] = job.get(name, 0) + value

for name in sorted(job):
    print(f"{name:6} {job[name]:,}")
m_1 attempt 0: FAILED, its counts are dropped DRINK 190,033 MEAL 80,219 SNACK 139,638
  • if state != "SUCCEEDED":Only successful attempts count, so a retried task is never counted twice.
  • job[name] = job.get(name, 0) + valueThe framework adds the same-named counter from every task: DRINK becomes 190,033 as in our lab.

Run it on your PC: a counters-only job, then too many counters

Uses the Module 8 lab cluster (Lesson 8.1). student_orders.csv and menu.csv come from make_student_orders.py, shared by all Module 9 lessons. Copy the files into the namenode container, compile the Module 9 programs into mr9.jar, then run the script.

Enum and dynamic counters, read by the driver

// OrderCounters.java: a map-only job that only counts. Enum counters, dynamic counters,
// and reading every counter back in the driver after the job.
// -D counters.perStudent=true adds one dynamic counter per student (to hit the counter limit)
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Counter;
import org.apache.hadoop.mapreduce.CounterGroup;
import org.apache.hadoop.mapreduce.Counters;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.TaskCounter;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.NullOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;

public class OrderCounters {
  enum Category { DRINK, SNACK, BREAKFAST, MEAL, UNKNOWN }

  public static class M extends Mapper<LongWritable, Text, NullWritable, NullWritable> {
    private boolean perStudent;
    @Override
    protected void setup(Context ctx) { perStudent = ctx.getConfiguration().getBoolean("counters.perStudent", false); }

    @Override
    protected void map(LongWritable k, Text line, Context ctx) {
      String[] f = line.toString().split(",");          // order_id,student_id,time,item,amount
      String item = f[3];
      Category c;
      switch (item) {
        case "chai": case "coffee": c = Category.DRINK; break;
        case "samosa": case "vada pav": c = Category.SNACK; break;
        case "poha": case "idli": c = Category.BREAKFAST; break;
        case "dosa": case "thali": c = Category.MEAL; break;
        default: c = Category.UNKNOWN;
      }
      ctx.getCounter(c).increment(1);                             // enum counter
      ctx.getCounter("Orders by hour", f[2].substring(11, 13)).increment(1);   // dynamic counter: group, name
      ctx.getCounter("Revenue (Rs)", c.name()).increment(Long.parseLong(f[4]));
      if (perStudent) ctx.getCounter("Orders by student", f[1]).increment(1);
    }
  }

  public static void main(String[] args) throws Exception {
    Configuration conf = new Configuration();
    String[] rest = new GenericOptionsParser(conf, args).getRemainingArgs();
    Job job = Job.getInstance(conf, "order counters");
    job.setJarByClass(OrderCounters.class);
    job.setMapperClass(M.class);
    job.setNumReduceTasks(0);
    job.setOutputFormatClass(NullOutputFormat.class);            // no output files at all
    FileInputFormat.addInputPath(job, new Path(rest[0]));
    boolean ok = job.waitForCompletion(false);                   // false: don't print progress and counters

    // The driver reads the final counters and decides what to do with them
    Counters cs = job.getCounters();
    long in = cs.findCounter(TaskCounter.MAP_INPUT_RECORDS).getValue();
    System.out.println("job " + (ok ? "SUCCEEDED" : "FAILED") + ", map input records = " + in);
    for (String group : new String[] {OrderCounters.Category.class.getName(), "Orders by hour", "Revenue (Rs)"}) {
      CounterGroup g = cs.getGroup(group);
      System.out.println("[" + g.getDisplayName() + "]");
      for (Counter c : g) System.out.printf("  %-10s %,d%n", c.getDisplayName(), c.getValue());
    }
    if (cs.findCounter(Category.UNKNOWN).getValue() > 0) { System.out.println("unknown items found!"); System.exit(2); }
    System.exit(ok ? 0 : 1);
  }
}

// In the mr-lab folder:
//   python3 make_student_orders.py
//   docker exec lab8-namenode-1 mkdir -p /tmp/mr9
//   docker cp OrderCounters.java lab8-namenode-1:/tmp/mr9/
//   docker cp student_orders.csv lab8-namenode-1:/tmp/mr9/
//   docker exec -it lab8-namenode-1 bash
// Inside the container:
//   cd /tmp/mr9
//   hdfs dfs -D dfs.blocksize=8388608 -put student_orders.csv /user/hadoop/data/
//   mkdir -p classes && javac -cp $(hadoop classpath) -d classes *.java && jar cf mr9.jar -C classes .
//   hadoop jar mr9.jar OrderCounters /user/hadoop/data/student_orders.csv
== OrderCounters (map-only, NullOutputFormat: no output files) exit code: 0 job SUCCEEDED, map input records = 500000 [OrderCounters$Category] BREAKFAST 90,110 DRINK 190,033 MEAL 80,219 SNACK 139,638 [Orders by hour] 08 55,852 09 43,655 10 24,986 11 25,002 12 31,091 13 56,026 14 62,213 15 37,945 16 25,218 17 37,777 18 50,133 19 31,378 20 18,724 [Revenue (Rs)] BREAKFAST 2,476,620 DRINK 3,052,315 MEAL 4,670,075 SNACK 2,652,992

Real output. The file is stored with 8 MB blocks so the job has 3 map tasks; the counters are the sum of all three. Nothing is written to HDFS.

The whole lab, including the counter limit

# counters_lab.sh: enum and dynamic counters read back by the driver; then too many counters
cd /tmp/mr9
D=/user/hadoop/data/student_orders.csv
echo "== OrderCounters (map-only, NullOutputFormat: no output files)"
hadoop jar mr9.jar OrderCounters $D > /tmp/oc.log 2>&1; echo "exit code: $?"
grep -vE "INFO|WARN" /tmp/oc.log
echo "== one dynamic counter per student (2,000 of them)"
hadoop jar mr9.jar OrderCounters -D counters.perStudent=true $D > /tmp/oc2.log 2>&1; echo "exit code: $?"
grep "job FAILED" /tmp/oc2.log
app=$(grep -o "application_[0-9_]*" /tmp/oc2.log | head -1)
yarn application -status $app 2>/dev/null | grep -A1 "Diagnostics :" | sed 's/^\s*//'
echo "-- why the map attempts failed (JobHistory REST)"
J=job_${app#application_}
curl -s http://historyserver:19888/ws/v1/history/mapreduce/jobs/$J/tasks/${J/job/task}_m_000000/attempts \
  | python3 -c 'import sys, json; t = json.load(sys.stdin)["taskAttempts"]["taskAttempt"]; t = t if isinstance(t, list) else [t]; print(next(a for a in t if a["state"] == "FAILED")["diagnostics"].splitlines()[0])'
echo "== the limit"
hdfs getconf -confKey mapreduce.job.counters.max

# Inside the namenode container, in /tmp/mr9 (attempts are read from the JobHistory Server REST API):
#   bash counters_lab.sh
== OrderCounters (map-only, NullOutputFormat: no output files) exit code: 0 job SUCCEEDED, map input records = 500000 [OrderCounters$Category] BREAKFAST 90,110 DRINK 190,033 MEAL 80,219 SNACK 139,638 [Orders by hour] 08 55,852 09 43,655 10 24,986 11 25,002 12 31,091 13 56,026 14 62,213 15 37,945 16 25,218 17 37,777 18 50,133 19 31,378 20 18,724 [Revenue (Rs)] BREAKFAST 2,476,620 DRINK 3,052,315 MEAL 4,670,075 SNACK 2,652,992 == one dynamic counter per student (2,000 of them) exit code: 1 job FAILED, map input records = 0 Diagnostics : Task failed task_1792643020992_0036_m_000002 Job failed as tasks failed. failedMaps:1 failedReduces:0 killedMaps:0 killedReduces: 0 -- why the map attempts failed (JobHistory REST) Error: org.apache.hadoop.mapreduce.counters.LimitExceededException: Too many counters: 121 max=120 == the limit 120

Real output. With per-student counters every map attempt fails within seconds with LimitExceededException; after one task fails 4 times the job is failed. The limit is mapreduce.job.counters.max (120).

8 Line by line

Counting categories the way the mapper does

Follow four orders updating an enum-style group and a dynamic “Revenue” group, exactly like the three getCounter(...).increment(...) lines in OrderCounters.java.

Worked examples: real problems, solved and traced in memory

Three counter problems: reading a quality gate, designing counter names, and explaining a surprising number.

Example 1BeginnerReading · built-in

What do these counters say about the job?

Problem statement

From Lesson 8.3: Map input records = 300,302; GOOD = 300,059; HEADER = 42; WRONG_FIELDS = 94; BAD_AMOUNT = 107. Do the custom counters explain every input record? What share of the input was bad?

Input
five counters from CleanOrders
Expected output
explained: True
bad rows 243 = 0.081% of input

Approach

  1. Add the custom counters and compare with Map input records.
  2. Bad = everything except GOOD.
  3. Share = bad / input.

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

Counters or output?

Problem statement

A team wants, for a 10-crore-row clickstream job: (a) number of rows with a missing user id, (b) clicks per page for 40,000 pages, (c) clicks per device type (mobile, desktop, tablet, other), (d) clicks per country (about 200). Which belong in counters?

Input
4 requested statistics
Expected output
missing user id           1 names -> counter
clicks per device         4 names -> counter
clicks per country      200 names -> job output
clicks per page       40000 names -> job output

Approach

  1. Counters suit a small, fixed set of names.
  2. Count the names each statistic needs.
  3. Compare with the 120-counter limit and the built-ins already used (about 50).

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 3AdvancedInternals · attempts

Why didn't the speculative copy double the count?

Problem statement

In Lesson 8.1, 5 map attempts ran for 4 splits (one speculative copy was killed after processing about a quarter of its split). Map input records was exactly 27,000,000, the true number of lines. Show why, using attempt states.

Input
4 successful attempts + 1 killed
Expected output
all attempts 28,700,000 · successful only 27,000,000

Approach

  1. List what every attempt read.
  2. Sum all attempts (wrong) and only successful ones (what Hadoop does).
  3. Compare with 27,000,000.

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.4 in one screen

  • Counter = job-wide long in a group; tasks increment, the framework sums.
  • Built-in groups: File System, Job Counters, Map-Reduce Framework, File Input/Output Format, Shuffle Errors.
  • Enum counters: getCounter(Category.DRINK). Dynamic: getCounter("group", "name").
  • Final values = sum over each task's successful attempt; failed and killed attempts don't count.
  • Driver: job.getCounters(), findCounter, getGroup; exit non-zero as a quality gate.
  • Lab: DRINK 190,033 · SNACK 139,638 · BREAKFAST 90,110 · MEAL 80,219; busiest hour 14:00 (62,213); no output files.
  • Limit: mapreduce.job.counters.max = 120 (groups 50). Per-student counters → “Too many counters: 121 max=120”.

Interview and viva questions

What are counters in Hadoop MapReduce?

Counters are named long values that tasks increment while a job runs and the framework aggregates into job totals. Hadoop provides built-in counters (records read and written, bytes, spills, launched and failed tasks) and lets programs define their own, typically to count categories or data-quality problems.

What is the difference between enum counters and dynamic counters?

Enum counters are Java enum constants passed to context.getCounter(Enum); the enum class becomes the group and names are checked at compile time. Dynamic counters are created with context.getCounter(String group, String name), so names can be decided at run time, for example one counter per hour. Dynamic names must still stay few, because of the counter limit.

How do you read counters in the driver?

After job.waitForCompletion() returns, call job.getCounters() to get a Counters object. Use findCounter(TaskCounter.MAP_INPUT_RECORDS) or findCounter(MyEnum.X) for a single value, or getGroup("name") to loop over a group. The driver can then print the values or decide its exit code.

What is the limit on the number of counters?

mapreduce.job.counters.max limits a job to 120 counters by default, and mapreduce.job.counters.groups.max to 50 groups. Exceeding it throws LimitExceededException in the task (“Too many counters: 121 max=120” in our lab) and the job fails. The limit exists because counters are kept in memory by tasks, the ApplicationMaster and the JobHistory Server.

Are counters from failed task attempts included?

No. The job's final counters are aggregated from the successful attempt of each task, so retries and speculative copies do not inflate the totals. While a job is still running, the displayed values are progress snapshots and can change.

Practice problems

Counter exercises using the real numbers from the lab.

P1Easy

Do the categories add up?

Check that the four category counters add up to Map input records (5,00,000).

Input
90,110 · 190,033 · 80,219 · 139,638
Expected output
True
Show solution and explanation
print(90110 + 190033 + 80219 + 139638 == 500000)
True

How it works: True: every order fell in exactly one category, and UNKNOWN was 0.

P2Easy

Average order value per category

Divide each revenue counter by its order counter.

Input
revenue and order counters
Expected output
BREAKFAST Rs 27.48
DRINK     Rs 16.06
MEAL      Rs 58.22
SNACK     Rs 19.00
Show solution and explanation
orders = {"BREAKFAST": 90110, "DRINK": 190033, "MEAL": 80219, "SNACK": 139638}
revenue = {"BREAKFAST": 2476620, "DRINK": 3052315, "MEAL": 4670075, "SNACK": 2652992}
for c in orders:
    print(f"{c:9} Rs {revenue[c] / orders[c]:.2f}")
BREAKFAST Rs 27.48 DRINK Rs 16.06 MEAL Rs 58.22 SNACK Rs 19.00

How it works: Meals average about ₹58 per order, drinks about ₹16: drinks lead in orders, meals in money.

P3Easy

Peak hour share

What share of all orders came in the 14:00 hour?

Input
62,213 of 5,00,000
Expected output
12.4%
Show solution and explanation
print(f"{62213 / 500000:.1%}")
12.4%

How it works: 12.4%: one hour in thirteen takes an eighth of the day's orders.

P4Medium

Busiest and quietest hour

From the “Orders by hour” group, find the busiest and quietest hours and their ratio.

Input
13 hourly counters
Expected output
14 62213 20 18724 ratio 3.3x
Show solution and explanation
hours = {"08": 55852, "09": 43655, "10": 24986, "11": 25002, "12": 31091, "13": 56026, "14": 62213,
         "15": 37945, "16": 25218, "17": 37777, "18": 50133, "19": 31378, "20": 18724}
busy = max(hours, key=hours.get)
quiet = min(hours, key=hours.get)
print(busy, hours[busy], quiet, hours[quiet], f"ratio {hours[busy] / hours[quiet]:.1f}x")
14 62213 20 18724 ratio 3.3x

How it works: 14:00 vs 20:00, about 3.3 times busier: the canteen needs most staff around lunch, least at closing time.

P5Medium

Counter budget

A job has 54 built-in counters. You add 5 enum quality counters and an hour group (24 names). How many counters are left before the limit?

Input
54 + 5 + 24, limit 120
Expected output
37 left
Show solution and explanation
print(120 - (54 + 5 + 24), "left")
37 left

How it works: 37 left. Enough for small groups, nowhere near enough for one per customer.

P6Medium

Merge with a retry

Merge these attempt counters into job counters, ignoring failed and killed attempts.

Input
4 attempts
Expected output
{'BAD': 8}
Show solution and explanation
attempts = [("SUCCEEDED", {"BAD": 3}), ("FAILED", {"BAD": 2}), ("SUCCEEDED", {"BAD": 5}), ("KILLED", {"BAD": 1})]
total = {}
for state, cs in attempts:
    if state == "SUCCEEDED":
        for k, v in cs.items():
            total[k] = total.get(k, 0) + v
print(total)
{'BAD': 8}

How it works: BAD = 8. Adding all attempts would wrongly give 11.

P7Business problem

Is today's load normal?

Yesterday the job read 5,00,000 orders; today 2,10,000. Alert if today is below 70% of yesterday. What should happen?

Input
500,000 vs 210,000
Expected output
today is 42% of yesterday -> ALERT: check the upstream feed
Show solution and explanation
y, t = 500000, 210000
print(f"today is {t / y:.0%} of yesterday ->", "ALERT: check the upstream feed" if t < 0.7 * y else "normal")
today is 42% of yesterday -> ALERT: check the upstream feed

How it works: 42%: almost certainly a missing input file, not a quiet day. Counters caught it before the report went out.

P8Advanced

Counters vs output for skewed students

Instead of per-student counters, find the 3 students with the most orders from (student, orders) pairs, the way a reducer's output would be used.

Input
a few (student, orders) rows
Expected output
S0003 4031
S0002 4015
S0007 3934
Show solution and explanation
rows = [("S0001", 3899), ("S0007", 3934), ("S1413", 184), ("S0002", 4015), ("S0003", 4031)]   # real counts from Lessons 9.1 and 9.7
top = sorted(rows, key=lambda r: -r[1])[:3]
for s, n in top:
    print(s, n)
S0003 4031 S0002 4015 S0007 3934

How it works: Per-key numbers come from the job's output (a reducer keyed by student), then any sort or top-N runs on that output. No counter limit involved.

Real-world scenario questions (asked in company interviews)

Counters come up in interviews as “how do you track bad records?” and in debugging questions about strange numbers.

Concepts · basicsFresher

“How would you count malformed records in a MapReduce job?”

A standard question after you explain mappers.

The interviewer is checking: custom counters and what to do with them.

Show a strong answer
  • Define an enum (e.g. GOOD, BAD_FORMAT, BAD_AMOUNT) and call context.getCounter(Quality.BAD_FORMAT).increment(1) when skipping a row.
  • After the job, read job.getCounters() in the driver and fail the pipeline if bad rows exceed a threshold.
  • Optionally write the bad rows themselves to a side output (MultipleOutputs) for inspection.
Debugging · limitsExperienced

A job fails with “Too many counters: 121 max=120”

It started after a developer added counters “for every product”.

The interviewer is checking: knowing the right fix, not just the property.

Show a strong answer
  • Unbounded dynamic counter names (one per product) hit mapreduce.job.counters.max.
  • Raising the limit is the wrong fix: counters live in memory in every task, the AM and the history server.
  • Move per-product numbers into the job output (a reducer keyed by product) and keep counters for a small, fixed set of facts.
Internals · retriesExperienced

“Our task was retried twice. Are the counters triple-counted?”

An analyst doubts the record counts in the job report.

The interviewer is checking: attempt-level aggregation.

Show a strong answer
  • No: final job counters sum only the successful attempt of each task; failed and killed attempts are excluded.
  • Values seen while the job runs are snapshots and may include in-progress attempts.
  • Side effects are a different story: writes to external systems from a failed attempt are NOT undone, which is why tasks should be idempotent.
Operations · monitoringExperienced

The nightly report was published with half the data

An upstream feed delivered only some files, and the job “succeeded”.

The interviewer is checking: using counters as guardrails.

Show a strong answer
  • Compare Map input records (or HDFS bytes read) with the previous run or an expected range, and exit non-zero on a large drop.
  • Collect counters from the JobHistory REST API into monitoring and alert on anomalies.
  • Also validate inputs before the job (expected file count, _SUCCESS markers from upstream).