Take a Break
5:00
Inhale…
Give your mind a break — no phone, no music, just idle time or a quick walk.
MapReduce Architecture, HDFS Operations, Execution Lifecycle, and Optimization Techniques
Prerequisite Knowledge
This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.
Previously Covered in This Subject
- HDFS storage layer and master-slave node roles — covered in Lecture 3 (Hadoop Ecosystem, HDFS & Distributed Systems)
- HDFS block splitting, replication factor, and rack awareness — covered in Lecture 3 (Hadoop Ecosystem, HDFS & Distributed Systems)
- MapReduce and data locality — covered in Lecture 3 (Hadoop Ecosystem, HDFS & Distributed Systems)
- Distributed systems foundations, scaling models, and CAP theorem — covered in Lecture 2 (Distributed Systems Architecture and NoSQL Paradigms)
4.1 HDFS Topology, Balancing, Rack Awareness, and File I/O Anatomy
Hook: When you upload a 260 MB file to HDFS, it doesn't simply copy to one server. It breaks your file into pieces, duplicates each piece three times, and scatters those copies across different physical machines in different racks. But how does the system decide which machines? And when you read the file back, how does it know the fastest path to retrieve your data?
Understanding modern big data file systems requires an in-depth examination of the Hadoop Distributed File System (HDFS) operational topology, file input/output (I/O) mechanics, rack-aware replica placement policies, and administrative maintenance procedures. In previous lectures, the foundational master-slave architecture was established, where a single master node manages system metadata while worker nodes handle physical block storage. This section expands on the physical mechanics of node additions, cluster storage rebalancing, rack topology optimization, and the exact step-by-step network handshakes that execute during file read and write operations.
Intuition — The Library System Analogy: Think of HDFS as a massive library system. The NameNode is the central catalog — it knows exactly which shelf holds which book (file block), but it doesn't physically store any books itself. The DataNodes are the individual library branches that physically hold the books. When you request a book, the catalog (NameNode) tells you the nearest branch that has a copy. If one branch burns down, the catalog knows about the other branches holding duplicate copies. The rack awareness system is like knowing which branches are in the same city (2 hops away) versus which are in different cities (4 hops away).
4.1.1 HDFS Master-Slave Topology and Dynamic Node Additions
Core Architecture: An HDFS cluster operates under a centralized master-slave model:
- NameNode (Master Node): Manages the file system directory tree, namespace hierarchy, permissions, and file-to-block mapping locations stored entirely within volatile system memory (RAM). The NameNode does not store actual data blocks — it only stores metadata (which block lives on which DataNode). This metadata includes the file-to-block mapping and the block-to-DataNode mapping.
- DataNodes (Slave Worker Nodes): Host physical block files on their native local file systems (such as EXT4 or NTFS), process block read and write requests issued by clients, and continuously transmit heartbeat signals and periodic block inventory reports to the NameNode. Each DataNode manages its own local storage independently.
The NameNode stores its persistent metadata in two files on its local disk:
- FsImage: A complete snapshot of the entire filesystem namespace at a point in time (every file, directory, and block mapping).
- EditLog: A sequential transaction log recording every change made since the last FsImage snapshot (new file creations, deletions, block allocations).
In production environments, cluster storage capacity must expand dynamically as enterprise data volumes grow. When a system administrator attaches a new slave server to an existing cluster (expanding from a 1-master/3-worker setup to a 4-worker cluster), the newly commissioned DataNode initially starts in a completely empty state with zero stored data blocks.
To prevent older DataNodes from becoming storage bottlenecks while new DataNodes remain underutilized, HDFS employs an automated cluster rebalancing process (Storage Balancing). The NameNode identifies the storage utilization percentage across all registered worker nodes. It periodically instructs over-utilized DataNodes to transfer specific data blocks across the internal cluster network to under-utilized DataNodes until storage consumption across all physical machines converges within a configurable threshold.
Worked Example — Dynamic Node Addition:
Consider a cluster with 3 DataNodes holding a total of 900 GB of data (300 GB each):
- Before addition: DN1 = 300 GB, DN2 = 300 GB, DN3 = 300 GB → utilization: 100% per node
- After adding DN4 (empty): DN1 = 300 GB, DN2 = 300 GB, DN3 = 300 GB, DN4 = 0 GB
- After rebalancing: DN1 = 225 GB, DN2 = 225 GB, DN3 = 225 GB, DN4 = 225 GB → utilization: 75% per node
The NameNode orchestrates this by instructing DN1, DN2, and DN3 to transfer specific blocks to DN4. The rebalancer runs as a background process and does not block ongoing read/write operations. A typical rebalancing threshold is 10% — meaning the system will rebalance if any node's utilization deviates from the cluster average by more than 10%.
Scope: The NameNode is a single point of failure (SPOF) in basic HDFS. If the NameNode crashes, all metadata is lost and the entire filesystem becomes unavailable. In production clusters, this is mitigated by:
- Secondary NameNode (merges FsImage + EditLog periodically but is not a hot standby)
- HDFS High Availability (HA) — active-standby NameNode pair with shared edit log via Quorum Journal Manager (QJM)
The Secondary NameNode's name is misleading — it cannot replace the NameNode if it fails. It only performs checkpointing.
4.1.2 Physical Rack Topology and Rack Awareness
In large-scale data centers, servers are physically mounted inside vertical server racks. A server rack represents a structural chassis containing multiple independent server units (resembling stacked pizza boxes) connected to a common Top-of-Rack (ToR) network switch and shared power distribution unit.
Rack Awareness is the internal intelligence mechanism by which the NameNode tracks the exact physical rack location of every DataNode in the cluster network topology. The distance between two nodes is measured in network hops — each router or switch the data must traverse counts as one hop:
- Inter-node distance within the same rack: 2 network hops (Node 1 \(\to\) Rack Switch \(\to\) Node 2).
- Inter-node distance across different racks: 4 network hops (Node 1 \(\to\) Local Rack Switch \(\to\) Core Cluster Switch \(\to\) Remote Rack Switch \(\to\) Node 9).
- Inter-node distance across different data centers: 6 network hops (not common in Hadoop deployments).
Mathematically, this distance metric is defined as: for any two nodes \(n_1\) and \(n_2\), their distance equals the sum of their distances to their lowest common ancestor in the network tree: \[ \text{distance}(n_1, n_2) = d(n_1, \text{LCA}(n_1, n_2)) + d(n_2, \text{LCA}(n_1, n_2)) \] where LCA stands for Lowest Common Ancestor in the network topology tree.
Understanding rack topology allows HDFS to optimize both write replication reliability and read retrieval latency.
Why Rack Awareness Matters: Without rack awareness, HDFS might place all three replicas of a block on the same physical rack. If that rack's switch fails, all three copies become inaccessible — data loss occurs despite having "three replicas." Rack awareness ensures replicas are spread across racks for fault tolerance, while reads are optimized to use the nearest copy for maximum throughput.
Assumption: Rack awareness must be configured manually. Hadoop cannot automatically discover your network topology. By default, it assumes all nodes are on a single rack (flat topology). For small clusters (fewer than ~20 nodes), this default is acceptable. For large production clusters, you must configure topology scripts that map IP addresses to rack locations.
4.1.3 Detailed Anatomy of HDFS File Write Operations
Write Protocol Overview: When a client application writes a file to HDFS (for example, uploading a 260 megabyte binary installer file), the system executes a strict multi-step protocol that guarantees data durability through replication:
- Block Partitioning and Metadata Request: The client library divides the local file into fixed-size block chunks based on the configured cluster block size \(B = 128\text{ MB}\). A 260 MB file is partitioned into three blocks:
\[ \text{Block } A = 128\text{ MB}, \quad \text{Block } B = 128\text{ MB}, \quad \text{Block } C = 4\text{ MB} \] The client contacts the NameNode to request block allocation and replication targets for the first block (Block A).
- Target Node Selection: The NameNode consults its internal rack topology map and returns an ordered list of target DataNodes matching the cluster replication factor \(R = 3\). Under the standard Rack Awareness placement policy:
- Target 1 (DN1): Located on the same local rack as the client (or chosen locally if client is outside the cluster).
- Target 2 (DN5): Located on a completely different remote rack (Rack 2).
- Target 3 (DN9): Located on the same remote rack as Target 2 (Rack 2), but on a different physical server node.
- Pipeline Setup and Data Streaming: The client opens a TCP socket connection directly to Target 1 (DN1) and begins streaming data packets (typically 64 KB packets). DN1 stores packets to its local disk while concurrently piping them to DN5 across the core switch. DN5 receives packets, writes to local disk, and pipes them to DN9. This forms a continuous Data Streaming Pipeline (DN1 \(\to\) DN5 \(\to\) DN9).
- Acknowledgement (ACK) Chain: Once DN9 receives and writes a packet, it transmits an ACK response upstream to DN5. DN5 forwards the ACK to DN1, and DN1 sends the final ACK back to the client. This pipeline streaming repeats until Block A is completely written.
- Next Block Allocation: The client repeats the protocol for Block B and Block C, requesting new DataNode pipeline allocations from the NameNode for each successive block.
Worked Example — Write Pipeline for 260 MB File:
Step-by-step trace with concrete numbers:
| Step | Action | Network Path |
|---|---|---|
| 1 | Client splits 260 MB file into: Block A (128 MB), Block B (128 MB), Block C (4 MB) | Local |
| 2 | Client asks NameNode: "Where to write Block A?" | Client → NameNode (RPC) |
| 3 | NameNode returns: [DN1 (Rack 1), DN5 (Rack 2), DN9 (Rack 2)] | NameNode → Client |
| 4 | Client opens TCP to DN1, streams 64 KB packets | Client → DN1 → DN5 → DN9 |
| 5 | DN9 ACKs receipt → DN5 forwards ACK → DN1 forwards ACK → Client | DN9 → DN5 → DN1 → Client |
| 6 | After Block A complete, client asks NameNode for Block B targets | Repeat steps 2-5 |
| 7 | Block C (4 MB) is much smaller than block size — stored as-is | Repeat steps 2-5 |
Total network transfers per block: Each byte crosses 3 network hops (DN1→DN5→DN9 pipeline). With replication factor \(R=3\), this is the minimum cost to achieve 3 copies.
Key insight: The pipeline design is more efficient than having the client write to all three DataNodes directly. Instead of 3 parallel writes, the client writes once and the data flows through the pipeline — reducing client-side network bandwidth by 67%.
Pitfalls to Avoid:
- Block size mismatch: If you change the block size from 128 MB to 64 MB, your 260 MB file splits into 5 blocks instead of 3, increasing NameNode metadata overhead.
- Replication factor vs. available DataNodes: If your cluster has only 2 DataNodes but \(R=3\), HDFS will log under-replication warnings but will still write successfully with fewer replicas.
- Pipeline failure handling: If DN5 crashes mid-write, the pipeline is rebuilt without DN5. The partial block on DN5 is abandoned, and a new replica is created on another DataNode asynchronously.
4.1.4 Detailed Anatomy of HDFS File Read Operations
Read Protocol: When a client application requests to read a file from HDFS, the retrieval protocol prioritizes network data locality to minimize network bandwidth consumption:
- Metadata Query: The client issues a read request to the NameNode specifying the HDFS file path.
- Location Lookup and Distance Ranking: The NameNode consults its namespace metadata table and returns a list of block locations for every block comprising the file. Crucially, the NameNode sorts the DataNode addresses for each block based on physical proximity (network hop distance) relative to the client.
- Localized Data Retrieval:
- For Block A (located on DN1, DN5, and DN9): If the client is executing on a machine in Rack 1, the NameNode returns DN1 as the primary source because DN1 shares the local rack (2 network hops). The client opens a direct socket to DN1 and reads Block A.
- For Block B (located on DN3, DN4, and DN8): The NameNode evaluates that DN3 resides on Rack 1 (local rack). Thus, DN3 is returned as the primary target over DN4 and DN8 (which reside on Rack 2).
- Rack Failure Fallback: If a local rack switch experiences a hardware failure or network congestion makes DN1 unreachable, the NameNode detects the transport timeout and seamlessly redirects the client to the next physically closest node, DN5 (located on Rack 2, 4 network hops away). For Block B, if Rack 1 fails, the NameNode chooses DN4 over DN8 because DN4 resides in the same rack chassis as DN5, allowing shared rack switch bandwidth during fallback reads.
Worked Example — Read with Rack Awareness:
Scenario: Client on Rack 1 requests a file with 2 blocks:
| Block | Replica Locations | NameNode's Choice | Reason |
|---|---|---|---|
| Block A | DN1 (Rack 1), DN5 (Rack 2), DN9 (Rack 2) | DN1 | Same rack = 2 hops (fastest) |
| Block B | DN3 (Rack 1), DN4 (Rack 2), DN8 (Rack 2) | DN3 | Same rack = 2 hops (fastest) |
Failure scenario: If Rack 1's switch fails mid-read:
| Block | Available Replicas | NameNode's Choice | Reason |
|---|---|---|---|
| Block A | DN5 (Rack 2), DN9 (Rack 2) | DN5 | Both 4 hops, DN5 chosen arbitrarily |
| Block B | DN4 (Rack 2), DN8 (Rack 2) | DN4 | Same rack as DN5 (shared switch bandwidth) |
Sense-check: Reading locally (2 hops) vs. cross-rack (4 hops) means local reads are approximately 2× faster in network transfer time, assuming equal bandwidth on both switches.
Critical Design Principle: The client contacts DataNodes directly to retrieve data and is guided by the NameNode to the best DataNode for each block. This design allows HDFS to scale to a large number of concurrent clients because the data traffic is spread across all the DataNodes in the cluster. The NameNode only services block location requests (which are stored in memory, making them very efficient) and does not serve data — which would quickly become a bottleneck.
4.1.5 HDFS Administrative Commands and Shell Operations
Interacting with HDFS from the command line interface (CLI) requires standard Hadoop utility tools:
Starting and Stopping DFS Daemons:
Executed from the Hadoop installation sbin directory:
start-dfs.sh
stop-dfs.sh
In a pseudo-distributed environment (where a single physical host acts as both master NameNode and slave DataNode), running start-dfs.sh automatically launches two separate background process windows/daemons: one for the NameNode master daemon and one for the DataNode slave daemon.
Command Syntax Paradigm:
Standard Linux file commands (ls, mv, rm, mkdir) operate on HDFS by prefixing the command string:
hadoop fs -<command> <arguments>
# OR
hdfs dfs -<command> <arguments>
Common operations with explanations:
| Command | Purpose | Example |
|---|---|---|
hdfs dfs -ls / |
List files in HDFS root | Shows all top-level directories |
hdfs dfs -put local.txt /dir/ |
Upload local file to HDFS | Copies from local filesystem to HDFS |
hdfs dfs -get /dir/file.txt ./ |
Download file from HDFS | Copies from HDFS to local filesystem |
hdfs dfs -mkdir -p /dir/subdir |
Create directory tree | -p creates parent dirs recursively |
hdfs dfs -rm -r /dir/old_data |
Delete directory recursively | -r required for non-empty dirs |
hdfs dfs -cat /dir/file.txt |
Display file contents | Streams file to stdout |
hdfs dfs -du -h /dir |
Show disk usage | -h for human-readable sizes |
Web Monitoring Interfaces and Port Configurations:
| Interface | Default Port (Hadoop 2.x) | Hadoop 3.x Port | Purpose |
|---|---|---|---|
| NameNode Web UI | 50070 | 9870 | Cluster health, DataNode counts, SafeMode status, file browser |
| DataNode Web UI | 50075 | 9864 | Individual node block storage metrics and logs |
| NameNode RPC | 8020 | 9820 | Client-to-NameNode communication port |
Heartbeat Interval: DataNodes transmit heartbeat signals to the NameNode every \(3\text{ seconds}\). If the NameNode fails to receive heartbeats from a specific DataNode for 10 consecutive minutes (by default), it flags the DataNode as dead and initiates automatic replica reconstruction on remaining healthy DataNodes.
File System Check (fsck) Tool:
The fsck utility audits file health, block allocation status, and replication metrics across HDFS:
hdfs fsck /path/to/file -files -blocks -locations
Worked Example — Running fsck on a 260 MB File:
Running fsck on a 260 MB file provides a detailed breakdown confirming:
- Total file size: 260 MB across 3 blocks
- Configured replication factor: 3 vs actual healthy replica count per block
- Exact DataNode IP addresses and port locations hosting each block ID
Example output interpretation:
Block blk_1073741825: len=134217728 repl=3 [10.0.0.1:9866, 10.0.1.5:9866, 10.0.1.9:9866]
Block blk_1073741826: len=134217728 repl=3 [10.0.0.3:9866, 10.0.1.4:9866, 10.0.1.8:9866]
Block blk_1073741827: len=4194304 repl=3 [10.0.0.2:9866, 10.0.1.6:9866, 10.0.1.7:9866]
Each line shows: block ID, length in bytes, replication count, and the IP:port of every DataNode holding a replica.
4.1.6 Student Questions and Answers
Q: When launching start-dfs in a single-machine pseudo-distributed environment, why do two separate command windows pop up?
A: The two windows correspond to the two distinct JVM daemon processes running on the machine: one window handles the master NameNode process (which manages metadata, fsimage, and editlog), while the second window handles the slave DataNode process (which manages physical block storage on disk). In a production cluster, these would run on separate physical machines.
Q: If a client requires Block A and Block B from HDFS, why does the NameNode explicitly return DataNode 1 for Block A and DataNode 3 for Block B during file read?
A: The NameNode is rack-aware. It evaluates the network topology distance between the requesting client and all DataNodes holding replicas. It returns DN1 for Block A and DN3 for Block B because both DataNodes reside on the exact same physical rack (Rack 1) as the client machine, minimizing network switch hops and maximizing data throughput. The distance metric is 2 hops (same rack) vs. 4 hops (different rack) — the NameNode always prefers the minimum distance.
Pitfalls:
- Confusing Secondary NameNode with HA standby: The Secondary NameNode only performs checkpointing (merging FsImage + EditLog). It cannot take over if the primary NameNode fails. For high availability, you need HDFS HA with active-standby NameNodes.
- Forgetting that block size is not a storage limit: A 1 MB file stored with 128 MB block size uses only 1 MB of disk space, not 128 MB. The block size is the chunking unit for large files, not a minimum allocation.
- Misunderstanding the heartbeat timeout: The 10-minute timeout means that if a DataNode crashes, there is up to a 10-minute window before the NameNode detects it and begins replicating under-replicated blocks to other nodes.
Recap + Bridge: HDFS uses a master-slave architecture where the NameNode tracks metadata and DataNodes store actual blocks. Rack awareness (2 hops same rack, 4 hops cross-rack) optimizes both write placement and read locality. File writes follow a pipeline protocol (client → DN1 → DN5 → DN9) with ACK chains; reads prioritize the nearest replica. Now that we understand how data is stored and retrieved, we turn to the compute engine designed to process those blocks in parallel: MapReduce.
Real-World & Domain Connection: HDFS powers data infrastructure at companies like Yahoo (which pioneered Hadoop), Facebook, LinkedIn, and major cloud providers. In financial services, HDFS stores transaction logs that must be replicated across geographic regions for disaster recovery. In genomics research, HDFS stores terabytes of sequencing data where rack-aware replication ensures that a single rack failure doesn't lose critical research datasets. The rack-aware placement strategy directly reduces cross-rack network traffic by approximately 67% compared to random placement, which translates to significant cost savings on network infrastructure at hyperscale.
4.2 MapReduce Programming Model, Execution Lifecycle, and Core Abstractions
Hook: How do you process a 10 terabyte dataset that doesn't fit on a single machine? You could buy a supercomputer, or you could split the data across 100 cheap commodity servers and process it in parallel. But coordinating 100 machines — handling failures, data distribution, result merging — is an engineering nightmare. MapReduce solves this by providing a simple programming model: you write just two functions (map and reduce), and the framework handles everything else — parallelism, fault tolerance, data movement, and result collection.
Having established how HDFS stores data across distributed blocks, we turn to the fundamental compute engine designed to process those blocks in parallel: Apache Hadoop MapReduce Framework by Doug Cutting. Formulated originally by Google in 2004 (Google MapReduce Whitepaper (2004)), MapReduce provides a structured software framework for executing batch computations across massive multi-terabyte datasets without requiring developers to write complex socket networking or fault-recovery code.
Intuition — The Assembly Line Analogy: Think of MapReduce like a factory assembly line for data. Raw materials (input data) enter at one end. Workers at the Map station each take a piece, identify what it is, and tag it with a label. A conveyor belt (Shuffle & Sort) automatically groups all items with the same label together. Workers at the Reduce station take each group and combine them into a final product. The beauty is that the factory can have thousands of Map workers and hundreds of Reduce workers all operating simultaneously — and if one worker drops dead, their work is automatically reassigned.
4.2.1 Core Operational Principles and Data Locality
Data Locality is the foundational architectural shift introduced by MapReduce — the principle of "moving computation to data" rather than "moving data to computation."
In traditional relational database architectures, computational programs execute on a centralized application server, requiring massive raw datasets to be read from storage disks and transmitted over the network into application RAM. For a 10 TB dataset, this means transferring 10 TB across the network to the application server — a process that could take hours even on a fast network.
In MapReduce, raw data blocks remain static on DataNode disks. The compiled, lightweight MapReduce Java program (typically a few MB JAR file) is shipped across the cluster network to run directly on the specific DataNode worker machines where target data blocks already reside. The program processes the local data and emits results — the data never moves.
Why this matters: If your program is 10 MB and your data is 10 TB, shipping the program to the data is 1,000,000× more efficient than shipping the data to the program.
Scope: Data locality is an optimization, not a guarantee. Hadoop will try to run a map task on the node where the data resides, but if that node is busy, it will try a node on the same rack (rack-local), and only as a last resort, a node on a different rack (off-rack). The three levels of locality are:
- Data-local (same node): 0 network hops — fastest
- Rack-local (same rack): 2 network hops — acceptable
- Off-rack (different rack): 4 network hops — slowest
4.2.2 The Key-Value Pair Processing Abstraction
Every stage of a MapReduce data processing pipeline consumes and produces structured key-value pairs. The MapReduce signature transformation is:
Phase 1 — Input Format (Record Reader): \[ \text{Record Reader: Raw bytes} \longrightarrow (K_1, V_1) \] Raw input file bytes are transformed into \((K_1, V_1)\) pairs by a Record Reader component. For text files, \(K_1\) is the byte offset and \(V_1\) is the line text.
Phase 2 — Map: \[ \text{Map: } (K_1, V_1) \longrightarrow \text{List}(K_2, V_2) \] A user-defined map() function processes each \((K_1, V_1)\) input pair individually and emits zero, one, or multiple intermediate \((K_2, V_2)\) pairs. The mapper is a filter and transformer — it extracts relevant fields and re-keys the data.
Phase 3 — Shuffle and Sort (Framework-managed): \[ \text{Shuffle \& Sort: } \text{List}(K_2, V_2) \longrightarrow (K_2, \text{Iterable}(V_2)) \] The MapReduce framework automatically groups all intermediate \((K_2, V_2)\) pairs by key, sorting keys in natural ascending order and constructing an iterable value list. This is the critical bridge phase — it transforms a flat stream of key-value pairs into a grouped, sorted collection.
Phase 4 — Reduce: \[ \text{Reduce: } (K_2, \text{Iterable}(V_2)) \longrightarrow \text{List}(K_3, V_3) \] A user-defined reduce() function processes each \((K_2, \text{Iterable}(V_2))\) pair and emits final aggregated \((K_3, V_3)\) pairs to output files on HDFS.
Symbol Dictionary:
- \(K_1, V_1\): Input key and value from the Record Reader (e.g., byte offset, line text)
- \(K_2, V_2\): Intermediate key and value emitted by the Mapper (e.g., "year", temperature)
- \(K_3, V_3\): Final output key and value from the Reducer (e.g., "year", max_temperature)
- The types \(K_1, K_2, K_3, V_1, V_2, V_3\) can all be different — they are specified by the programmer
Worked Example — Concrete Type Flow for Max Temperature:
Given a weather dataset where each line contains: station_id, year, temperature, quality_code
| Phase | Input Type | Output Type | Example |
|---|---|---|---|
| Record Reader | Raw bytes | \((K_1=\text{LongWritable}(0), V_1=\text{Text}("0067011990..."))\) | Byte offset 0, line text |
| Map | \((K_1, V_1)\) | List\((K_2=\text{Text}("1950"), V_2=\text{IntWritable}(22))\) | Extract year=1950, temp=22 |
| Shuffle & Sort | List\((K_2, V_2)\) | \((K_2=\text{Text}("1950"), \text{Iterable}(\text{IntWritable})([0, 22, -11]))\) | Group all 1950 readings |
| Reduce | \((K_2, \text{Iterable}(V_2))\) | List\((K_3=\text{Text}("1950"), V_3=\text{IntWritable}(22))\) | Max of [0, 22, -11] = 22 |
Sense-check: The output year 1950 has max temperature 22 (2.2°C), which is plausible for a global maximum.
4.2.3 Classroom Analogy: The Distributed Coin and Metal Counting Problem
Professor's Analogy — Preserved in Full:
To build complete intuition for the MapReduce execution pipeline, consider a physical classroom problem: calculating the total monetary value and item counts of a giant, unsorted sack of physical metal coins and scrap pieces.
Roles:
- Master Coordinator (NameNode / JobTracker): Master supervisor (Nadeem) managing work distribution.
- Worker Nodes (DataNodes / TaskTrackers): Four students (Student A, Student B, Student C, Student D) assigned to perform local counting.
Execution Workflow without Optimization:
- Record Reading: The master divides the large sack into four smaller bags and hands one bag to each student. Each student opens their bag item-by-item (Record Reader).
- Map Phase: Each student creates a raw ledger table. As they pick up a metal piece, they write down the metal type and a count of 1.
For instance, Student A logs: aluminum -> 1, aluminum -> 1, copper -> 1, platinum -> 1, aluminum -> 1. Student B logs: platinum -> 1, platinum -> 1, silver -> 1, platinum -> 1, platinum -> 1.
- Raw Output Transmission: Students submit all raw ledger entries directly to the master supervisor. If the four students process 40 raw items, 40 individual rows are transmitted to the master.
- Shuffle and Sort (Grouping): The master receives all 40 rows, groups all identical metal labels together, and sorts them alphabetically:
aluminum: \([1, 1, 1, 1, 1, 1, 1]\)copper: \([1, 1, 1]\)platinum: \([1, 1, 1, 1, 1]\)silver: \([1, 1]\)
- Reduce Phase: The master assigns specific metals to individual reducers (or processes them sequentially). The reducer iterates over the list of ones, sums them up, and outputs final counts:
aluminum -> 7, platinum -> 5, copper -> 3, silver -> 2.
Worked Example — Full Trace with Numbers:
Input: 40 metal items across 4 students' bags.
| Student | Items in Bag | Map Output (raw rows) |
|---|---|---|
| Student A | 3 aluminum, 1 copper, 1 platinum | 5 rows |
| Student B | 4 platinum, 1 silver | 5 rows |
| Student C | 3 aluminum, 2 copper | 5 rows |
| Student D | 1 platinum, 2 silver, 2 aluminum | 5 rows |
Total Map output: 20 raw rows sent to master.
After Shuffle & Sort (grouping by metal type): | Metal | Values | Count | |-------|--------|-------| | aluminum | [1,1,1, 1,1,1, 1,1] | 8 | | copper | [1, 1,1] | 3 | | platinum | [1,1,1,1, 1] | 5 | | silver | [1, 1,1] | 3 |
After Reduce (sum): | Metal | Final Count | |-------|-------------| | aluminum | 8 | | copper | 3 | | platinum | 5 | | silver | 3 |
Total items: 8 + 3 + 5 + 3 = 19 items. Sense-check: This matches the expected count from our input distribution.
Key Insight from the Analogy: The Shuffle and Sort phase is the critical "magic" of MapReduce — the framework automatically handles the grouping and sorting that would be tedious to code manually. Without it, each student would need to send every individual item to the master, creating massive network traffic. The Shuffle and Sort phase acts as the bridge between independent Map workers and coordinated Reduce workers.
4.2.4 Record Readers and Input Format Mechanics
The Record Reader is the architectural component responsible for converting raw byte streams from HDFS block files into structured key-value records that the map() function can process. It is the first stage of every MapReduce job.
Default Line Record Reader (TextInputFormat): Reads standard line-delimited text files (such as CSV or log files).
- Input Key (\(K_1\)):
LongWritablerepresenting the byte offset of the line from the beginning of the file. - Input Value (\(V_1\)):
Textrepresenting the raw string content of the line.
Specialized Record Readers:
SequenceFileInputFormat: Reads binary key-value file formats optimized for Hadoop inter-job data transfers.NLineInputFormat: Reads a fixed number of lines per split (useful when each line requires significant processing).- Custom Record Readers: Developed for specialized data formats, such as audio MP3 files (parsing packet-by-packet) or raw image files (parsing pixel matrix blocks).
Pitfalls:
- Assuming the key is meaningful: In
TextInputFormat, the key is the byte offset (a number like 0, 106, 212...). Most mappers ignore this key entirely — don't confuse it with a logical key like a name or ID. - Forgetting that mappers see one record at a time: Each call to
map()receives exactly one \((K_1, V_1)\) pair. If you need to compare across records, you must use a custom InputFormat or a two-stage MapReduce job. - Confusing Map output types with Reduce output types: The Mapper output \((K_2, V_2)\) and Reducer output \((K_3, V_3)\) can have completely different types. If they differ, you must explicitly set the map output types using
setMapOutputKeyClass()andsetMapOutputValueClass().
4.2.5 Student Questions and Answers
Q: Why are intermediate key-value pairs produced by Mappers written to local disk on the DataNode rather than being written directly to HDFS?
A: Writing intermediate Map outputs to local disk avoids the high overhead of HDFS block replication (\(R=3\)). Intermediate map outputs are temporary data required only until the Reduce phase completes. If we wrote them to HDFS with replication factor 3, every intermediate byte would be written 3 times — tripling disk I/O and network traffic for data that will be deleted after the job finishes. If a DataNode crashes before the Reduce phase, the master simply re-executes the specific Map task on another DataNode hosting a replica of the original input block. This is the fault-tolerance strategy: recompute rather than replicate.
Exam note: Map tasks write output to local disk, not HDFS. Reduce tasks write output to HDFS (because it's the final result that must be persisted). This is a common exam question.
Assumptions & Scope:
MapReduce works best under these conditions:
- Batch processing: MapReduce is designed for high-throughput batch jobs, not low-latency queries. A simple MapReduce job has startup overhead of 10-30 seconds — not suitable for interactive queries.
- Data-intensive, not compute-intensive: The model shines when processing large datasets (TB to PB). For small datasets (< 1 GB), the overhead of job setup, task scheduling, and HDFS I/O dominates.
- Independent records: Each map task processes records independently. If your computation requires global state or complex record dependencies, MapReduce is not the right model.
- Writable serialization: All keys and values must implement the
Writableinterface (Hadoop's custom serialization). Standard Java objects cannot be used directly.
Recap + Bridge: MapReduce processes data through a four-phase pipeline: Record Reader → Map → Shuffle & Sort → Reduce. The key insight is data locality — ship the code to the data. Map output goes to local disk (not HDFS) to avoid replication overhead; Reduce output goes to HDFS for persistence. Now that we understand the basic pipeline, we turn to the two critical optimizations that make it practical at scale: Combiners (local pre-aggregation) and Partitioners (controlling which Reducer gets which keys).
Real-World & Domain Connection: MapReduce was originally designed by Google to build their search index — processing the entire web crawl (petabytes of HTML) to produce an inverted index. Today, MapReduce-style processing powers recommendation systems (analyzing click logs to find patterns), financial risk analysis (processing millions of transactions daily), and genomics pipelines (aligning DNA sequences in parallel). The "move computation to data" principle has been adopted by virtually every distributed computing framework that followed, including Apache Spark, Flink, and Presto.
4.3 MapReduce Optimizations: Combiner and Partitioner
Hook: Imagine you're one of 50,000 servers processing a trillion log entries. Your server alone generates 20 million (key, 1) rows from your local data chunk. Sending all 20 million rows across the network to the reducers would overwhelm the cluster's switches. But what if you could pre-aggregate locally — combining (error, 1) repeated 5,000 times into a single (error, 5000) — and send only 500 summary rows instead? That's the Combiner. But there's a catch: it only works for certain mathematical operations. Get it wrong, and your results silently corrupt.
Executing raw MapReduce pipelines across enterprise clusters processing multi-terabyte datasets can create massive network I/O bottlenecks during the Shuffle and Sort phase. To maximize cluster efficiency, Hadoop provides two critical optimization components: the Combiner and the Partitioner.
Intuition — Extending the Classroom Analogy:
Remember the coin counting example? Student A transmitted 40 individual ledger rows across the network to the master supervisor. In large-scale clusters with 50,000 worker nodes, streaming billions of unaggregated (key, 1) records saturates network switches and degrades query throughput.
With a Combiner: After Student A processes their local bag and generates raw entries (aluminum -> 1, aluminum -> 1, aluminum -> 1), Student A executes a local pre-aggregation count within their own workspace. Student A condenses those 3 raw rows into a single combined entry: aluminum -> 3. Instead of sending 40 raw rows to the master, the four students transmit only 17 pre-aggregated summary rows across the network switch.
The Combiner is like a "local secretary" that summarizes each worker's results before sending them to headquarters. The secretary doesn't have all the information (only their own worker's data), but they can reduce the amount of paperwork dramatically.
4.3.1 The Combiner Concept (In-Mapper Local Reduction)
A Combiner is an optional, local mini-reducer that executes directly within the Mapper process on the local DataNode machine before data is written to local disk or transmitted across the network. It operates on the Mapper's output before the Shuffle phase.
Without Combiner: Mapper → 40 raw rows → Network → Shuffle & Sort → Reducer With Combiner: Mapper → 40 raw rows → Combiner → 17 summary rows → Network → Shuffle & Sort → Reducer
Key properties:
- The Combiner runs on the same node as the Mapper — no network transfer
- It receives the Mapper's local output only — it cannot see data from other Mappers
- The framework may run it 0, 1, or many times — you cannot depend on it running
- It uses the same Java class as the Reducer (typically) but operates on a subset of data
Worked Example — Combiner Impact on Network Traffic:
Scenario: Word count on a dataset where 4 Mappers each process 10,000 words.
| Metric | Without Combiner | With Combiner |
|---|---|---|
| Map output rows per Mapper | 10,000 | 10,000 |
| After local pre-aggregation | 10,000 | ~500 (unique words) |
| Total rows sent over network | 40,000 | ~2,000 |
| Network reduction | — | 95% |
The reduction depends on the data's key cardinality. For word count on English text, there are ~100,000 unique English words vs. millions of total words — so Combiners reduce traffic by 90-99%.
Real-World: Environmental and Corporate Sustainability: Pre-aggregating intermediate Map outputs using Combiners reduces network bandwidth and disk I/O by up to 90%. In hyper-scale data centers operated by cloud providers (such as Microsoft Azure or Google Cloud), this drastic reduction in CPU cycles and network switch traffic lowers electrical power consumption, directly supporting corporate green computing initiatives. A 90% reduction in shuffle data translates to approximately 90% reduction in the electricity consumed by network switches during MapReduce jobs.
4.3.2 Strict Mathematical Requirements for Combiners
Scope: A Combiner cannot be applied arbitrarily to any MapReduce pipeline. Because the MapReduce framework may execute the Combiner zero, one, or multiple times depending on buffer memory constraints, a Combiner can ONLY be safely used if the reduction operation satisfies two mathematical properties:
Property 1: Commutative
An operation \(\oplus\) is commutative if swapping the order of operands produces an identical result: \[ a \oplus b = b \oplus a \]
Examples: | Operation | Commutative? | Verification | |-----------|-------------|--------------| | Addition (\(+\)) | Yes | \(3 + 5 = 5 + 3 = 8\) | | Multiplication (\(\times\)) | Yes | \(3 \times 5 = 5 \times 3 = 15\) | | Subtraction (\(-\)) | No | \(3 - 5 = -2\), but \(5 - 3 = 2\) | | Division (\(\div\)) | No | \(6 \div 3 = 2\), but \(3 \div 6 = 0.5\) | | Maximum (max) | Yes | \(\max(3,5) = \max(5,3) = 5\) | | Minimum (min) | Yes | \(\min(3,5) = \min(5,3) = 3\) |
Property 2: Associative
An operation \(\oplus\) is associative if regrouping operands produces an identical result: \[ (a \oplus b) \oplus c = a \oplus (b \oplus c) \]
Examples: | Operation | Associative? | Verification | |-----------|-------------|--------------| | Addition (\(+\)) | Yes | \((3+5)+2 = 8+2 = 10\) and \(3+(5+2) = 3+7 = 10\) | | Multiplication (\(\times\)) | Yes | \((3 \times 5) \times 2 = 30\) and \(3 \times (5 \times 2) = 30\) | | Subtraction (\(-\)) | No | \((3-5)-2 = -2-2 = -4\), but \(3-(5-2) = 3-3 = 0\), and \(-4 \neq 0\) | | Division (\(\div\)) | No | \((12 \div 3) \div 2 = 4 \div 2 = 2\), but \(12 \div (3 \div 2) = 12 \div 1.5 = 8\), and \(2 \neq 8\) | | Maximum (max) | Yes | \(\max(\max(3,5),2) = \max(5,2) = 5\) and \(\max(3,\max(5,2)) = \max(3,5) = 5\) |
Why Both Properties Are Required:
The Combiner may execute on different subsets of the Mapper's output in different runs. If the operation is not commutative, the order in which keys are processed matters. If it is not associative, the grouping of values matters. Both must hold for the result to be independent of how the framework splits and regroups the data.
Operations that ARE safe for Combiners: Sum, Count, Maximum, Minimum, Set Union Operations that are NOT safe for Combiners: Average, Median, Standard Deviation (unless restructured)
4.3.3 Detailed Counterexample: Why Combiners Fail for Average Computations
Exam note: A classic exam question asks whether a Combiner can be directly assigned to calculate an average value across a dataset using a standard reduce() function. You must be prepared to prove why this fails.
Worked Example — Proving Average is NOT Associative:
Consider a dataset containing three numerical observations: \(x_1 = 3\), \(x_2 = 2\), and \(x_3 = 5\).
Step 1: Compute the true average (correct answer): \[ \text{Average}_{\text{true}} = \frac{x_1 + x_2 + x_3}{3} = \frac{3 + 2 + 5}{3} = \frac{10}{3} \approx 3.333 \]
Step 2: Apply a naive Average Combiner (what would happen with two Mappers):
Assume the three observations are split across two Mappers:
- Mapper 1 receives observations: \(x_1 = 3\) and \(x_2 = 2\).
Mapper 1 applies the local Average Combiner: \[ \text{Avg}_1 = \frac{3 + 2}{2} = 2.5 \] Mapper 1 emits: \((\text{"all"}, 2.5)\)
- Mapper 2 receives observation: \(x_3 = 5\).
Mapper 2 applies the local Average Combiner: \[ \text{Avg}_2 = \frac{5}{1} = 5.0 \] Mapper 2 emits: \((\text{"all"}, 5.0)\)
Step 3: The Reducer receives the two pre-combined averages:
The Reducer computes the average of the averages: \[ \text{Average}_{\text{combiner}} = \frac{2.5 + 5.0}{2} = \frac{7.5}{2} = 3.750 \]
Step 4: Compare: \[ \text{Average}_{\text{combiner}} = 3.750 \neq 3.333 = \text{Average}_{\text{true}} \]
Error: \(3.750 - 3.333 = 0.417\) — a 12.5% error from the correct answer!
Root Cause: Average is not associative because combining intermediate subset averages weights small partitions equally with large partitions, distorting the final global mean. The Mapper 1 average (2.5) is computed from 2 values, while Mapper 2 average (5.0) is computed from 1 value — but the Reducer treats them as equally weighted.
The Fix: To compute an average using a Combiner safely, Mappers must emit intermediate custom tuples containing both local sums and local counts \((\text{sum}, \text{count})\), allowing the Combiner and Reducer to sum both components independently before dividing: \[ \text{Average} = \frac{\sum \text{sum}_i}{\sum \text{count}_i} \] This works because sum and count are both commutative and associative.
Exam Checklist for Combiner Questions:
- State the two required properties (commutative + associative)
- For Average: show the counterexample with concrete numbers
- Explain the fix: emit (sum, count) tuples instead of averages
- Mention that Combiner is optional — Hadoop may skip it entirely
4.3.4 The Partitioner Component
While the Combiner optimizes local Mapper output, the Partitioner controls how intermediate key-value pairs are assigned and routed across multiple parallel Reducer nodes. The Partitioner determines which Reducer processes which keys.
Default Hash Partitioner (HashPartitioner): Routes keys using a standard modulo hash function: \[ \text{Reducer Index} = \left| \text{hashCode}(K_2) \right| \pmod{\text{numReduceTasks}} \]
This guarantees two things:
- Consistency: All intermediate pairs sharing the exact same key \(K_2\) are routed to the exact same Reducer node.
- Distribution: Different keys are evenly distributed across available Reducer nodes (assuming a good hash function).
Worked Example — Hash Partitioner in Action:
Given 4 Reducers (numReduceTasks = 4) and keys: "apple", "banana", "cherry", "date"
| Key | hashCode() | \ | hashCode\ | mod 4 | Assigned Reducer |
|---|---|---|---|---|---|
| "apple" | 93029277 | 1 | Reducer 1 | ||
| "banana" | -199846371 | 1 | Reducer 1 | ||
| "cherry" | -1963247272 | 0 | Reducer 0 | ||
| "date" | 98404 | 0 | Reducer 0 |
Note: "apple" and "banana" both map to Reducer 1 — they will be processed by the same Reducer instance. "cherry" and "date" map to Reducer 0.
Single Reducer case: When numReduceTasks = 1, ALL keys go to Reducer 0. The Reducer processes keys serially in strict alphabetical key order — first all "apple" values, then all "banana" values, etc. This is a common source of performance bottlenecks.
Custom Partitioners: Developers can override the default partitioner to enforce custom routing logic. For example:
- Route all transactions from Asia to Reducer 0, Europe to Reducer 1, Americas to Reducer 2
- Route all data for a specific date range to a dedicated Reducer
- Ensure that a specific key always goes to a specific Reducer for ordered output
To implement a custom partitioner, extend the Partitioner class and override getPartition():
public class RegionPartitioner extends Partitioner<Text, Text> {
@Override
public int getPartition(Text key, Text value, int numPartitions) {
String region = key.toString();
if (region.equals("Asia")) return 0;
else if (region.equals("Europe")) return 1;
else return 2;
}
}
Pitfalls:
- Forgetting the modulo constraint: If you have 4 Reducers but only 3 distinct keys, one Reducer will be idle. Always choose
numReduceTasksbased on expected key cardinality, not cluster size. - Hash collisions: Different keys can hash to the same Reducer — this doesn't cause errors, but it can cause uneven load if some Reducers get more data than others.
- Depending on key ordering within a partition: The Partitioner controls which Reducer gets each key, but within a single Reducer, keys are sorted. If you need global ordering across all Reducers, you need secondary sorting techniques.
4.3.5 Student Questions and Answers
Q: If a Combiner uses the exact same Java code logic as the Reducer, why is it listed as an optional component?
A: A Combiner is optional because the MapReduce framework does not guarantee it will run for every single record. If a node has ample RAM and low disk I/O, Hadoop may bypass the Combiner entirely. Therefore, pipeline correctness must never depend on whether a Combiner executes; Combiners must strictly serve as performance optimizations for associative and commutative operations. If your Combiner produces different results when skipped, your logic is wrong.
Recap + Bridge: The Combiner reduces network traffic by pre-aggregating Map output locally, but only for operations that are both commutative and associative. The classic trap: Average cannot use a naive Combiner (fix: emit sum/count tuples). The Partitioner controls which Reducer gets which keys via hash modulo. These two optimizations together make MapReduce practical at scale. Next, we look at the Java API that implements these concepts — the Mapper, Reducer, Driver, and Hadoop's custom serialization system.
Real-World & Domain Connection: Combiners are used in virtually every production MapReduce job. In log analysis (counting error types across millions of server logs), Combiners reduce shuffle data by 99%+. In recommendation engines (counting co-occurrences of products), Combiners transform billions of (product_pair, 1) records into millions of summary rows. The Partitioner is critical in time-series data processing — routing all data for a specific hour to the same Reducer enables efficient hourly aggregations without global coordination.
4.4 Java MapReduce API and Serialization Data Types
Hook: Java's built-in serialization (java.io.Serializable) adds 100+ bytes of metadata per object — class names, field descriptors, type information. When you're shuffling billions of objects across a cluster, that overhead multiplies into gigabytes of wasted bandwidth. Hadoop solved this by inventing its own serialization system called Writable — stripping away all metadata and sending only raw bytes.
Writing production MapReduce applications in Java requires understanding Hadoop's custom serialization model and driver job configuration APIs.
4.4.1 Hadoop Writable Serialization System
Why Not Use Standard Java Serialization?
Standard Java object serialization (java.io.Serializable) is designed for general-purpose object persistence. It writes heavy class metadata, object headers, and type descriptors alongside raw data, making byte streams bloated and slow to deserialize over network sockets.
Hadoop's Solution — Writable:
Hadoop introduces its own lightweight, high-performance serialization framework based on the Writable and WritableComparable interfaces (org.apache.hadoop.io). Hadoop Writable types serialize pure data payload bytes without metadata overhead, enabling rapid binary comparison of keys on disk without deserializing full objects.
Key Differences:
| Feature | Java Serializable | Hadoop Writable |
|---|---|---|
| Metadata overhead | ~100+ bytes per object | 0 bytes |
| Deserialization required for comparison? | Yes | No (binary comparison) |
| Designed for | General persistence | High-throughput data pipelines |
Writable Type Mapping:
| Standard Java Native Type | Hadoop Writable Equivalent | Description |
|---|---|---|
int |
IntWritable |
32-bit serializable integer |
long |
LongWritable |
64-bit serializable long integer |
double |
DoubleWritable |
64-bit serializable floating-point number |
boolean |
BooleanWritable |
Serializable boolean flag |
String |
Text |
UTF-8 serializable text string |
Important: All keys in MapReduce must implement WritableComparable (which extends both Writable and Comparable). Values only need to implement Writable. This is because keys are sorted during the Shuffle and Sort phase — they need to be comparable.
Pitfall: The Text class is NOT the same as Java String. Text uses UTF-8 encoding internally. A common mistake is to use String methods on a Text object — always call .toString() first to convert, then use String methods. Also, Text is mutable (you can call .set() to change its content), unlike String which is immutable.
4.4.2 Driver Setup and Job Configuration
A MapReduce application is launched via a Java Driver class. The Driver configures cluster execution parameters using the org.apache.hadoop.mapreduce.Job object:
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "Average Unit Price per Country");
// Specify Driver class JAR
job.setJarByClass(AverageDriver.class);
// Specify Mapper, Combiner, and Reducer classes
job.setMapperClass(AverageMapper.class);
job.setReducerClass(AverageReducer.class);
// job.setCombinerClass(AverageCombiner.class); // Optional
// Specify Output Key and Value types
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(DoubleWritable.class);
// Configure HDFS Input and Output File Paths
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// Set number of parallel Reducer tasks
job.setNumReduceTasks(4);
// Submit job to cluster and wait for completion
System.exit(job.waitForCompletion(true) ? 0 : 1);
Critical API Rules:
- Output directory must not exist — Hadoop will refuse to run if the output path already exists (prevents accidental data loss). Delete it first with
hdfs dfs -rm -r /output/path. - Map output types default to Reduce output types — If Mapper emits different types than Reducer, you MUST explicitly set
setMapOutputKeyClass()andsetMapOutputValueClass(). setNumReduceTasks(0)— Setting to 0 disables the Reduce phase entirely. The Map output goes directly to HDFS. Useful for embarrassingly parallel operations like format conversion.
4.4.3 Detailed Worked Code Walkthrough 1: Average Unit Price per Country
Worked Example — Complete MapReduce Application:
Consider an enterprise sales CSV dataset structured with columns: [Region, Country, Item_Type, Sales_Channel, Order_Priority, Order_Date, Order_ID, Ship_Date, Units_Sold, Unit_Price, Total_Revenue].
Our objective is to compute the average unit price for every unique country.
SQL Equivalent: SELECT Country, AVG(Unit_Price) FROM sales GROUP BY Country;
#### Mapper Implementation (AverageMapper.java)
public class AverageMapper extends Mapper<LongWritable, Text, Text, DoubleWritable> {
private Text countryKey = new Text();
private DoubleWritable priceValue = new DoubleWritable();
@Override
public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String line = value.toString();
String[] tokens = line.split(",");
if (tokens[0].equals("Region")) return; // Skip header row
if (tokens.length > 9) {
String country = tokens[1].trim();
double unitPrice = Double.parseDouble(tokens[9].trim());
countryKey.set(country);
priceValue.set(unitPrice);
context.write(countryKey, priceValue);
}
}
}
Type flow: Input \((K_1 = \text{LongWritable}(\text{byte offset}), V_1 = \text{Text}(\text{CSV line}))\) → Output \((K_2 = \text{Text}(\text{country}), V_2 = \text{DoubleWritable}(\text{unit\_price}))\)
#### Reducer Implementation (AverageReducer.java)
public class AverageReducer extends Reducer<Text, DoubleWritable, Text, DoubleWritable> {
private DoubleWritable resultAverage = new DoubleWritable();
@Override
public void reduce(Text key, Iterable<DoubleWritable> values, Context context)
throws IOException, InterruptedException {
double sumPrice = 0.0;
long recordCount = 0;
for (DoubleWritable val : values) {
sumPrice += val.get();
recordCount++;
}
double averagePrice = sumPrice / recordCount;
resultAverage.set(averagePrice);
context.write(key, resultAverage);
}
}
Type flow: Input \((K_2 = \text{Text}(\text{country}), \text{Iterable}(\text{DoubleWritable}(\text{prices})))\) → Output \((K_3 = \text{Text}(\text{country}), V_3 = \text{DoubleWritable}(\text{avg\_price}))\)
Important note: This Reducer computes average using (sum, count) internally — it does NOT use a naive Combiner. This is the correct approach.
#### Execution Dynamics: Serial vs Parallel Reducer Allocation
Worked Example — Single vs Parallel Reducers:
Scenario A: job.setNumReduceTasks(1) (Single Reducer):
All intermediate key-value groups are routed to a single Reducer node. The Reducer processes keys serially in strict alphabetical order:
| Order | Key | Values | Average |
|---|---|---|---|
| 1st | Bangladesh | [70.0] | 70.0 |
| 2nd | India | [100.0, 500.0] | 300.0 |
| 3rd | Pakistan | [200.0] | 200.0 |
| 4th | Sri Lanka | [300.0, 200.0, 50.0] | 183.33 |
Scenario B: job.setNumReduceTasks(4) (Parallel Reducers):
The HashPartitioner assigns distinct country keys to separate physical worker nodes across the cluster. All four Reducer nodes execute their reduce() loops concurrently in parallel, reducing overall query execution time linearly.
| Reducer Node | Key | Status |
|---|---|---|
| Reducer 0 | Bangladesh | Running |
| Reducer 1 | India | Running |
| Reducer 2 | Pakistan | Running |
| Reducer 3 | Sri Lanka | Running |
Performance impact: With 4 reducers, execution time is approximately \(\frac{1}{4}\) of the single-reducer case (assuming equal data distribution).
Exam note: Single Reducer processes keys serially in strict alphabetical key order. Multiple Reducers execute in parallel. The Partitioner determines which Reducer gets which key.
Recap + Bridge: The Writable serialization system replaces Java's Serializable for speed — raw bytes only, no metadata. The Driver class orchestrates the entire MapReduce job. The Average Unit Price example demonstrates the complete pattern with proper (sum, count) averaging. Now we explore advanced MapReduce patterns — sorting, composite keys, and SQL equivalence.
Real-World & Domain Connection: The Writable serialization pattern influenced modern data formats like Apache Parquet and Apache Avro, which also prioritize compact binary encoding. In production MapReduce jobs at companies like LinkedIn and Twitter, the choice between Writable types directly impacts job performance — using IntWritable vs. Text for numeric keys can change sort performance by 10× due to binary comparison efficiency.
4.5 Advanced MapReduce Worked Examples and SQL Equivalence
Hook: Every SQL query you've ever written — SELECT, GROUP BY, ORDER BY, HAVING, WHERE — has a direct equivalent in MapReduce. Understanding this mapping is the key to appreciating why high-level tools like Hive and Pig were invented: they translate your SQL into MapReduce automatically. But before you can optimize those translations, you need to understand the underlying patterns.
While Java MapReduce provides total low-level control over execution logic, writing verbose Java code for standard relational queries creates a steep developer learning curve. In later enterprise ecosystems, high-level abstractions like Apache Pig (Pig Latin) and Apache Hive (HQL SQL) were developed to automatically compile SQL queries into underlying MapReduce job DAGs.
This section presents three advanced architectural patterns in MapReduce alongside their ANSI SQL equivalents.
4.5.1 Worked Example 2: Global Dataset Sorting by Total Profit (Secondary Sorting)
SQL Equivalent:
SELECT total_profit, record_data
FROM sales_dataset
ORDER BY total_profit ASC;
MapReduce Strategy: Standard MapReduce automatically sorts intermediate keys during the Shuffle and Sort phase using a distributed merge sort. To sort a dataset by a specific numeric column, we simply invert the key-value structure in the Mapper — put the sort column as the KEY and the full record as the VALUE.
Why this works: The Shuffle and Sort phase sorts all intermediate keys. By making total_profit the key, we exploit the framework's built-in sorting to achieve global ordering without writing any comparison code.
Worked Example — Sorting by Total Profit:
Mapper Logic: The Mapper parses the CSV line, extracts total_profit, and sets total_profit as the output KEY (DoubleWritable). The entire raw record text is assigned as the output VALUE (Text). \[ \text{Mapper Output: } ( \text{total\_profit}, \text{record\_text} ) \]
Shuffle and Sort Phase: Hadoop automatically sorts all intermediate numeric keys in ascending order across the cluster network.
Reducer Logic: The Reducer receives intermediate keys in pre-sorted numeric sequence. It iterates over the values and writes the pre-sorted records directly to output files:
public class SortReducer extends Reducer<DoubleWritable, Text, DoubleWritable, Text> {
@Override
public void reduce(DoubleWritable key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
for (Text record : values) {
context.write(key, record);
}
}
}
Concrete trace with 5 records:
| Original Order | total_profit | After Shuffle & Sort |
|---|---|---|
| Record 3 | 150.0 | (150.0, "Record 3") — 1st |
| Record 1 | 200.0 | (200.0, "Record 1") — 2nd |
| Record 5 | 350.0 | (350.0, "Record 5") — 3rd |
| Record 2 | 500.0 | (500.0, "Record 2") — 4th |
| Record 4 | 800.0 | (800.0, "Record 4") — 5th |
Key insight: The Mapper emits nothing for filtering — every record passes through. The only purpose of Map here is to re-key the data by the sort column.
4.5.2 Worked Example 3: Minimum and Maximum Sales per Year and Country (Composite Keys)
SQL Equivalent:
SELECT year, country, MIN(total_sales), MAX(total_sales)
FROM sales_dataset
GROUP BY year, country;
MapReduce Strategy: Group By queries involving multiple columns require formulating a Composite Key. The Mapper concatenates the discrete field values into a single delimited text key string.
Why Composite Keys: MapReduce only supports a single key per record. To group by multiple columns, you must combine them into one key. The delimiter choice (underscore _ here) must be a character that never appears in the key values.
Worked Example — Composite Key for Year + Country:
Mapper Logic: The Mapper extracts year (e.g., "2017"), country (e.g., "Iran"), and total_sales (e.g., 700.0). It constructs a composite key string: "2017_Iran". \[ \text{Mapper Output: } ( \text{Text}("2017\_Iran"), \text{DoubleWritable}(700.0) ) \]
Intermediate Grouping: The framework groups all values sharing the exact composite key string:
"2017_Iran"\(\longrightarrow [200.0, 500.0, 700.0]\)"2017_India"\(\longrightarrow [100.0, 300.0]\)"2018_Iran"\(\longrightarrow [400.0, 600.0]\)
Reducer Logic (MinMaxReducer.java):
public class MinMaxReducer extends Reducer<Text, DoubleWritable, Text, Text> {
@Override
public void reduce(Text key, Iterable<DoubleWritable> values, Context context)
throws IOException, InterruptedException {
double minSales = Double.MAX_VALUE;
double maxSales = Double.MIN_VALUE;
for (DoubleWritable val : values) {
double current = val.get();
if (current < minSales) minSales = current;
if (current > maxSales) maxSales = current;
}
String resultStr = "Min: " + minSales + ", Max: " + maxSales;
context.write(key, new Text(resultStr));
}
}
Trace for "2017_Iran":
| Step | current | minSales | maxSales |
|---|---|---|---|
| Init | — | MAX_VALUE | MIN_VALUE |
| 1st val: 200.0 | 200.0 | 200.0 | 200.0 |
| 2nd val: 500.0 | 500.0 | 200.0 | 500.0 |
| 3rd val: 700.0 | 700.0 | 200.0 | 700.0 |
Output: "2017_Iran" → "Min: 200.0, Max: 700.0"
Sense-check: From [200, 500, 700], min=200 and max=700 — correct.
Pitfall: Choosing a delimiter that appears in key values will corrupt grouping. For example, if country names contain underscores (unlikely but possible), use a different delimiter like \t (tab) or | (pipe). Never use commas in composite keys if your input is CSV.
4.5.3 Worked Example 4: Maximum Profit Item Identification and Filtering
SQL Equivalent:
SELECT category, item_type, MAX(profit)
FROM sales_dataset
WHERE profit > 1000.0
GROUP BY category, item_type;
MapReduce Strategy — Combining WHERE, GROUP BY, and MAX:
- Mapper Filter (WHERE): The Mapper evaluates
profit > 1000.0. If a record falls below the threshold, the Mapper discards it immediately without emitting, preventing unnecessary network traffic.
- Mapper Key (GROUP BY): Emits
categoryas Key, and a custom Writable tuple(item_type, profit)as Value.
- Reducer Tracking (MAX): The Reducer iterates through intermediate values grouped by category, tracking the running scalar maximum profit and retaining the associated
item_typestring.
Key Pattern — SQL-to-MapReduce Mapping:
| SQL Clause | MapReduce Equivalent |
|---|---|
WHERE |
Mapper filtering (don't emit non-matching records) |
SELECT col1, col2 |
Mapper key-value emission |
GROUP BY |
Mapper key selection + Shuffle & Sort grouping |
ORDER BY |
Mapper key selection + Shuffle & Sort sorting |
HAVING |
Reducer-side filtering |
JOIN |
Multiple mappers + tagged values (Map-Side Join or Reduce-Side Join) |
DISTINCT |
Mapper keys + Reducer (keys are automatically deduplicated) |
Recap + Bridge: Three advanced MapReduce patterns: (1) Sorting exploits the framework's built-in key sort; (2) Composite keys enable multi-column GROUP BY; (3) Mapper-side filtering implements WHERE clauses. These patterns form the foundation for understanding Hive and Pig query compilation. Now we look at how non-Java languages can use these patterns through Hadoop Streaming.
4.6 Polyglot MapReduce: Hadoop Streaming API
Hook: Not everyone writes Java. Data scientists use Python, systems engineers use C++, and analysts use Ruby or R. Hadoop Streaming lets any of these languages run MapReduce jobs — no Java required. The secret: your program just reads from stdin and writes to stdout, and Hadoop handles the rest.
While Hadoop is written natively in Java, enterprise data engineering teams frequently include analysts and developers specializing in Python, Ruby, R, or C++. To support non-Java programming languages without requiring custom Java wrappers, Hadoop provides the Hadoop Streaming API.
Named References: The Hadoop Streaming protocol is formally documented in the Official Apache Hadoop Documentation and detailed in technical tutorials such as the Tutorialspoint Hadoop Streaming Guide.
4.6.1 Hadoop Streaming Architecture
Hadoop Streaming is a utility framework (hadoop-streaming.jar) that uses standard Unix input/output streams (stdin and stdout) to interface external executables with the Hadoop cluster.
Architecture Diagram:
+-------------------------------------------------------------------------+
| HADOOP STREAMING FRAMEWORK |
| |
| +--------------------+ stdin +--------------------------------+ |
| | HDFS Input Record | ----------> | External Mapper Executable | |
| | (LineText / Offset)| | (e.g. mapper.py / C++ binary) | |
| +--------------------+ +---------------+----------------+ |
| | |
| | stdout |
| v (Key \t Value) |
| +--------------------------------+ |
| | Cluster Shuffle & Sort Engine | |
| +---------------+----------------+ |
| | |
| +--------------------+ stdin +---------------+----------------+ |
| | HDFS Output File | <---------- | External Reducer Executable | |
| | (Final Aggregation)| stdout | (e.g. reducer.py / Ruby script)| |
| +--------------------+ +--------------------------------+ |
+-------------------------------------------------------------------------+
Key insight: Hadoop Streaming treats your external program as a black box. It doesn't need to know what language you're using — it only needs stdin/stdout communication. This means you can use ANY language: Python, Ruby, Perl, Bash, C++, Go, or even a compiled binary.
4.6.2 The Standard I/O Contract
Any executable binary or script can serve as a Mapper or Reducer in Hadoop Streaming, provided it adheres to two core rules:
Mapper Executable Contract:
- Reads raw text line-by-line from standard input (
stdin) - Processes each record
- Writes intermediate key-value pairs to standard output (
stdout) - Separator: Key and value must be separated by a tab character (
\t)
Reducer Executable Contract:
- Reads pre-sorted
key\tvaluelines line-by-line fromstdin - The framework guarantees lines arrive sorted by key
- Tracks key transitions (detects when a new key group begins)
- Computes aggregations per key group
- Writes final
key\tvaluelines tostdout
Critical: The tab character (\t) is the ONLY valid delimiter between key and value in Streaming. Using spaces or commas will cause the framework to misparse your output.
Pitfall: In Java MapReduce, the framework handles key grouping automatically (you get an Iterable<V> for each key). In Streaming, YOU must detect key transitions yourself — when the current key differs from the previous key, you've entered a new group. This is a common source of bugs for developers transitioning from Java MapReduce to Streaming.
4.6.3 Executing a Python MapReduce Job via Hadoop Streaming
Worked Example — Python Streaming Command:
To execute a Python MapReduce job across a cluster, submit the job using the hadoop jar command specifying the streaming library:
hadoop jar \$HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-*.jar \
-files mapper.py,reducer.py \
-mapper "python mapper.py" \
-reducer "python reducer.py" \
-input /user/hadoop/sales_data.csv \
-output /user/hadoop/output_python_sales
Parameter Roles:
| Parameter | Purpose | Example |
|---|---|---|
-files |
Ships local script files across the network to all worker DataNodes | mapper.py,reducer.py |
-mapper |
Shell command to launch the mapper process on worker nodes | "python mapper.py" |
-reducer |
Shell command to launch the reducer process on worker nodes | "python reducer.py" |
-input |
HDFS input path (file or directory) | /user/hadoop/sales_data.csv |
-output |
HDFS output path (must not exist) | /user/hadoop/output/ |
Optional parameters:
-combiner: Specifies a combiner executable (can be same as reducer)-numReduceTasks 0: Disables reduce phase (map-only job)-D mapred.reduce.tasks=4: Sets number of reducers via Java property
Worked Example — Minimal Python Mapper/Reducer:
mapper.py:
#!/usr/bin/env python
import sys
for line in sys.stdin:
line = line.strip()
fields = line.split(",")
if fields[0] == "Region": # Skip header
continue
country = fields[1]
unit_price = float(fields[9])
print "%s\t%s" % (country, unit_price)
reducer.py:
#!/usr/bin/env python
import sys
last_key = None
total = 0.0
count = 0
for line in sys.stdin:
line = line.strip()
key, value = line.split("\t")
if last_key and last_key != key:
print "%s\t%s" % (last_key, total / count)
total = 0.0
count = 0
last_key = key
total += float(value)
count += 1
if last_key:
print "%s\t%s" % (last_key, total / count)
How it works: The mapper reads CSV lines, extracts country and price, and emits country\tprice. The reducer detects key transitions (when country changes) and computes the average for each country. The framework handles the sorting and grouping between them.
Exam note: Hadoop Streaming uses tab-delimited (\t) key-value text streams. The framework sorts lines by key between map and reduce. The reducer must detect key transitions manually. Any language that can read stdin and write stdout can be used.
Recap + Bridge: Hadoop Streaming enables polyglot MapReduce by communicating via stdin/stdout with tab-delimited key-value pairs. The Mapper reads lines and emits key\tvalue to stdout. The Reducer reads sorted key\tvalue lines and must track key transitions itself. This completes our coverage of the MapReduce execution model — from HDFS storage through the Map/Reduce pipeline, optimizations (Combiner/Partitioner), Java API, advanced patterns, and now cross-language support.
Real-World & Domain Connection: Hadoop Streaming was widely used by data science teams before Apache Spark became dominant. Python-based MapReduce via Streaming powered early recommendation systems at Spotify and YouTube. Today, the Streaming concept has evolved into frameworks like Apache Beam and Kafka Streams, which maintain the same stdin/stdout-like abstraction for language-agnostic stream processing.
Exam Guidance Summary
Exam note: This summary consolidates all exam-critical points from Lecture 4. Review each item and ensure you can explain it from memory.
1. Rack Awareness Distance Rules: Memorize the network hop distance calculations:
- Intra-rack node distance: 2 hops (Node 1 \(\to\) Rack Switch \(\to\) Node 2)
- Inter-rack node distance: 4 hops (Node 1 \(\to\) Local Switch \(\to\) Core Switch \(\to\) Remote Switch \(\to\) Node 9)
2. HDFS File Read Locality Placement: On file read requests, NameNode returns DataNode addresses sorted by physical network hop distance relative to client. Local rack DataNodes are always prioritized over remote rack DataNodes.
3. Combiner Mathematical Eligibility: A Combiner can ONLY be applied if the reduction operation satisfies both:
- Commutative: \(a \oplus b = b \oplus a\)
- Associative: \((a \oplus b) \oplus c = a \oplus (b \oplus c)\)
4. Average Combiner Trap: Be prepared to prove why standard Average cannot use a simple Combiner. Show that: \[ \text{Avg}(\text{Avg}(3,2), 5) = \frac{2.5 + 5.0}{2} = 3.750 \] whereas true average is: \[ \frac{3+2+5}{3} = \frac{10}{3} = 3.333 \] Fix: Emit (sum, count) tuples instead of averages.
5. Partitioner Routing Formula: Default HashPartitioner routes keys to reducers via: \[ \text{Reducer Index} = \left| \text{hashCode}(\text{key}) \right| \pmod{\text{numReduceTasks}} \] Single reducer executes tasks serially in strict alphabetical key order; multiple reducers execute in parallel.
6. MapReduce Signature Types: Memorize the complete key-value signature transformation: \[ \text{Map}: (K_1, V_1) \to \text{List}(K_2, V_2) \] \[ \text{Shuffle}: \text{List}(K_2, V_2) \to (K_2, \text{Iterable}(V_2)) \] \[ \text{Reduce}: (K_2, \text{Iterable}(V_2)) \to \text{List}(K_3, V_3) \]
7. Hadoop Writable System: Understand why Java native serialization is replaced by Hadoop Writable types (IntWritable, DoubleWritable, Text). Keys must implement WritableComparable; values implement Writable.
8. Hadoop Streaming Mechanics: Non-Java programs interface via stdin/stdout using tab-delimited (\t) key-value text streams passed via hadoop-streaming.jar. Reducer must track key transitions manually.
9. Map vs Reduce Output Storage: Map output goes to local disk (temporary, no replication). Reduce output goes to HDFS (persistent, replicated). This is because map output is intermediate data; reduce output is the final result.
10. SQL-to-MapReduce Equivalents:
WHERE→ Mapper filteringGROUP BY→ Mapper key + Shuffle & SortORDER BY→ Mapper key (sort column) + Shuffle & SortHAVING→ Reducer filtering
Key Industry Applications
Real-World & Domain Connection: The MapReduce and HDFS concepts from this lecture power critical infrastructure across multiple industries. Below are three major application domains.
1. Sustainability and Green Cloud Computing: Energy-efficient big data processing leverages Combiner pre-aggregations to reduce network switch traffic and CPU disk writes by up to 90%. Deployed across hyper-scale cloud providers (such as Microsoft Azure and Google Cloud) to lower data center power consumption and meet corporate carbon-neutral targets. A 90% reduction in shuffle data translates directly to proportional savings in network electricity costs — significant when operating clusters with 50,000+ nodes.
2. Enterprise Financial Data Pipelines: Processing banking transaction records across global geographic regions. Using custom Partitioners and Composite Keys (Year_Country) to calculate localized regional spend metrics and min/max credit volumes concurrently. Financial institutions use MapReduce for daily settlement processing, fraud detection across millions of transactions, and regulatory reporting. The rack-aware replication strategy ensures transaction logs survive individual rack failures without data loss.
3. Distributed Log and Sales Analytics: Processing multi-terabyte enterprise sales datasets. Translating relational SQL analytics (GROUP BY, ORDER BY, HAVING) into optimized MapReduce job execution chains for automated daily financial statement generation. Retail companies like Walmart and Amazon process petabytes of clickstream and sales data using MapReduce patterns, with HDFS providing the storage layer that handles the volume and velocity of incoming data.
BDA Lecture 4 notes
Sections Breakdown
HDFS uses a master-slave architecture with NameNode (metadata) and DataNodes (block storage). Rack awareness measures distance in network hops (2 same rack, 4 cross-rack). File writes follow a pipeline protocol with ACK chains; reads prioritize nearest replica.
MapReduce processes data through Record Reader → Map → Shuffle & Sort → Reduce. Data locality moves computation to data. Map output goes to local disk; Reduce output goes to HDFS. Classroom analogy: coin counting with students as mappers.
Combiner pre-aggregates Map output locally (requires commutative + associative ops). Average fails: Avg(Avg(3,2),5)=3.75 ≠ true 3.33. Fix: emit (sum,count). Partitioner routes keys via hash mod numReducers.
Hadoop Writable serialization replaces Java Serializable for speed (no metadata). Driver class configures Mapper, Reducer, and job parameters. Complete code walkthrough for Average Unit Price per Country.
Three advanced patterns: (1) Sorting by re-keying data to exploit framework's built-in sort; (2) Composite keys for multi-column GROUP BY; (3) Mapper-side filtering for WHERE clauses. SQL-to-MapReduce mapping table provided.
Hadoop Streaming enables non-Java MapReduce via stdin/stdout with tab-delimited key-value pairs. Mapper reads stdin, emits key\tvalue to stdout. Reducer reads sorted key\tvalue lines, must track key transitions manually.
Consolidated exam guidance covering: rack awareness distances, HDFS read locality, Combiner commutative/associative requirements, Average counterexample, Partitioner formula, MapReduce type signatures, Writable system, Streaming mechanics.
Three application domains: (1) Green cloud computing via Combiner optimization, (2) Financial data pipelines with custom Partitioners and Composite Keys, (3) Distributed log/sales analytics translating SQL to MapReduce.
Exam Revision Notes
Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.
HDFS Topology, Balancing, Rack Awareness, and File I/O Anatomy
Must-know: Network hop distances: 2 hops same rack, 4 hops cross-rack. NameNode sorts DataNode addresses by distance on read. Write pipeline: client→DN1→DN5→DN9 with ACK chain.
\[\text{distance}(n_1, n_2) = d(n_1, \text{LCA}) + d(n_2, \text{LCA})\]
⚠️ Top pitfall: Secondary NameNode is NOT a hot standby; cannot replace failed NameNode. 10-minute heartbeat timeout before DataNode declared dead.
Self-check: If a client on Rack 1 reads Block A replicated on DN1 (Rack 1), DN5 (Rack 2), DN9 (Rack 2), which DataNode serves the read and why?
Connects to: MapReduce Programming Model, Execution Lifecycle, and Core Abstractions
MapReduce Programming Model, Execution Lifecycle, and Core Abstractions
Must-know: MapReduce signature: Map(K1,V1)→List(K2,V2), Shuffle: List(K2,V2)→(K2,Iterable<V2>), Reduce(K2,Iterable<V2>)→List(K3,V3). Map output to local disk, Reduce output to HDFS.
\[\text{Map: } (K_1, V_1) \longrightarrow \text{List}(K_2, V_2)\]
⚠️ Top pitfall: Map output types (K2,V2) can differ from Reduce output types (K3,V3) — must set map output types explicitly if different.
Self-check: Why does MapReduce write map output to local disk instead of HDFS?
Connects to: HDFS Topology, Balancing, Rack Awareness, and File I/O Anatomy; MapReduce Optimizations: Combiner and Partitioner
MapReduce Optimizations: Combiner and Partitioner
Must-know: Combiner requires commutative (a⊕b=b⊕a) AND associative ((a⊕b)⊕c=a⊕(b⊕c)). Average fails: prove with Avg(Avg(3,2),5)=3.75≠3.33. Fix: emit (sum,count). Partitioner: hash(key) mod numReducers.
\[\text{Reducer Index} = \left| \text{hashCode}(K_2) \right| \pmod{\text{numReduceTasks}}\]
⚠️ Top pitfall: Average is NOT safe for naive Combiner. Combiner may run 0, 1, or many times — correctness must not depend on it.
Self-check: Can you use a Combiner for calculating median? Why or why not?
Connects to: MapReduce Programming Model, Execution Lifecycle, and Core Abstractions; Java MapReduce API and Serialization Data Types
Java MapReduce API and Serialization Data Types
Must-know: Writable vs Serializable: Writable has no metadata overhead. Keys must implement WritableComparable. Single Reducer processes keys serially in alphabetical order; multiple Reducers run in parallel.
⚠️ Top pitfall: Text class is NOT Java String — mutable, UTF-8 based. Must call .toString() first.
Self-check: What interface must MapReduce keys implement, and why?
Connects to: MapReduce Optimizations: Combiner and Partitioner; Advanced MapReduce Worked Examples and SQL Equivalence
Advanced MapReduce Worked Examples and SQL Equivalence
Must-know: ORDER BY = put sort column as key. GROUP BY multiple columns = composite key with delimiter. WHERE = mapper filtering (don't emit non-matching records). HAVING = reducer-side filtering.
⚠️ Top pitfall: Composite key delimiter must not appear in key values. Using underscore with country names that contain underscores will corrupt grouping.
Self-check: How would you implement SELECT year, country, AVG(sales) FROM table GROUP BY year, country in MapReduce?
Connects to: MapReduce Programming Model, Execution Lifecycle, and Core Abstractions; MapReduce Optimizations: Combiner and Partitioner
Polyglot MapReduce: Hadoop Streaming API
Must-know: Streaming uses stdin/stdout with tab-delimited (\t) key-value pairs. Reducer must detect key transitions manually. -files ships scripts to DataNodes. Any language that reads stdin/writes stdout works.
⚠️ Top pitfall: In Streaming, the reducer must manually track key transitions (unlike Java MapReduce which provides Iterable<V>). Tab is the only valid delimiter.
Self-check: What is the key difference between Java MapReduce and Hadoop Streaming in terms of how the reducer processes grouped data?
Connects to: MapReduce Programming Model, Execution Lifecycle, and Core Abstractions; Java MapReduce API and Serialization Data Types