MapReduce and Unix Pipelines: How Batch Processing Scales
From Unix pipelines to MapReduce: map, shuffle and reduce with word count, why the shuffle dominates, combiners, fault tolerance and multi-stage limits.
MapReduce is the programming model that made it practical to process datasets far bigger than one machine: split the input, run a map function on every piece in parallel, shuffle the results so equal keys meet, then reduce each key's values. Its ideas come straight from a much older tool: the Unix pipeline.
The foundation: Unix pipelines
In the 1970s, Unix engineers processed data by chaining small programs: each reads standard input, transforms it and writes standard output, and the shell connects them with pipes (|). To find the 10 most requested URLs in a web server's access log (millions of lines of timestamp, client IP, method, URL and status):
cat access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -10cat access.log: stream the file's lines.awk '{print $7}': keep just the URL (7th field).sort: sort URLs so identical ones are adjacent.uniq -c: count each run of identical lines.sort -rn: sort by count, highest first.head -10: keep the top 10.
Each program does one thing: sort knows nothing about URLs and uniq nothing about logs, yet together they analyze traffic. And the whole file never sits in memory: lines stream through. Counting a 100 GB log with an in-memory hash map would need 100 GB+ of RAM; the pipeline needs little (only sort buffers, spilling to disk for big inputs). The general lesson: streaming through data beats loading it into memory when data exceeds RAM.
- Composability: want top URLs by response time instead? Change the
awkstep and add an aggregation; the rest stays. - Process isolation: each step is its own process, so a CPU-bound
sortand an I/O-boundcatrun in parallel, scheduled by the kernel. - Restartability: if
sortcrashes, rerun it; its input can be regenerated from the original file. This matters enormously once failures are routine. - Implicit backpressure: if a producer outpaces its consumer, the pipe buffer fills and the producer blocks, so memory never blows up. See backpressure.
The wall: a pipeline runs on one machine. Sorting 100 GB takes hours, 10 TB won't fit on one disk, and a petabyte is out of the question. MapReduce takes the same ideas (streaming transformations, isolated steps, restartability) and spreads them across hundreds of machines.
The three phases of MapReduce
Input lives in a distributed file system such as HDFS, split into blocks.
- Map: the framework launches one mapper per input block across the cluster. Each reads its block, applies your function and emits key-value pairs. It's like 1,000 copies of
awk, each on a different chunk. - Shuffle: the framework routes every pair so that all pairs with the same key reach the same reducer, which means heavy network transfer from every mapper to every reducer. It's
sort, but across machines. - Reduce: each reducer gets all values for its keys and aggregates them (sum, count, join…). Reducers run in parallel, each owning part of the key space. It's
uniq -c, split across the cluster.
Word count, end to end
Map input: "hello world hello"
Map output: (hello, 1), (world, 1), (hello, 1)
Reduce input: hello -> [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] (from 10 mappers)
Reduce output: (hello, 10)If "hello" appears in documents read by 10 mappers, each emits (hello, 1); the shuffle sends all ten to one reducer, which outputs (hello, 10). The same flow, simulated in Python with a combiner:
from collections import Counter, defaultdict
blocks = ["hello world hello", "world of hello", "hello again"] # input splits
NUM_REDUCERS = 2
def map_fn(text): # user code: emit (word, 1)
for word in text.split():
yield word, 1
def combine(pairs): # optional: pre-aggregate on the mapper
return Counter(k for k, _ in pairs).items()
def reduce_fn(word, counts): # user code: sum counts for one key
return word, sum(counts)
# Map (+ combine) runs independently per block, in parallel on a real cluster
mapped = [list(combine(map_fn(b))) for b in blocks]
# Shuffle: route every key to one reducer by hash
partitions = [defaultdict(list) for _ in range(NUM_REDUCERS)]
for pairs in mapped:
for word, count in pairs:
partitions[hash(word) % NUM_REDUCERS][word].append(count)
# Reduce: each reducer handles its keys
result = dict(reduce_fn(w, c) for part in partitions for w, c in part.items())
print(result) # {'hello': 4, 'world': 2, 'of': 1, 'again': 1} (key order may vary)Why the shuffle is the bottleneck
Map and reduce run locally (mappers read nearby HDFS blocks; reducers write locally), but the shuffle moves data from every mapper to every reducer over the network. On large jobs terabytes cross the cluster, and that network I/O, not computation, usually sets the job's duration. Three standard optimizations:
- Combiners pre-aggregate on each mapper: send
(hello, 47)instead of 47 copies of(hello, 1). - Compression of map output trades CPU for network bandwidth.
- Partitioning controls which keys go to which reducer, enabling locality and balancing skewed keys.
Fault tolerance through determinism
Map and reduce functions are deterministic, so any task can simply be re-run:
- Mapper fails: restart it on another node; its input is still in HDFS, which keeps several replicas of each block.
- Reducer fails: re-run it; mapper outputs were written to local disk, so it re-reads them (or the mappers re-run).
- Machine fails: its tasks move to healthy nodes and only those tasks repeat.
That's why jobs can span thousands of machines even though failures are routine; Google's original MapReduce paper describes clusters where hardware failed during every significant job.
The limitation: multi-stage pipelines
One map–shuffle–reduce is rigid. Iterative algorithms such as PageRank need a separate MapReduce job per iteration, and between jobs every intermediate result is written to HDFS and read back. A 10-stage pipeline is 10 jobs, each materializing to distributed storage before the next starts, and that disk I/O adds a lot of latency. Spark removes most of it by keeping data in memory between stages.
Where it’s used
Top URLs, error counts and per-customer usage over terabytes of logs.
Google built its web index with MapReduce: map documents to (term, doc) pairs, reduce into postings lists.
Hadoop jobs that join and summarize raw data into warehouse tables (now mostly on Spark).
The same map/sort/count shape on a single machine for files that fit on one disk.
In an interview
- Explain map, shuffle and reduce with word count in under a minute.
- Point out that the shuffle (network) dominates cost, and mention combiners.
- Explain fault tolerance by re-running deterministic tasks on replicated input.
- Know why Spark replaced most MapReduce jobs: in-memory multi-stage pipelines.
Further reading
- MapReduce: Simplified Data Processing on Large ClustersGoogle Research