"Make it faster" broke it. The checks noticed.
The pipeline works, and Mrs. Shinde is happy. Arjun Sir now plays the reviewer every real project meets before production: is it correct, is it fast enough for 40 canteens, what happens when a machine dies, and can you explain it in an interview? Kabir tries to speed up the clean job. His first two attempts finish in under 45 seconds and give wrong numbers. Only check.py and the counters catch it.
Thirty-eight seconds! Seven times faster. Ship it?
Read the counters first. CLEAN 192,799, but we know there are 354,135 orders. Fast and wrong is worse than slow and right, because nobody notices.
How did a speed setting change the answer?
Two real Hadoop behaviours we had never met before. That is why reviews exist, and why "tell me about a bug you found" is the most common interview question about a project. Today we review the capstone the way a senior engineer would, then turn it into answers for your interviews.
A project review, and a project story
A production-readiness review examines a data pipeline before it is relied on, across five areas: correctness (acceptance tests, reconciliation, conservation of records), performance and scalability (job time, task counts, behaviour at expected growth), reliability (behaviour under task, node and disk failures; idempotent re-runs), cost and capacity (storage, replication, cluster size) and operability (counters, logs, alerts, runbooks). Every change made during the review must be regression-tested: the new version must produce results proven equal to the old one. In interviews the same project is presented with the STAR method: Situation, Task, Action and Result, with measured numbers.
Before trusting a pipeline, check that it is right, fast enough, survives failures, does not waste money and can be run by someone else. Every improvement must give exactly the same answers. Then tell the story: what was the problem, what did you do, what did it achieve.
Words you'll meet in this lesson
Architecture at a glance: what the review changed
From our lab (the review experiments)
| Baseline | 104 splits, 104 maps (total map time 2,049,842 ms ≈ 19.7 s per map, mostly start-up), 4 min 39 s |
|---|---|
| Combine #1 | CombineTextInputFormat, 8 MB splits: 3 maps, 38 s; CLEAN 192,799, DUPLICATE 164,120, MALFORMED 400: Streaming passed the byte offset as a key, so the mapper's "order_id" became the offset |
| Combine #2 | + stream.map.input.ignoreKey=true: 44 s, all counters right except LATE 46,646: the file-name variable is set once per task, not per file |
| Fix | Spark compaction tags every line with its file day (1 min 17 s, 4 files) → clean v2: 4 maps, 53 s, output md5 identical to baseline |
| Drill | Killed task JVMs and stopped worker2's DataNode mid-job: failed attempts re-ran, YARN said SUCCEEDED, output identical, but the client printed "Streaming Command Failed!": the job-history file lost its write pipeline |
| Drill fixed | replace-datanode-on-failure.policy=NEVER: 4 failed attempts, 108 maps launched, "completed successfully", same counters and md5, 5 min 48 s |
The review at three levels
Beginner What a review asks
- Is it right? 10/10 checks, and the numbers add up.
- Is it fast enough? Today yes; at 40 canteens the 104 tiny files become 1,040 a month.
- Does it survive failures? We broke a worker on purpose to find out.
- Can you explain it? In two minutes, with numbers, to an interviewer.
Intermediate Why the two speed-ups went wrong
- Keys in Streaming: with plain
TextInputFormatthe keys (byte offsets) are discarded and only the lines are piped to the mapper. WithCombineTextInputFormatour mapper receivedkey TAB lineuntil we setstream.map.input.ignoreKey=true. Our mapper split on commas, so the "order_id" wasoffset TAB order_idand its key was the offset. - File name per task: Streaming copies the job configuration into the environment (dots become underscores) when it starts the Python process. With combined splits one Python process reads many files, so
mapreduce_map_input_fileholds one name for all of them. 44,546 extra orders looked late. - The real fix: stop depending on file names. Put the lineage (file day) into the data during compaction, then any engine and any split size gives the same answer.
Advanced Reliability on a tiny cluster
- Task failures are routine: YARN re-ran the killed attempts on other nodes and the counters of failed attempts were discarded, so totals stayed exact.
- With 3 DataNodes and replication 3, losing one leaves no spare node for an open write pipeline. The client's default policy tries to replace the bad node and gives up: "Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try". The AM's job-history file died; the client could not fetch the final status.
hdfs-default.xmlnotes that on extremely small clusters (3 nodes or less) administrators may want policyNEVER: keep writing to the remaining nodes. On normal clusters keepDEFAULT.- Lesson for operators: the client's exit code is not the job's result. Check
yarn application -status(FINISHED / SUCCEEDED) and the output.
Exam tip: When asked "how do you know an optimisation is safe?", answer: run old and new on the same input and compare every counter and a checksum of the sorted output; ours matched exactly (md5 e4cb8704…), while two faster versions failed the comparison.
The review checklist, with our evidence
| Area | Question | Our evidence | Status |
|---|---|---|---|
| Correctness | Do results equal an independent truth? | check.py 10/10; conservation 357,332 → 357,215 → 354,431 | ✓ |
| Idempotency | Is a re-run safe? | backfill ×2 unchanged; append demo doubled | ✓ |
| Performance | How long, and where does time go? | 104 maps × ~20 s start-up → compaction: 53 s | fixed |
| Scalability | What at 40 canteens, 3 years? | sizing: ~6 GB raw a year at 40 canteens, small for HDFS; the file count is the issue | planned |
| Reliability | Task and node failures? | drill: same output; history bug on 3 nodes found and fixed | fixed |
| Operability | Can someone else run it? | counters per rule, quarantine, scripts, runbook (to write) | open |
| Security | Who may read student IDs? | lab has no Kerberos; production needs it + Ranger (Lesson 12.5) | open |
Ways you will be asked about this project
"Walk me through your project"
Two minutes: problem, data, architecture, one hard problem, result with numbers.
every interview"What was the hardest bug?"
The combine-input bugs: fast, wrong, caught by counters and checks; fixed with lineage in the data.
depth"What if a node fails?"
Our drill: attempts re-ran, output identical; the 3-node history-file problem and its fix.
reliability"How would it scale?"
Sizing with real numbers; small files, partitioning, compaction, more reducers.
designAnimated example: a fast job that was wrong
- The baseline is correct but slow: each tiny file gets a map task, and each task spends most of its 20 seconds starting a JVM and Python.
- With CombineTextInputFormat, Streaming sends "byte offset TAB line" to Python. The mapper keeps everything before the first comma, so the order_id it prints starts with the offset; the key becomes a number. (Offset shown is illustrative.)
- Unrelated orders now share keys and are "deduplicated": the counters are obviously wrong.
- Telling Streaming to ignore the key fixes the lines, but the file name comes from an environment variable set when Python starts, so one name covers many files.
- Everything matches truth except LATE: 46,646 instead of 2,100.
- The fix: compaction writes the file day into every line; the job reads 4 files, runs 4 maps and produces byte-identical output.
Keep a "golden" output from a trusted run (counters + checksum of sorted output). Every change, even a config change, must reproduce it before it ships.
Reviewing, fixing and presenting the capstone
Find where the time goes
The work per map is tiny; the cost is starting a container, a JVM and Python 104 times on 6 vcores (Lesson 7.5's small files problem, now measured).Launched map tasks=104 Total time spent by all map tasks (ms)=20498422,049,842 ms / 104 maps ≈ 19.7 s per map for ~3,400 lines eachTry combining small files
7× faster and badly wrong. Never judge an optimisation by its run time; judge it by its counters.-D mapreduce.input.fileinputformat.split.maxsize=8388608 \ -inputformat org.apache.hadoop.mapred.lib.CombineTextInputFormatnumber of splits:3 real 0m38.006s CLEAN=192799 DUPLICATE=164120 LATE=192799 MALFORMED=400Fix the key, find the second bug
Lines are right now, but LATE uses the file day from-D stream.map.input.ignoreKey=true \real 0m43.720s CLEAN=354135 DUPLICATE=2784 LATE=46646 MALFORMED=296mapreduce_map_input_file, which is exported once per task. A combined split holds about 35 files.Put the lineage into the data
Spark's.withColumn("fday", F.regexp_extract(F.input_file_name(), r"_(\d{4}-\d{2}-\d{2})\.csv$", 1)) …partitionBy("college").text(f"{B}/bronze_monthly/orders")lines: 357228 8607649 /capstone/bronze_monthly/orders/college=KHR/part-00002-….c000.txt 4192061 /capstone/bronze_monthly/orders/college=NRL/part-00000-….c000.txt 5728542 /capstone/bronze_monthly/orders/college=PNL/part-00001-….c000.txt 7329058 /capstone/bronze_monthly/orders/college=VSH/part-00000-….c000.txtinput_file_name()is evaluated per row, so each line gets its own file day. Headers are dropped: 357,332 − 104 = 357,228 lines. Bronze itself stays untouched.Run v2 and prove it equal
The mapper change is two lines (read the tag if present). Same counters, same checksum: this is a regression test you can show a reviewer.hdfs dfs -cat '/capstone/stage/clean/part-*' | sort | md5sum hdfs dfs -cat '/capstone/stage/clean_v2/part-*' | sort | md5sumnumber of splits:4 LATE=2100 e4cb870499eba837b494c5e4e85582dc - e4cb870499eba837b494c5e4e85582dc - real 0m52.718sFailure drill: break worker2 mid-job
The data survived (output md5 identical), but the client "failed". The AM's history file was being written through all 3 DataNodes; with one dead there was no replacement, so the history server never got the job.docker exec lab8-worker2-1 bash -c 'pkill -9 -f "[Y]arnChild"; hdfs --daemon stop datanode'Task Id : attempt_1792730826465_0014_m_000033_0, Status : FAILED Killed by external signal Task Id : attempt_1792730826465_0014_r_000000_0, Status : FAILED ERROR StreamJob - Error Launching job : java.io.IOException: Unknown Job job_1792730826465_0014 Streaming Command Failed! $ yarn application -status application_1792730826465_0014 Final-State : SUCCEEDEDFix and repeat the drill
Four killed attempts re-ran elsewhere (108 launched for 104 maps); counters of failed attempts were discarded, so totals stayed exact. Recovery cost about 70 extra seconds.-D dfs.client.block.write.replace-datanode-on-failure.policy=NEVER \Job job_1792730826465_0015 completed successfully Failed map tasks=4 Launched map tasks=108 CLEAN=354135 LATE=2100 e4cb870499eba837b494c5e4e85582dc - real 5m48.109sWrite it up for interviews
Three lines, all measured. The worked examples and the STAR scenario below turn this into a two-minute answer.Built a Hadoop/Spark batch pipeline for 4 canteens (130 daily files, 3.57 lakh records): dedupe + quality rules in MapReduce, Parquet silver/gold in Spark, UPI reconciliation; 10/10 acceptance checks; cut clean time 4m39s → 53s via compaction (byte-identical output).
Our first drill command was pkill -9 -f YarnChild inside bash -c '…'. The shell's own command line contains "YarnChild", so pkill killed the shell before it could stop the DataNode. Writing the pattern as [Y]arnChild matches the processes but not the command that contains it.
The failure drill: the log and the cluster
Our second drill (with the fix), second by second. Left: the client's terminal. Right: what YARN and HDFS do on the three workers.
- The clean job starts: 104 map tasks queued.
- About 70 seconds in, we break worker2.
- Four running attempts on worker2 die: "Killed by external signal".
- The job keeps going; progress continues from where it was.
- It completes successfully: 108 map attempts for 104 maps, and every counter is exactly right.
- The output is byte-identical to the baseline. Recovery cost about 70 seconds.
- Map tasks run on all three workers; every input block has a replica on each worker.
- Meanwhile the ApplicationMaster writes the job-history file through a pipeline of the three DataNodes.
- We kill worker2's task JVMs and its DataNode. The AM reschedules the failed attempts; readers use the other replicas.
- The history file's pipeline lost a node. With only 3 DataNodes there is no replacement; policy NEVER lets the write continue on two.
- Only successful attempts contribute counters, so the totals are exact.
- In the first drill, without the setting, the history file failed: the client reported failure although YARN and the data said success. (Container squares are symbolic.)
Reviews and interviews at work
1. Production-readiness reviews
Many data teams require a checklist before a pipeline is scheduled in production: tests and reconciliation, run time at expected volume, failure behaviour, alerts, a runbook and an owner, much like our table.
2. Platform upgrades
Before upgrading Hadoop, Spark or a library, teams re-run key jobs on a fixed input and compare outputs and counters with a golden run, the same comparison that caught our combine bugs.
3. Failure drills ("game days")
Operations teams switch off nodes, disks or whole racks in a planned window to prove that jobs recover and alerts fire, and they often find surprises like our job-history problem.
4. Product-company interviews
Interviews often include a project deep dive: one project, many follow-ups ("why that key?", "what if 10×?", "what broke?"). One project known to the last number beats five listed on a CV.
5. Service-company and campus interviews
Expect concept questions across the stack (HDFS, MapReduce, YARN, Hive, Spark) plus "explain your project". The question bank in this lesson maps each common question to the lesson that answers it.
Interview question bank: where each answer lives
The questions that come up again and again in Hadoop and data-engineering interviews, with the lessons that answer them. Revise the linked lesson's "Quick revision" card and its interview questions the night before.
| Question | Lesson |
|---|---|
| What is HDFS? Why 128 MB blocks and 3 replicas? | 5.2 · 5.3 |
| What happens when a DataNode dies? | 5.5 · this lesson's drill |
| How is a file written and read in HDFS? | 6.1 · 6.2 |
| NameNode high availability? | 7.1 |
| What is the small files problem? How do you fix it? | 7.5 · this lesson |
| Explain MapReduce with word count; what happens in shuffle and sort? | 8.4 · 8.6 |
| Combiner vs partitioner? | 9.1 · 9.2 |
| How do you join two big datasets? | 9.5 · 9.6 |
| Parquet vs ORC vs Avro; splittable compression? | 10.2 · 10.3 |
| YARN architecture; life of an application? | 11.1 · 11.2 |
| Capacity vs Fair scheduler; container memory errors? | 11.3 · 11.4 |
| How do you size a cluster? | 12.3 |
| Kerberos and Ranger in one minute? | 12.5 |
| Hadoop vs Spark? Is Hadoop dead? | 13.2 · 2.1 |
| What is a lakehouse? Idempotent pipelines? | 13.4 · 14.2 |
| Walk me through your project. | this lesson: STAR answer below |
Simplified; organisations differ in process. Kadak Canteens and its data are fictional.
Sources (checked 23 Oct 2026): Apache Hadoop: Hadoop Streaming (keys discarded with TextInputFormat; configuration copied to the environment) · Cloudera Community: replace-datanode-on-failure.policy on a 3-DataNode cluster (quotes hdfs-default.xml)
Why one file name for many files breaks "late"
A simulation of our second combine attempt. One Python process reads lines from three files but is told only one file name at start-up, as Streaming does with its environment.
files = {"orders_KHR_2026-09-12.csv": ["KHR-0912-1,2026-09-12", "KHR-0912-2,2026-09-12"],
"orders_KHR_2026-09-14.csv": ["KHR-0912-9,2026-09-12", "KHR-0914-1,2026-09-14"],
"orders_KHR_2026-09-15.csv": ["KHR-0915-1,2026-09-15"]}
def late_count(lines_with_day):
return sum(1 for fday, line in lines_with_day if fday > line.split(",")[1])
# baseline: one process per file, each knows its own file name
per_file = [(name[-14:-4], line) for name, lines in files.items() for line in lines]
# combined split: one process, one file name for the whole split (here: the last file)
told = list(files)[-1][-14:-4]
combined = [(told, line) for lines in files.values() for line in lines]
# fix: the file day travels inside every line (compaction)
tagged = [(name[-14:-4], line) for name, lines in files.items() for line in lines]
print("one file per map :", late_count(per_file))
print("combined, env var:", late_count(combined))
print("lineage in data :", late_count(tagged))told = list(files)[-1][-14:-4]Like mapreduce_map_input_file: fixed when the process starts, wrong for the other files in the split.tagged = [(name[-14:-4], line) for name, lines in files.items() for line in lines]compact.py does this with input_file_name(): each line carries its own file day.
Run it on your PC: the review experiments
Same container and folder as Lessons 14.1–14.2, after the baseline clean job. Job logs are filtered to the lines that matter (splits, attempts, counters, timing).
1 · Combine small files (first try: wrong)
# combine_job.sh: the same clean job, but pack the 104 small files into a few splits
hdfs dfs -rm -r -f -skipTrash /capstone/stage/clean_combined
mapred streaming \
-D mapreduce.job.name=capstone-clean-combined \
-D mapreduce.job.reduces=3 \
-D mapreduce.input.fileinputformat.split.maxsize=8388608 \
-files clean_mapper.py,clean_reducer.py,capstone_raw/menu.csv \
-inputformat org.apache.hadoop.mapred.lib.CombineTextInputFormat \
-input '/capstone/bronze/orders/college=*' \
-output /capstone/stage/clean_combined \
-mapper clean_mapper.py -reducer clean_reducer.py3 maps instead of 104 and 7× faster, but CLEAN is 192,799 instead of 354,135: the mapper received "offset TAB line".
2 · Combine with ignoreKey (second try: LATE wrong)
# combine_job.sh: the same clean job, but pack the 104 small files into a few splits
hdfs dfs -rm -r -f -skipTrash /capstone/stage/clean_combined
mapred streaming \
-D mapreduce.job.name=capstone-clean-combined \
-D mapreduce.job.reduces=3 \
-D mapreduce.input.fileinputformat.split.maxsize=8388608 \
-D stream.map.input.ignoreKey=true \
-files clean_mapper.py,clean_reducer.py,capstone_raw/menu.csv \
-inputformat org.apache.hadoop.mapred.lib.CombineTextInputFormat \
-input '/capstone/bronze/orders/college=*' \
-output /capstone/stage/clean_combined \
-mapper clean_mapper.py -reducer clean_reducer.pyEvery counter equals truth except LATE: one file name per task for about 35 files.
3 · Compact with lineage, then clean v2
# compact.py: 104 small daily files -> one file per canteen, each line tagged with the day it arrived
from pyspark.sql import SparkSession, functions as F
spark = SparkSession.builder.appName("capstone-compact").getOrCreate()
B = "hdfs://namenode/capstone"
lines = (spark.read.text(f"{B}/bronze/orders/college=*")
.withColumn("fday", F.regexp_extract(F.input_file_name(), r"_(\d{4}-\d{2}-\d{2})\.csv$", 1))
.withColumn("college", F.regexp_extract(F.input_file_name(), r"college=(\w+)", 1))
.where(~F.col("value").startswith("order_id,")))
(lines.select(F.concat_ws("\t", "fday", "value").alias("value"), "college")
.repartition("college").write.mode("overwrite").partitionBy("college").text(f"{B}/bronze_monthly/orders"))
print("lines:", spark.read.text(f"{B}/bronze_monthly/orders").count())
export SPARK_HOME=/tmp/opt/spark-4.0.4-bin-hadoop3 HADOOP_CONF_DIR=/opt/hadoop/etc/hadoop
$SPARK_HOME/bin/spark-submit --master yarn --num-executors 3 --executor-memory 768m --driver-memory 768m compact.py
hdfs dfs -ls -R /capstone/bronze_monthly/orders | grep part- | awk '{print $5, $8}'
# clean_mapper_v2.py = clean_mapper.py plus two lines after line.rstrip():
# if "\t" in line:
# fday, line = line.split("\t", 1)
# clean_job_v2.sh: the clean job on 4 compacted files (one per canteen)
hdfs dfs -rm -r -f -skipTrash /capstone/stage/clean_v2
mapred streaming \
-D mapreduce.job.name=capstone-clean-v2 \
-D mapreduce.job.reduces=3 \
-files clean_mapper_v2.py,clean_reducer.py,capstone_raw/menu.csv \
-input '/capstone/bronze_monthly/orders/college=*' \
-output /capstone/stage/clean_v2 \
-mapper clean_mapper_v2.py -reducer clean_reducer.py
hdfs dfs -cat '/capstone/stage/clean/part-*' | sort | md5sum
hdfs dfs -cat '/capstone/stage/clean_v2/part-*' | sort | md5sumSpark also printed a harmless "FileNotFoundException … college=*" while checking the glob for a metadata folder (removed). The two md5 lines are the baseline and v2 outputs: identical.
4 · Failure drill (first run: client fails, job succeeds)
# drill.sh: run the clean job and break worker2 while it runs (run from the host, not a container)
docker exec -w /tmp/capstone lab8-namenode-1 bash clean_job.sh > tx_drill.txt 2>&1 &
sleep 75
docker exec lab8-worker2-1 bash -c 'pkill -9 -f "[Y]arnChild"; hdfs --daemon stop datanode' # kill running tasks + DataNode
wait
docker exec lab8-worker2-1 hdfs --daemon start datanode
# afterwards, inside the namenode container:
# yarn application -status application_<id> | grep State
# yarn logs -applicationId application_<id> -log_files syslog | grep JobHistoryEventHandlerThe output in HDFS was byte-identical to the baseline (same md5). Only the job-history file was lost, so the client could not report the final status.
5 · Failure drill with the small-cluster setting
# clean_job_small.sh: clean_job.sh for a cluster of only 3 DataNodes (keep writing if one dies)
hdfs dfs -rm -r -f -skipTrash /capstone/stage/clean
mapred streaming \
-D mapreduce.job.name=capstone-clean \
-D mapreduce.job.reduces=3 \
-D dfs.client.block.write.replace-datanode-on-failure.policy=NEVER \
-files clean_mapper.py,clean_reducer.py,capstone_raw/menu.csv \
-input '/capstone/bronze/orders/college=*' \
-output /capstone/stage/clean \
-mapper clean_mapper.py -reducer clean_reducer.py4 failed attempts, 108 map attempts launched, every counter exact, same checksum. Same drill.sh, calling clean_job_small.sh.
A regression check, line by line
The check that rejected the second combine attempt: compare every counter of a new run with the trusted baseline and list the differences.
Worked examples: real problems, solved and traced in memory
Three calculations a reviewer or interviewer will ask you to do on the spot.
How much of the job was start-up?
Problem statement
The baseline spent 2,049,842 ms in 104 map tasks of about 3,435 lines each. If the real processing is about 1 second per map, what share of the map time was overhead?
2,049,842 ms · 104 maps · ~1 s real work per map
avg seconds per map, overhead %: (19.7, 95)
Approach
- Average map time = total ÷ maps.
- Overhead = average − real work.
- Share = overhead ÷ average.
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.
Size the system for 40 canteens and 3 years
Problem statement
Per canteen per month our raw data is (20.9 + 28.8) ÷ 4 MB. Kadak plans 40 canteens and wants 3 years online with replication 3. How much HDFS space, and how many daily order files a year without compaction?
12.4 MB per canteen-month · 40 canteens · 36 months · rf 3
raw GB, with replicas GB: (17.5, 52.4) order files per year: 12480
Approach
- Raw = per canteen-month × canteens × months.
- Disk = raw × 3.
- Files = canteens × open days per year.
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.
What did the failure cost?
Problem statement
The drill killed 4 running map attempts on worker2. Each map takes about 20 s; the job ran 5 min 48 s instead of the baseline 4 min 39 s. How much of the extra time is re-running the 4 killed maps in parallel, and how much is something else?
4 attempts × ~20 s · 348 s vs 279 s
extra seconds, not explained by re-runs: (69, 49)
Approach
- Extra time = drill − baseline.
- Re-run cost ≈ one wave of 20 s.
- The rest: detecting failures, fewer DataNodes to read from, run-to-run noise.
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 14.3 in one screen
- Review areas: correctness, idempotency, performance, scalability, reliability, cost, operability, security.
- Baseline: 104 maps × ~19.7 s, 4 min 39 s; the work per map was tiny: start-up dominated.
- Combine #1: 3 maps, 38 s, wrong (offset keys). Combine #2 (ignoreKey): 44 s, LATE 46,646 (env var once per task).
- Fix: compact.py tags each line with its file day → 4 maps, 53 s, identical counters and md5 e4cb8704…
- Drill: killed tasks + DataNode: attempts re-ran, output identical; 3-node history-file failure fixed with policy NEVER.
- Sizing: ~6 GB raw a year at 40 canteens; files, not bytes, are the scaling problem.
- Interview: STAR with numbers, one hard bug, one failure story, one design trade-off.
Interview and viva questions
Walk me through your capstone project.
Situation: four college canteens sent 130 daily files a month (POS orders, UPI payments, a menu) and could not trust their totals. Task: daily revenue per canteen, item mix, peak hours and UPI reconciliation, proven correct. Action: landed raw files in HDFS, wrote a MapReduce Streaming job keyed on order_id to deduplicate and apply quality rules with counters, used Spark to write Parquet silver partitioned by day and five gold tables, and wrote ten acceptance checks against a trusted total. Result: 10/10 checks, ₹43,710 of undercharging and a ₹6,854 UPI gap found, and the clean job cut from 4 min 39 s to 53 s with byte-identical output.
What was the hardest problem?
Making the clean job faster. Packing small files with CombineTextInputFormat made it 7× faster but wrong: the Python mapper received byte-offset keys, and after fixing that the input-file environment variable named only one file per combined split, so 46,646 orders looked late instead of 2,100. Counters and acceptance checks caught both; the fix was compacting the files and writing each line's source day into the data.
How does Hadoop handle a node failure during a job?
The ApplicationMaster re-runs failed or lost task attempts on other nodes, and HDFS readers switch to other replicas. Counters from failed attempts are discarded, so results stay exact. In our drill 4 attempts were killed, 108 launched for 104 maps, and the output was byte-identical. On a 3-DataNode cluster open writes can fail when one DataNode dies unless the replace-datanode-on-failure policy is NEVER.
How do you prove that an optimisation did not change the results?
Run old and new versions on the same input, compare every counter and a checksum of the sorted output, and run the acceptance checks. Keep that golden output for future changes.
Why did you use both MapReduce and Spark?
MapReduce made deduplication explicit as a shuffle on order_id and shows the classic Hadoop job model many companies still run; Spark was better for typed columns, Parquet, joins and several aggregations in one job. In a new project I would likely do it all in Spark (dropDuplicates or a window) with the same rules and checks.
What would you change for production?
Schedule it in Airflow with retries and an SLA; compact daily; move silver to a table format such as Iceberg for safe corrections; add Kerberos and Ranger for access to student IDs; alert on check failures and on the UPI gap table; write a runbook that includes checking the YARN final status, not only the client exit code.
Practice problems
Review calculations and interview drills using the experiment numbers.
Speed-up
Baseline 4 min 39 s, compacted 53 s. What is the speed-up? Include the 1 min 17 s compaction: is it still worth it?
279 s · 53 s · 77 s
5.3 x without compaction, 2.1 x including it
Show solution and explanation
print(round(279 / 53, 1), "x without compaction,", round(279 / (53 + 77), 1), "x including it")How it works: 5.3× for the job, 2.1× even counting compaction; compaction runs once while the clean job may run many times.
Lines per map
357,332 lines over 104 maps, or 357,228 lines over 4 maps. How many lines per map task?
two layouts
3436 89307
Show solution and explanation
print(round(357332 / 104), round(357228 / 4))How it works: 3,436 vs 89,307 lines per task: the second is a sensible amount of work per task.
Extra attempts
The drill launched 108 map attempts for 104 maps with 4 failed. How many attempts succeeded?
108 · 4
104
Show solution and explanation
print(108 - 4)How it works: 104: one successful attempt per map, which is what the counters are built from.
How wrong was combine #1?
Compute the error in CLEAN and DUPLICATE against truth.
192,799 vs 354,135 · 164,120 vs 2,784
CLEAN -161336 -46% DUPLICATE 161336 +5795%
Show solution and explanation
for name, got, want in [("CLEAN", 192799, 354135), ("DUPLICATE", 164120, 2784)]:
print(name, got - want, f"{(got - want) / want:+.0%}")How it works: Orders −46%, duplicates +5,795%: wrong enough to notice, if anyone looks at the counters.
Average file size after compaction
The four compacted files were 8,607,649, 4,192,061, 5,728,542 and 7,329,058 bytes. Average size, and share of a 128 MB block?
4 sizes
6.5 MB, 4.8% of a block
Show solution and explanation
sizes = [8607649, 4192061, 5728542, 7329058]
avg = sum(sizes) / len(sizes)
print(round(avg / 1e6, 1), "MB,", f"{avg / (128 * 1024 * 1024):.1%} of a block")How it works: About 6.5 MB, 4.8% of a block: still small but fine for one month; at 40 canteens monthly files would be about 65 MB.
STAR in numbers
Print a one-line result statement from a dictionary of measured numbers.
your project numbers
10/10 checks; found Rs 43,710 undercharged and a Rs 6,854 UPI gap; clean job 279s -> 53s (5.3x)
Show solution and explanation
r = {"checks": "10/10", "undercharged": 43710, "upi_gap": 6854, "before_s": 279, "after_s": 53}
print(f"{r['checks']} checks; found Rs {r['undercharged']:,} undercharged and a Rs {r['upi_gap']:,} UPI gap; "
f"clean job {r['before_s']}s -> {r['after_s']}s ({r['before_s'] / r['after_s']:.1f}x)")How it works: Results with numbers are what interviewers remember.
Is the pipeline worth it?
The accountant spent 4 days a month reconciling. The pipeline found ₹43,710 undercharged and ₹6,854 unconfirmed in one month. Accountant cost ₹2,500/day. What is the first month's value?
4 days × ₹2,500 · ₹43,710 · ₹6,854
Rs 60,564 in the first month (illustrative)
Show solution and explanation
time_saved = 4 * 2500
print(f"Rs {time_saved + 43710 + 6854:,} in the first month (illustrative)")How it works: About ₹60,600 in month one; the price bug, once fixed, keeps paying back.
Plan a failure test matrix
List failure drills for the pipeline with what to observe, and print them as a checklist.
components
1. kill a map task JVM -> attempt re-runs, counters exact 2. stop a DataNode -> reads use replicas; writes continue (policy on small clusters) 3. stop a NodeManager -> containers lost after expiry, tasks rescheduled 4. fill a disk to 97% -> write failures; alert before it happens (Lesson 13.4 incident) 5. re-run a finished day -> idempotent: totals unchanged
Show solution and explanation
drills = [("kill a map task JVM", "attempt re-runs, counters exact"),
("stop a DataNode", "reads use replicas; writes continue (policy on small clusters)"),
("stop a NodeManager", "containers lost after expiry, tasks rescheduled"),
("fill a disk to 97%", "write failures; alert before it happens (Lesson 13.4 incident)"),
("re-run a finished day", "idempotent: totals unchanged")]
for i, (what, observe) in enumerate(drills, 1):
print(f"{i}. {what:<22} -> {observe}")How it works: A drill without a written expectation is just breaking things; each line is a test with a pass condition.
Real-world scenario questions (asked in company interviews)
Project deep-dive questions, the kind asked for 20–30 minutes in data-engineering interviews.
"Tell me about a project you are proud of" (2 minutes)
The opening question of most interviews.
The interviewer is checking: clarity, ownership and numbers.
Show a strong answer
- S: Four college canteens, 130 daily files a month from tills and the UPI gateway, totals never matched.
- T: Daily revenue per canteen, item mix, peak hours and UPI reconciliation that the owner could trust.
- A: HDFS bronze; MapReduce Streaming keyed on order_id for dedupe and rules with counters; Spark silver Parquet by day and gold tables; ten acceptance checks against trusted totals.
- R: 10/10 checks; found ₹43,710 undercharged at one canteen and a ₹6,854 gateway gap in one hour; cut the clean job from 4 min 39 s to 53 s with byte-identical output.
"What went wrong, and how did you find it?"
The follow-up to every project story.
The interviewer is checking: honesty and debugging method.
Show a strong answer
- A speed-up (combining small files) produced wrong counters twice: offset keys, then one file name per task making 46,646 orders "late".
- Found by comparing counters with the baseline and running acceptance checks, not by looking at run time.
- Fixed by carrying the source file day in the data; proved with identical counters and output checksum.
"The business grows 10×. What breaks first?"
40 canteens, 3 years of history.
The interviewer is checking: sizing with numbers, knowing bottlenecks.
Show a strong answer
- Bytes are fine: about 6 GB raw a year, about 52 GB with replicas for 3 years.
- Files are not: 12,480 order files a year would mean 1,040 map tasks a month and many NameNode objects; compact daily and partition silver by day.
- Then: more reducers for the clean shuffle, Spark for everything, an Iceberg table for corrections, Airflow for scheduling and backfills.
"Your scheduler marked the job failed, but the data looks complete. What do you do?"
The client exited with an error at the very end.
The interviewer is checking: not trusting a single signal.
Show a strong answer
- Check the application's final state in YARN (in our drill: FINISHED / SUCCEEDED), the output's _SUCCESS marker, counters and checks.
- Read the AM log: ours showed the job-history file lost its write pipeline when a DataNode died on a 3-node cluster.
- Decide from the checks (the job is idempotent, so a re-run is safe anyway); fix the root cause and add it to the runbook.
"I see Hadoop on your resume. Is it still relevant?"
A common challenge in 2026 interviews.
The interviewer is checking: balanced judgement, not tool loyalty.
Show a strong answer
- New projects mostly use Spark, object storage and lakehouse tables, but many banks, telecoms and governments still run Hadoop clusters, and migrations need people who understand them (Lessons 2.1, 13.3).
- The ideas carry over directly: blocks and replicas, data locality, shuffle by key, small files, YARN scheduling, idempotent pipelines.
- My capstone shows both: a MapReduce job and a Spark job on the same cluster, verified the same way.