MSN-013 · 2026 · hadoop, mapreduce, spark, kubernetes, docker, distributed-systems, aws, benchmarking
Systems evaluation of distributed compute: MapReduce Order Inversion & Stripes, Spark shuffle memory mechanics, bare-metal Hadoop/EMR, and Kubernetes microservice orchestration.
Distributed computing is frequently treated as a magical abstraction layer: write a functional transformation or query, submit it to a cluster manager, and trust the runtime to schedule tasks across hundreds of worker nodes. But at datacenter scale, naive abstractions break down violently.
A poorly partitioned MapReduce key space floods switch bisection bandwidth with catastrophic shuffle hot spots; an innocent `.groupByKey()` call in Apache Spark forces full in-memory serialization across partitions, triggering severe JVM garbage collection pauses, disk spilling, and Executor out-of-memory crashes; and assuming that cloud virtual machines scale compute linearly with vCPU counts ignores the physical realities of hypervisor scheduling, SMT thread contention, and memory bus channel saturation.
This investigation conducts empirical systems evaluations, algorithmic implementations, and infrastructure orchestration across modern distributed computing architectures: 1. Cloud Hardware Microbenchmarking & Virtualization Overhead: Empirical performance evaluation of AWS EC2 compute, DDR3 vs. DDR4 memory channel architecture, and intra-VPC vs. cross-region WAN transit latency. 2. Hadoop MapReduce Algorithmic Optimization: Implementation of the Pairs vs. Stripes co-occurrence models, the Order Inversion design pattern with custom Partitioners, and a two-pass correlation pipeline using HDFS distributed cache preloading. 3. Apache Spark RDD Internals & Shuffle Optimization: Memory hierarchy profiling, execution plan differences between `groupByKey()` and `reduceByKey()`, and a distributed log telemetry pipeline analyzing NASA Kennedy Space Center HTTP access traces. 4. Cluster Orchestration & Production Deployment Architectures: Bare-metal multi-node Hadoop/YARN provisioning, managed AWS EMR clusters with S3 storage-compute decoupling, Docker containerized runtimes, and declarative Kubernetes microservice orchestration.
- SMT Contention: On CPU-intensive prime calculations, two independent physical cores (`t2.medium`) achieved scaling, while two hyperthreads sharing a single physical core (`c5d.large`) achieved only scaling due to execution unit and L1/L2 cache contention. - Memory Bus Architecture: `c5d.large` delivered memory throughput ( over `t2.medium` at ) by utilizing 6-channel DDR4-2666 ( theoretical peak) versus 4-channel DDR3-1600 ( peak). - WAN Transit Penalty: Cross-continental traffic between AWS `us-east-1` (Virginia) and `us-west-2` (Oregon) suffered a latency spike () and an throughput collapse (). - Streaming Marginal Computation: The Order Inversion pattern combined with custom partition routing eliminated all Reducer-side in-memory buffering for relative frequency calculations, computing in true space.
---
To evaluate how cloud hypervisors allocate physical silicon to multi-tenant virtual machines, I benchmarked AWS EC2 instances across the General Purpose (`t2.micro`, `t2.medium`) and Compute Optimized (`c5d.large`) families in the `us-east-1` (N. Virginia) region running Ubuntu 22.04 LTS.
While automated suites like the Phoronix Test Suite are common in desktop evaluations, they present significant memory footprints and heavy background dependency chains that cause immediate out-of-memory crashes on small cloud instances (`t2.micro` with 957 MB RAM).
I selected Sysbench 1.0.20 and iPerf3 for deterministic, low-overhead resource characterization: - Single-Threaded CPU: `sysbench cpu --cpu-max-prime=20000 --threads=1 run` (isolates single-core latency-sensitive execution speed). - Multi-Threaded CPU: `sysbench cpu --cpu-max-prime=20000 --threads=$(nproc) run` (evaluates parallel throughput and core scaling efficiency). - Memory Subsystem: `sysbench memory --memory-total-size=10G --threads=$(nproc) run` (measures multi-threaded sequential read/write bandwidth).
Instance Type · vCPUs · RAM · Single-Core CPU (events/s) · Multi-Core CPU (events/s) · Multi-Core Scaling · Memory Bandwidth (MiB/s)
`t2.micro` · 1 · 957 MB · 877.95 · 880.11 · · 521.58 `t2.medium` · 2 · 3.8 GB · 882.32 · 1,565.29 · · 855.68 `c5d.large` · 2 · 3.7 GB · 450.82 · 703.32 · · 7,578.52
AWS EC2 Hardware Subsystem & Microarchitecture Benchmarks
Running `lscpu` on the instances uncovered a critical discrepancy in how AWS provisions 2 vCPUs:
t2.medium:
CPU(s): 2
Thread(s) per core: 1 (2 independent physical cores, Intel Xeon E5-2686 v4 @ 2.30 GHz)
Core(s) per socket: 2
c5d.large: CPU(s): 2 Thread(s) per core: 2 (1 physical core split into 2 logical hyperthreads, Xeon Platinum 8124M @ 3.00 GHz) Core(s) per socket: 1
1. Why `t2.medium` Scales Better for CPU: `t2.medium` provides two physical silicon cores with dedicated ALU pipelines, floating-point units, and private L1/L2 caches. When running two compute-intensive threads simultaneously, it achieves near-linear scaling. In contrast, `c5d.large`'s two vCPUs are Simultaneous Multi-Threading (SMT) siblings executing on a single physical core; when both threads stress compute-bound prime calculation, execution units stall on pipeline structural hazards, capping scaling at . 2. Why `c5d.large` Single-Core Event Rate Looks Lower: The `c5d.large` instance runs an Intel Xeon Platinum 8124M (Skylake). Even in a single-threaded test, hypervisor time-slicing and background logical sibling interference reduce raw event counts for pure integer prime verification, but its memory architecture tells a vastly different story.
`c5d.large` outperformed `t2.medium` in memory throughput by a staggering ( vs. ).
Theoretical memory bus bandwidth is governed by:
- Intel Xeon E5-2686 v4 (Broadwell, `t2` instances): - Intel Xeon Platinum 8124M (Skylake, `c5d` instances):
The Skylake platform provides 50% more memory channels (6 vs. 4) and 67% faster transfer rates (2666 vs. 1600 MT/s). Furthermore, AWS enforces aggressive hypervisor-level throttling on burstable `t2` tiers sharing a host bus, whereas compute-optimized `c5d` instances receive dedicated non-throttled memory controller slices.
Using `iPerf3` (TCP bandwidth) and ICMP `ping` (Round-Trip Time latency), I mapped network performance within `us-east-1` across instance combinations, and contrasted it against cross-continental links to `us-west-2` (Oregon):
Pairing · Link Type · TCP Bandwidth · RTT Latency
`c5n.large` `c5n.large` · Intra-Region (Private IP) · 4.97 Gbps · 0.200 ms `m5.large` `m5.large` · Intra-Region (Private IP) · 4.97 Gbps · 0.238 ms `t3.medium` `t3.medium` · Intra-Region (Private IP) · 4.09 Gbps · 0.248 ms `m5.large` `t3.medium` · Intra-Region (Private IP, Heterogeneous) · 4.87 Gbps · 1.085 ms `c5.large` `c5.large` · Cross-Region (`us-east-1` to `us-west-2`) · 529 Mbps · 54.800 ms
Within the same AWS VPC, TCP bandwidth remained pegged at the 5 Gbps Elastic Network Adapter (ENA) hypervisor limit. However, same-type pairs experienced RTT, while cross-type pairs (`m5.large` to `t3.medium`) jumped to ( higher). This demonstrates that AWS availability zones place identical instance families into the same physical datacenter racks and top-of-rack (ToR) switches, minimizing network hops.
---
In distributed data processing, computing co-occurrence frequencies across large corpora is the canonical benchmark for measuring network shuffle bottlenecks and memory pressure.
Given a text corpus, the objective is to compute the relative bigram frequency:
The naive approach emits every word pair as an intermediate composite key:
// Mapper emits (prev, w) -> 1
BIGRAM.set(prev, w);
context.write(BIGRAM, ONE);
- Pros: Minimal memory footprint in the Mapper. State is strictly per emitted pair. - Cons: Intermediate key space explodes. For a document of words, the shuffle phase must transfer and sort key-value records across the network, saturating cluster bisection bandwidth.
Instead of emitting individual pairs, the Mapper aggregates co-occurrences in-memory within an associative array (a "stripe") for each leading word :
// Mapper maintains in-memory stripe: Text(A) -> HashMapStringIntWritable(B -> count)
STRIPE.increment(w);
context.write(KEY, STRIPE);
- Pros: The network shuffle volume drops drastically. Reducers receive pre-aggregated stripes, transforming shuffle communication from pair records to stripe maps. - Cons: High heap memory pressure. If the vocabulary or context window is large, the in-mapper stripe map overflows available JVM heap space, risking `java.lang.OutOfMemoryError: Java heap space`.
PAIRS APPROACH:
[Mapper] ---> (A, B): 1, (A, C): 1, (A, B): 1 ---> [Heavy Network Shuffle] ---> [Reducer]
STRIPES APPROACH: [Mapper] ---> In-Memory Map {B: 2, C: 1} ---> [Light Network Shuffle] ---> [Reducer]
The primary challenge in the Pairs approach is computing : the Reducer needs the marginal sum *before* it can normalize the first pair .
A naive Reducer would buffer all pairs in an in-memory `ArrayList`, sum their counts, and then iterate through the list to divide. This breaks the fundamental tenet of MapReduce: never buffer an unbounded dataset in Reducer memory, as high-cardinality keys will inevitably crash the worker.
MapReduce Order Inversion Architecture Flow & O(1) Memory
I solved this using the Order Inversion Pattern:
// 1. Mapper emits special sentinel pair for the marginal count
BIGRAM.set(prev, "*");
context.write(BIGRAM, ONE);
// 2. Mapper emits actual bigram BIGRAM.set(prev, w); context.write(BIGRAM, ONE);
// 3. Custom Partitioner hashes ONLY on the left word (A)
public static class MyPartitioner extends Partitioner<PairOfStrings, IntWritable> {
@Override
public int getPartition(PairOfStrings key, IntWritable value, int numReduceTasks) {
return (key.getLeftElement().hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
// 4. Reducer processes the sentinel FIRST due to natural ASCII sort order ('*' < 'A')
if (key.getRightElement().equals("*")) {
marginal = sum; // Captured first! No buffering required.
OUTPUT_KEY.set(key.getLeftElement(), "");
VALUE.set(marginal);
context.write(OUTPUT_KEY, VALUE);
} else {
VALUE.set((float) sum / marginal); // Streamed directly to disk in O(1) memory!
context.write(key, VALUE);
}
Because the ASCII value of `*` (42) is lower than alphanumeric characters, Hadoop's intermediate sort phase guarantees that `(A, "*")` arrives at the Reducer before any real `(A, B)` pair. The Reducer records `marginal = sum` in a scalar primitive and processes subsequent pairs in a single streaming pass—achieving zero memory allocation overhead.
For computing symmetric word correlation coefficients: where is the number of co-occurring lines and is the total document frequency of word .
This cannot be completed in a single MapReduce job because and are global properties of the corpus.
I engineered a two-pass MapReduce pipeline: 1. Pass 1 (Global Word Count): Standard MapReduce job calculating across all lines, persisting output to intermediate HDFS storage (`mid/part-r-00000`). 2. Pass 2 (Distributed Cache Preloading in `setup()`): In `CORPairs.java` and `CORStripes.java`, the Reducer overrides the `setup()` lifecycle hook to open the intermediate HDFS file directly via the Hadoop `FileSystem` API, pre-populating an in-memory lookup table:
@Override
protected void setup(Context context) throws IOException, InterruptedException {
Configuration conf = context.getConfiguration();
Path mid_path = new Path(conf.get("MIDDLE_OUTPUT") + "/part-r-00000");
FileSystem fs = FileSystem.get(conf);
BufferedReader reader = new BufferedReader(new InputStreamReader(fs.open(mid_path)));
String line;
while ((line = reader.readLine()) != null) {
String[] terms = line.split("\\s+");
word_total_map.put(terms[0], Integer.parseInt(terms[1]));
}
reader.close();
}
The Mapper extracts unique tokens per line using a `HashSet`, sorts them alphabetically to emit canonical pairs (preventing double-counting), and the Reducer evaluates with lookups.
---
While Hadoop MapReduce enforces a strict Map-Sort-Reduce barrier that writes all intermediate data to local disk between stages, Apache Spark retains working sets in memory across a directed acyclic graph (DAG) of transformations.
A common failure mode in production PySpark applications is using `groupByKey()` for aggregation. I profiled both execution plans on a text corpus:
# ANTI-PATTERN: groupByKey()
words.map(lambda w: (w, 1)) \
.groupByKey() \
.mapValues(sum)
OPTIMAL: reduceByKey()
words.map(lambda w: (w, 1)) \
.reduceByKey(lambda a, b: a + b)
Apache Spark RDD Shuffle Mechanics: groupByKey vs reduceByKey
1. `groupByKey()` Shuffle Mechanics: Spark serializes every single key-value tuple over the network. If the word `"the"` appears 27,361 times across partitions, 27,361 individual records are serialized, transferred over the network, and unpacked into an in-memory `CompactBuffer` on the target executor. When partition cardinality is high, this buffer spills to disk, crushing I/O throughput. 2. `reduceByKey()` Combiner Optimization: Spark applies a map-side partial reduction (analogous to MapReduce's Combiner) within each executor partition *before* network shuffle serialization. The 27,361 instances of `"the"` are reduced to a single integer per partition prior to crossing the network, reducing network traffic and memory footprint by orders of magnitude.
To evaluate Spark's capabilities on unstructured production telemetry, I built an end-to-end log parsing and analytics pipeline processing August 1995 NASA Kennedy Space Center HTTP server logs.
# Compiled regex matching Apache Common Log Format
LOG_PATTERN = '^(\\S+) (\\S+) (\\S+) \\[([\\w:/]+\\s[+\\-]\\d{4})\\] "(\\S+) (\\S+)(?:\\s+(\\S+))?" (\\d{3}) (\\d+|-)'
The pipeline tokenizes raw text streams into structured NamedTuples: - Client Host (`host`) - Timestamps (`date_time`) - HTTP Verb & Endpoint (`method`, `endpoint`) - Response Status Code (`response_code`) - Content Byte Length (`content_size`, with `-` normalized to 0)
Calling `.cache()` or `.persist(StorageLevel.MEMORY_AND_DISK)` on the parsed `access_logs` and `badRecords` RDDs prevented Spark from re-reading and re-executing the expensive regex tokenization pipeline from source disk for every downstream analytical query. Because Spark uses lazy evaluation, failure to cache turns every subsequent action (`count()`, `takeOrdered()`, `reduceByKey()`) into a complete re-read of the raw input file.
---
Beyond algorithmic execution, distributed systems require robust operational topologies. I deployed, managed, and profiled distributed runtimes across bare-metal, managed cloud, containerized, and orchestrated Kubernetes infrastructure:
Architectural Dimension · Co-located Bare-Metal HDFS (Lab 6) · Decoupled Cloud Storage on AWS EMR (Lab 5)
Compute Lifecycle · Persistent 24/7 (instances must remain online) · Ephemeral (clusters spin up, execute steps, auto-terminate) Storage Durability · or replication bound to EBS volumes · 11 9's durability natively managed by Amazon S3 Idle Cost Profile · High (0.023/GB/month S3 storage) Resource Scaling · Coupled: adding storage forces buying more compute · Independent: scale worker nodes without repartitioning disk
Kubernetes DockerCoins Cluster Topology & Autoscaling Telemetry
- Declarative Infrastructure Manifests: Authored declarative YAML specifications for `Pods`, `Deployments`, and `Services`. - Service Discovery & Inter-Process Communication: Deployed internal `ClusterIP` services for headless backend RPC communication and exposed external control endpoints using `NodePort` (`30001`) and port-forwarding proxies (`kubectl port-forward`). - Distributed Workload Orchestration: Orchestrated the 5-service `DockerCoins` distributed mining architecture (`webui`, `rng`, `hasher`, `worker`, `redis`). - Horizontal Pod Autoscaling Benchmarks: - Baseline (1 Worker): Clocks , strictly bounded by single-threaded Python/Ruby event loop saturation on candidate hash generation. - Scale-Out (4 Workers): Executing `kubectl scale deployment worker --replicas=4` scaled aggregate cluster mining throughput to ( linear speedup across 4 vCPUs). - Fault Tolerance & Self-Healing Telemetry: Forcefully terminating an active worker pod (`kubectl delete pod
---