One lakh answer sheets, one week
Arjun Sir has been made the head examiner for the university's semester exams: 1,00,000 answer sheets, results due in one week. One teacher checks about 50 sheets a day. Riya does the maths and looks worried: one teacher would need 2,000 days.
Sir, with 100 teachers it's 1,000 sheets each: 20 days. Still too long!
Then 400 teachers: 250 sheets each, five days. But see what else we must organise. Who decides which bundle goes to whom? What if a teacher falls ill on day three? And we don't carry one lakh sheets to one room. Teachers go to the evaluation centres where the sheets already are.
And at the end, teachers don't send back the sheets, just the marks list. You only merge the lists.
Exactly. You've just described distributed computing: split the data, process the parts at the same time, move the workers to the data, send back small results, combine them, and plan for someone dropping out.
So Hadoop and Spark are… a very organised exam department?
A very fast and very strict one. Let's build a small one in Python.
Sir, at TiffinGo the orders are split by city. Mumbai alone is bigger than four other cities put together. Does that matter?
It matters a lot, Kabir. The job finishes only when the slowest piece finishes. Today you'll see that problem, called data skew, and also what a cluster does when it can't tell whether a machine is slow or dead.
What is distributed computing?
Distributed computing is a model in which a task is divided into smaller parts that are processed in parallel on multiple independent computers (nodes) connected by a network. The nodes coordinate by passing messages, and the system presents the combined result as if one computer had done the work. Key ideas are partitioning of data, parallel processing, data locality, aggregation of partial results, and fault tolerance through replication and re-execution.
Cut a huge job into pieces, give each piece to a different computer, let them all work at the same time, then put the answers together. If one computer breaks, give its piece to another.
Words you'll meet in this lesson
The five-step pattern
| 1. Partition | Split the data into blocks and spread them across nodes (with copies) |
|---|---|
| 2. Schedule | The master assigns one task per block, preferably on the node that holds it |
| 3. Process in parallel | Each worker processes only its own block, at the same time as all others |
| 4. Combine | Small partial results are sent over the network and merged |
| 5. Recover | If a worker fails, its task is re-run on another node that has a copy |
Distributed computing at three levels
Beginner Teamwork with rules
Distributed computing is teamwork with strict rules. Think of a wedding kitchen: one head cook (the master) plans the menu and gives each cook (a worker) one dish. Everyone cooks at the same time, and the head cook puts the plates together at the end.
- You use it daily: a UPI payment touches your app's servers, your bank, NPCI and the receiver's bank. A Google search is answered by thousands of machines, each searching its own slice of the web.
- The golden rules: split fairly, work in parallel, send back small answers, and have a plan when someone drops out.
Intermediate How the coordination actually works
- Heartbeats: every worker tells the master “I'm alive, task 60% done” every few seconds. No heartbeat for a while → the worker is treated as failed and its task is re-run elsewhere.
- Combiners: each worker first combines its own results (local counts) so that only small summaries travel over the network.
- Shuffle: when results must be grouped by key across machines (all Mumbai orders together), data is exchanged between workers. This network-heavy step is often the slowest part of a job.
- Easy vs tricky aggregations: sum, count, min and max combine directly. An average needs (sum, count) pairs; distinct counts need sets (or estimates); a median needs much more care.
- Speculative execution: if one task runs much slower than the rest, the master starts a backup copy on another node and uses whichever finishes first.
Advanced Partial failure, skew and the fallacies of distributed computing
- Partial failure: in one computer, things work or crash together. In a cluster, some parts fail while others keep going, and a silent node may be dead, slow or just cut off. A timeout can't tell which.
- Idempotent tasks: because a task may run twice (a retry plus a “dead” worker that comes back), outputs must be written so that running twice gives the same result, for example by committing only one attempt per task.
- Data skew: one hot key (one city, one celebrity's account) makes one partition huge. Fixes: split the hot key into sub-keys (salting), choose a better partition key, or handle the hot key separately.
- No global clock: each machine's clock drifts a little, so “which event happened first” across machines is hard. Systems use sequence numbers or logical clocks instead.
- The fallacies of distributed computing (from engineers at Sun Microsystems): it is wrong to assume the network is reliable, latency is zero, bandwidth is infinite, the network is secure, the topology doesn't change, there is one administrator, transport cost is zero, and the network is the same everywhere. Every big data tool is designed around these.
- Module 3 goes deeper into partitioning, replication, consistency and the CAP theorem.
Exam tip: Learn the pattern as partition → parallel process → aggregate, plus the two supporting ideas data locality (“move computation to data”) and fault tolerance (replication + re-execution). This is the foundation of MapReduce, which you'll study in the Hadoop course.
Building blocks and shapes of distributed systems
Six building blocks
Partitioning
Split data into blocks, by position (every 128 MB) or by key (all orders of one customer together).
SplitParallelism
Many tasks run at the same time on different nodes and cores.
SpeedData locality
Send the small program to the big data, not the big data to the program.
Less networkAggregation
Workers return small partial results that are merged. Choose results that can be combined correctly.
CombineReplication
Each block is stored on more than one node (HDFS keeps 3 copies by default).
SafetyCoordination
Someone must know which task runs where, which finished and which failed.
ControlTwo kinds of parallelism
| Data parallelism | Task parallelism | |
|---|---|---|
| Idea | Same operation on different pieces of data | Different operations at the same time |
| Example | Count orders in 100 blocks, one block per node | One node cleans data while another trains a model and a third builds a report |
| Used by | MapReduce, Spark, most big data processing | Workflow tools like Airflow running independent steps together |
Two common architectures
| Master–worker | Peer-to-peer | |
|---|---|---|
| How | One coordinator assigns work; workers report back | All nodes are equal and share responsibilities |
| Examples | HDFS (NameNode + DataNodes), Spark (driver + executors), YARN | Cassandra, BitTorrent |
| Strength | Simple to reason about and schedule | No single coordinator to fail |
| Weakness | Master must be protected (standby / high availability) | Harder to keep all nodes in agreement |
Not everything combines by adding. Sums and counts can be added up from partial results. An average of averages is wrong when blocks have different sizes, and counts of distinct customers can't be added either. You'll see both traps in the code below.
Count chai orders on three machines
Partition the data
In a real cluster, a 30 GB file becomes 240 blocks of 128 MB, and each block is stored on three different nodes.orders = ["chai", "samosa", "chai", "poha", "chai", "samosa"] blocks = [orders[0:2], orders[2:4], orders[4:6]] print(blocks)[['chai', 'samosa'], ['chai', 'poha'], ['chai', 'samosa']]Each machine counts only its block
All three machines run this at the same time, each on the block stored on its own disk. Nothing is sent over the network yet: that's data locality.block = ["chai", "poha"] # machine 2's block counts = {} for item in block: counts[item] = counts.get(item, 0) + 1 print(counts){'chai': 1, 'poha': 1}Send back small results
Machine 1 sends
Each block may be 128 MB, but its partial result is a few bytes: one number per item. The network carries the small summaries, not the data.{'chai': 1, 'samosa': 1}, machine 2 sends{'chai': 1, 'poha': 1}, machine 3 sends{'chai': 1, 'samosa': 1}.Combine
The same answer as counting all six orders on one machine. Counts combine by simple addition, which is why counting is the classic first distributed program.partials = [{"chai": 1, "samosa": 1}, {"chai": 1, "poha": 1}, {"chai": 1, "samosa": 1}] total = {} for p in partials: for item, n in p.items(): total[item] = total.get(item, 0) + n print(total){'chai': 3, 'samosa': 2, 'poha': 1}A machine fails
Machine 2 crashes halfway through its block.
The master notices that machine 2 stopped reporting (it misses its regular heartbeat). Another node holds a copy of block 2, so the master re-runs just that one task there. Blocks 1 and 3 are not redone.
A distributed job: what Riya sees, and what the cluster does
Step through the six frames. Left: the job's progress page. Right: the master and four workers.
- Riya submits a job that counts orders in a 512 MB file. She doesn't choose machines; she just submits.
- The page shows four tasks running at the same time.
- Task 3 turns red: worker lost. The other three continue.
- Task 3 starts again automatically. Three tasks are already finished.
- All four tasks finish, and the results are being combined.
- The job succeeds. The totals are exactly what one machine would have produced, even with the failure.
- The master splits the work into four tasks and assigns each to the worker that already stores the block: data locality.
- All four workers process their own block in parallel. Nothing big moves over the network.
- Worker 3 stops sending heartbeats. The master marks task 3 as failed. Tasks 1, 2 and 4 are not affected.
- Worker 1 holds a replica of block 3, so the master re-runs only task 3 there.
- Each worker sends back a tiny partial result: a few counts, not 128 MB of data.
- The master aggregates the partial results into the final answer.
What can go wrong in a distributed system
| Problem | What it means | Usual fix |
|---|---|---|
| Node failure | A machine crashes mid-task | Heartbeats detect it; re-run the task on a replica |
| Network delay | Messages are slow or lost | Timeouts, retries, keep data local |
| Stragglers / skew | One task is much slower than the rest | Split data more evenly; run a backup copy of slow tasks |
| Wrong combine | Partial results merged incorrectly | Send sum and count, not averages; use sets for distinct values |
| Duplicate work | A retried task writes its output twice | Make tasks idempotent: running twice gives the same result |
Real-time scenarios: a cluster at work
Three live scenarios: whether to move the data or the program, a job held up by one giant partition, and an advanced one about a worker that goes silent and then comes back.
Real-time scenario 1Move the data, or move the program?
A worker needs to process a 100 GB block stored on another machine. It could copy the data over the network, or the program could be sent to the machine that has the data. Watch the difference.
- The choice: node A has the program; node B has the 100 GB block. Copy the data, or send the program?
- Copy the data: 100 GB over a 1 Gbps link (about 125 MB/s) …
- … takes about 14 minutes, and while it happens the network is busy for everyone else. Multiply by hundreds of blocks and the network becomes the bottleneck.
- Send the program: the code is a few kilobytes and arrives almost instantly.
- Result: node B processes the data from its own disk and returns a tiny result. This is data locality, a core design idea of Hadoop and Spark.
Data skew: one giant partition holds up the whole job
TiffinGo's daily report splits orders by city, one city per worker. Mumbai has six times more orders than any other city. Watch the progress bars, then see two standard fixes.
- Start: the data is partitioned by city, one city per worker. It looks fair, but Mumbai's partition is 12 GB and the others are 2 GB.
- 2 minutes: four workers are done. W5 has finished only a sixth of Mumbai.
- 12 minutes: the job ends only when W5 ends. Four machines were idle for 10 minutes. This is data skew, and W5 is a straggler.
- Fix 1, salting: Mumbai's orders are split into sub-keys Mumbai-1 to Mumbai-6 and spread over all workers. Each worker gets about 4 GB and the job finishes in about 4 minutes. The six Mumbai results are added at the end.
- Fix 2, speculative execution: when the data is fair but one machine is slow (say, a failing disk), the master runs a backup copy of that task on a free worker and keeps the first result. (Times are illustrative.)
Slow or dead? The worker that came back
A cluster has no way to look inside a silent machine. Watch how heartbeats and timeouts handle a worker that goes quiet, and the surprising bug that appears when it comes back.
- Heartbeats: each worker sends a small “I'm alive, here's my progress” message to the master every 3 seconds. W3 is running task 7.
- Silence: W3's heartbeats stop. The master cannot know whether W3 crashed, is overloaded, or has lost its network connection.
- Timeout: after 30 seconds without a heartbeat, W3 is treated as failed. Task 7 is re-run on W4, which has a replica of the same data block.
- The surprise: W3 was only cut off from the network. It reconnects and also finishes task 7. Without care, the same orders would be counted twice.
- The fix: task outputs go to a temporary location, and the master commits exactly one attempt per task. Running a task twice now gives the same final result: the task is idempotent. (Timings are illustrative.)
Where distributed computing runs every day
1. Spark and Hadoop jobs
A Spark job is exactly this lesson: the driver (master) splits the data into partitions, executors (workers) process them in parallel, and failed tasks are retried. Hadoop MapReduce does the same with map and reduce tasks.
2. Distributed databases
Databases like Cassandra and MongoDB split tables across nodes by key (called sharding): customers A–M on some nodes, N–Z on others, each with replicas. A query for one customer goes straight to the right node.
3. Event streams with Kafka
Kafka splits each stream of events into partitions stored on different brokers, with replicas. Many consumers read different partitions in parallel, so millions of events per second can be handled.
4. Content delivery networks (CDNs)
Videos and images are copied to servers in many cities, and your phone fetches from the nearest one. That's replication and locality applied to users instead of processing.
5. Large-scale testing and builds
Big software companies run thousands of automated tests on every code change. They are split across many machines and run in parallel, so results come back in minutes instead of hours.
Examples are simplified to show the pattern. Real systems add many more details.
A tiny cluster in Python
Twelve canteen orders, three “machines”. The program partitions the data, lets each machine count only its own block, then combines the partial counts. On a real cluster the three counts would run at the same time on different computers. Then try the challenges: real parallel speed-up, a failure, Amdahl's law, a straggler and the average-of-averages trap.
orders = ["chai", "samosa", "chai", "vada pav", "chai", "poha",
"samosa", "chai", "coffee", "chai", "samosa", "poha"]
machines = 3
# 1. PARTITION: split the data into one block per machine
size = len(orders) // machines
blocks = [orders[i * size:(i + 1) * size] for i in range(machines)]
# 2. PROCESS: each machine counts only its own block
def count_block(block):
counts = {}
for item in block:
counts[item] = counts.get(item, 0) + 1
return counts
partials = [count_block(b) for b in blocks]
for i, p in enumerate(partials, 1):
print("Machine", i, "->", p)
# 3. COMBINE: merge the small partial results
total = {}
for p in partials:
for item, n in p.items():
total[item] = total.get(item, 0) + n
print("Final ->", total)orders[i * size:(i + 1) * size]Slicing gives machine 1 items 0–3, machine 2 items 4–7, machine 3 items 8–11: one partition each.def count_block(block):The same small function is sent to every machine. Shipping code to the data is data locality.partials = [count_block(b) for b in blocks]Here the calls run one after another. On a cluster each call runs on a different node at the same time.total[item] = total.get(item, 0) + nCounts combine by adding. That is why counting is the classic first distributed program.
Run it on your PC: use all your CPU cores
A plain Python loop uses one core. This program counts prime numbers below 20 lakh, first on one core, then split into blocks and run on all your cores with multiprocessing. It is a real parallel program on one machine, using the same split–process–combine pattern a cluster uses across machines. Save it as use_all_cores.py.
Split the work across cores
import os, time
from multiprocessing import Pool
def count_primes(block):
"""Count the prime numbers in one block (a range of numbers)."""
lo, hi = block
count = 0
for n in range(max(lo, 2), hi):
if all(n % d for d in range(2, int(n ** 0.5) + 1)):
count += 1
return count
if __name__ == "__main__": # needed on Windows for multiprocessing
cores = os.cpu_count()
N = 2_000_000
step = N // cores
blocks = [(i * step, (i + 1) * step) for i in range(cores)]
start = time.time()
serial = sum(count_primes(b) for b in blocks)
t1 = time.time() - start
print(f"Serial (1 core) : {serial} primes in {t1:.1f} s")
start = time.time()
with Pool(cores) as pool:
parallel = sum(pool.map(count_primes, blocks))
t2 = time.time() - start
print(f"Parallel ({cores} cores): {parallel} primes in {t2:.1f} s")
print(f"Speed-up: {t1 / t2:.1f}x")
# Big numbers take longer to test, so the last block is the slowest.
# Many small blocks spread the work more evenly between the cores.
small = N // (cores * 8)
many = [(i * small, (i + 1) * small) for i in range(cores * 8)]
start = time.time()
with Pool(cores) as pool:
balanced = sum(pool.map(count_primes, many))
t3 = time.time() - start
print(f"Parallel, {len(many)} small blocks: {balanced} primes in {t3:.1f} s, speed-up {t1 / t3:.1f}x")
# In a terminal: python use_all_cores.pyRecorded on a small 2-core cloud machine. On a 4- or 8-core laptop the speed-up will be larger, and your times will differ. Notice two real-world lessons: the answer is identical every time, and the speed-up is less than the number of cores, partly because bigger numbers take longer to test, so the last block is a straggler. Many smaller blocks balance the work better.
Partition, count, combine, one line at a time
The mini cluster with six orders and two machines. Watch count_block() get its own frame for each block and return a small dictionary, then watch the partial results merge into total.
Worked examples: real problems, solved and traced in memory
Four problems, from beginner to advanced: splitting fairly, combining correctly, recovering from failures and fixing a skewed partition. Each runs live and is traced in memory.
Split records into near-equal blocks
Problem statement
Split 10 records across 3 machines so that block sizes differ by at most one. (Plain slicing with 10 // 3 would leave one record out.)
records R1..R10, 3 machines
Machine 1 : ['R1', 'R2', 'R3', 'R4'] Machine 2 : ['R5', 'R6', 'R7'] Machine 3 : ['R8', 'R9', 'R10']
Approach
- Use
divmod(10, 3)to get the base size (3) and the remainder (1). - The first “remainder” blocks get one extra record.
- Track the start position as you cut each block.
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.
The correct distributed average
Problem statement
Two stores compute their bills locally. Store A has 8 bills of ₹100, store B has 2 bills of ₹1,000. Combine their results to get the true average bill.
A: 8 × 100 · B: 2 × 1000
Partials: [(800, 8), (2000, 2)] True average: 280.0
Approach
- Each store sends back (sum, count), not its average.
- Add all the sums and all the counts.
- Divide once at the end.
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.
Reassign failed tasks
Problem statement
Four tasks were sent to workers. Task T3 failed. The master must find failed tasks and give them to a healthy worker, without re-running tasks that succeeded.
T1 done, T2 done, T3 failed, T4 done · healthy workers W1, W2
Retrying T3 on W1 All done: True
Approach
- Keep the status of every task in a dictionary.
- Loop over the tasks; skip finished ones.
- Give each failed task to the next healthy worker and mark it done after the retry.
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.
Detect a hot key and salt it
Problem statement
Orders are partitioned by city. Mumbai's partition is far bigger than the rest, so one task runs six times longer. Measure the skew (largest ÷ average), then split the hot key into 6 sub-keys and show the new largest partition.
Pune 2, Delhi 2, Chennai 2, Kolkata 2, Mumbai 12 (GB)
Hot key: Mumbai | skew ratio: 3.0 Partitions now: 10 | largest: 2 GB
Approach
- Average size = total ÷ number of partitions. Skew ratio = largest ÷ average.
- Build a new dictionary. For the hot city, add sub-keys
Mumbai-0…Mumbai-5, each with one sixth of the data. - Copy every other city unchanged, then compare the largest partition before and after.
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 1.3 in one screen
- Heartbeats + timeouts detect failed workers; failed tasks are re-run on a node with a replica.
- Data skew: one hot key makes one huge partition (a straggler). Fix with salting or a better key; slow machines get speculative backup tasks.
- Retries mean a task can run twice, so outputs must be idempotent (commit one attempt per task).
- Distributed computing: many networked nodes work on parts of one job in parallel and combine the results.
- Pattern: partition → process in parallel → aggregate.
- Data locality: send the small program to the big data, not the reverse.
- Master–worker (HDFS, Spark, YARN) vs peer-to-peer (Cassandra).
- Fault tolerance: heartbeats detect failures; replicas let failed tasks re-run elsewhere.
- Combine carefully: sums and counts add up; average of averages is wrong; distinct counts need sets.
- Stragglers: the job waits for the slowest task. Amdahl's law: speed-up ≤ 1 ÷ serial fraction.
Interview and viva questions
What is distributed computing? How is it different from parallel computing on one machine?
Parallel computing runs parts of a task at the same time, often on cores sharing one machine's memory. Distributed computing spreads the task across separate computers connected by a network, each with its own memory and disk, coordinating by messages. It scales further and survives machine failures, but must deal with network delays and partial failures.
Explain data locality and why it matters in Hadoop.
Data locality means scheduling a task on the node that already stores its data block. Network bandwidth across a cluster is much scarcer than local disk bandwidth, so moving a few kilobytes of code instead of gigabytes of data saves time and avoids congestion. Hadoop's scheduler prefers node-local, then rack-local, then remote placement.
How does a distributed system handle the failure of a node?
Workers send heartbeats to a coordinator. When heartbeats stop, the node is marked dead. Its data is still available because blocks are replicated on other nodes, and its unfinished tasks are re-run on nodes holding those replicas. Tasks are designed to be idempotent so re-running them doesn't corrupt results.
What is Amdahl's law?
It gives the maximum speed-up from parallelising a job: speed-up = 1 ÷ ((1 − p) + p ÷ n), where p is the parallel fraction and n the number of processors. The serial part sets a hard limit: if 5% is serial, the job can never run more than 20× faster.
What are the main challenges of distributed systems?
Partial failures (some nodes fail while others work), network latency and lost messages, keeping replicas consistent, uneven work (skew and stragglers), coordinating many nodes, and combining partial results correctly. Frameworks like Hadoop and Spark handle much of this for the programmer.
What is data skew and how do you fix it?
Data skew happens when one partition key has far more data than others, so one task runs much longer and the whole job waits for it. Fixes include salting the hot key (splitting it into several sub-keys and combining their results later), choosing a partition key with more even values, handling the hot keys separately (for example, broadcasting a small table in a join), and in Spark enabling adaptive query execution, which can split skewed partitions automatically.
Practice problems
Split–process–combine exercises. The last three are business tasks about time, cost and correctness.
Chunks of three
Split the list 1 to 8 into chunks of at most 3 items.
[1..8]
[[1, 2, 3], [4, 5, 6], [7, 8]]
Show solution and explanation
nums = list(range(1, 9))
chunks = [nums[i:i + 3] for i in range(0, len(nums), 3)]
print(chunks)How it works: range(0, 8, 3) gives start positions 0, 3, 6; the last chunk is shorter.
Merge two partial counts
Machine 1 counted {"chai": 4, "poha": 1} and machine 2 counted {"chai": 2, "samosa": 3}. Print the combined counts.
two dictionaries
{'chai': 6, 'poha': 1, 'samosa': 3}Show solution and explanation
p1 = {"chai": 4, "poha": 1}
p2 = {"chai": 2, "samosa": 3}
total = dict(p1)
for item, n in p2.items():
total[item] = total.get(item, 0) + n
print(total)How it works: Copy the first result, then add the second item by item.
Total from partial sums
Four workers returned partial sums 1200, 950, 1430 and 880. Print the total.
[1200, 950, 1430, 880]
Total: 4460
Show solution and explanation
print("Total:", sum([1200, 950, 1430, 880]))How it works: Sums combine by adding: the simplest kind of aggregation.
Global maximum from local maximums
Each machine finds the largest bill in its block. Combine them to get the largest bill overall.
blocks [120, 450, 80], [990, 30], [300, 710]
Local: [450, 990, 710] Global max: 990
Show solution and explanation
blocks = [[120, 450, 80], [990, 30], [300, 710]]
local_max = [max(b) for b in blocks]
print("Local:", local_max)
print("Global max:", max(local_max))How it works: Max of maxes is correct (unlike average of averages), so each node only needs to send one number.
Amdahl's law
95% of a job can run in parallel. Print the speed-up on 20 machines and the limit.
p = 0.95, n = 20
Speed-up: 10.26 Limit: 20 x
Show solution and explanation
p, n = 0.95, 20
print("Speed-up:", round(1 / ((1 - p) + p / n), 2))
print("Limit:", round(1 / (1 - p)), "x")How it works: Even 5% serial work caps the speed-up at 20×; on 20 machines you get about 10×.
Which workers are dead?
The master marks a worker as dead if its last heartbeat is more than 30 seconds old. Given the last heartbeat time of each worker and the current time, print the dead workers and the tasks to re-run.
now = 1000 s
Dead workers: ['W3', 'W5'] Tasks to re-run: [4, 5, 7]
Show solution and explanation
now = 1000
last_beat = {"W1": 998, "W2": 990, "W3": 955, "W4": 999, "W5": 960}
tasks = {"W1": [1, 2], "W2": [3], "W3": [4, 5], "W4": [6], "W5": [7]}
TIMEOUT = 30
dead = [w for w, t in last_beat.items() if now - t > TIMEOUT]
rerun = [task for w in dead for task in tasks[w]]
print("Dead workers:", dead)
print("Tasks to re-run:", rerun)How it works: W3 has been silent 45 s and W5 40 s, both over the 30 s timeout. Their tasks are re-scheduled on healthy workers that hold replicas of the same blocks.
Commit one attempt per task
Because of retries, the same task can report results more than once. Keep only the first result for each task id, and print the correct total.
results (task, attempt, orders)
Naive total : 670 Correct total: 435
Show solution and explanation
results = [(1, "a", 120), (2, "a", 95), (3, "a", 140),
(3, "b", 140), (4, "a", 80), (2, "b", 95)]
committed = {}
for task, attempt, orders in results:
if task not in committed:
committed[task] = (attempt, orders)
naive = sum(r[2] for r in results)
correct = sum(o for _, o in committed.values())
print("Naive total :", naive)
print("Correct total:", correct)How it works: Adding every report counts tasks 2 and 3 twice. Committing one attempt per task makes retries harmless: this is the idempotent output idea from scenario 3.
Copy the data or move the code?
A job needs 500 GB that sits on another node. The network gives 10 Gbps (about 1,250 MB/s). Print the copy time in minutes. Sending the 10 KB program takes well under a second.
500 GB, 1250 MB/s
Copying data: 6.8 minutes
Show solution and explanation
mb = 500 * 1024
print("Copying data:", round(mb / 1250 / 60, 1), "minutes")How it works: About 7 minutes of a fully busy link for one block. Data locality avoids this entirely.
When does the job finish?
Six tasks run on six workers at the same time. Their times are 4, 5, 4, 21, 5 and 4 minutes. Print the job time and the average task time.
[4, 5, 4, 21, 5, 4]
Job time: 21 minutes Average task: 7.2 minutes
Show solution and explanation
times = [4, 5, 4, 21, 5, 4]
print("Job time:", max(times), "minutes")
print("Average task:", round(sum(times) / len(times), 1), "minutes")How it works: The straggler (21 min) decides the job time, not the average (7.2 min). Fix the skew or run a backup copy of the slow task.
Distinct customers across stores
Three stores report the customer ids they served. Print the number of distinct customers overall. (Adding the three counts would be wrong.)
store ids with overlaps
Sum of counts (wrong): 8 Distinct customers : 5
Show solution and explanation
stores = [{"C1", "C2", "C3"}, {"C2", "C4"}, {"C1", "C4", "C5"}]
print("Sum of counts (wrong):", sum(len(s) for s in stores))
print("Distinct customers :", len(set().union(*stores)))How it works: Customers who visit several stores would be counted more than once. Combine the sets, then count.
Real-world scenario questions (asked in company interviews)
Distributed-computing problems that show up in real jobs and interviews. Decide your answer first, then open the model answer.
The national average bill is wrong
Each region computes its average bill value and sends it to head office, which averages the 10 regional averages for the national figure. Finance says the number doesn't match their records.
The interviewer is checking: do you know which results can be combined, and how to combine averages correctly?
Show a strong answer
An average of averages gives every region equal weight, even though regions have very different numbers of bills.
- Each region should send (total value, number of bills).
- Head office adds the totals and the counts, then divides once.
- Medians and distinct counts need similar care: they can't be combined by simple addition either.
Avoid: “weighting by region size” using population or guesses instead of the actual counts.
199 tasks finish in 2 minutes, one takes 40
A daily job groups orders by seller. The Spark UI shows 199 tasks finishing in about 2 minutes and one task running for 40 minutes. Adding more machines didn't help.
The interviewer is checking: can you recognise data skew and stragglers, and explain why more machines don't fix them?
Show a strong answer
One partition holds far more data than the others, probably a few huge sellers. The job waits for that straggler; extra machines only speed up tasks that were already fast.
- Check record counts per key to confirm the skew.
- Split hot keys (add a random “salt” to spread one seller across several partitions, then combine).
- Pre-aggregate before the big grouping so less data moves.
- Enable the engine's skew-handling features where available.
Avoid saying: “Just add more nodes.”
“Why not copy all the data to one powerful machine first?”
A teammate suggests copying 50 TB from the cluster to one large server each night to run the report there, “because it's simpler”.
The interviewer is checking: do you understand data locality and network limits, with numbers?
Show a strong answer
At 10 Gbps (about 1.25 GB/s), copying 50 TB takes roughly 11–12 hours of a fully busy link, before any processing, and the single server then hits the walls from Lesson 1.2.
- Process the data where it lives: run the report as a distributed job on the cluster.
- Move only the small results (aggregates) to the reporting server or dashboard.
- Consider processing only the new day's data incrementally.
Avoid: ignoring network transfer time when comparing designs.
A worker died at 90% of the job
A 3-hour nightly job runs on 50 workers. At 2 h 40 min, one worker's machine reboots. A junior engineer wants to restart the whole job from the beginning. What should happen instead, and what must the job's code guarantee?
The interviewer is checking: do you know task-level retry and idempotency?
Show a strong answer
Frameworks like Spark and MapReduce re-run only the failed tasks on other nodes using replicated data; finished tasks are not redone. The job finishes a few minutes late, not three hours late.
- Task output must be idempotent: if a task runs twice, its result is written once (for example, write to a temporary location and commit atomically).
- Avoid side effects inside tasks (sending emails or payments) that would repeat on retry.
- For very long pipelines, add checkpoints between stages.
Avoid: designs where a retry double-counts money or sends duplicate notifications.