Why did 384 MB become 512?
Riya asked YARN for map containers of 384 MB. The ResourceManager's numbers say every map is holding 512 MB. Kabir has the opposite problem: his job keeps every order line in a list, and with a 64 MB heap it died with “Java heap space” ten times before Hadoop gave up. Arjun Sir pulls up one worker's memory on the whiteboard.
Two different limits are at work. YARN hands out containers: slices of a node's memory and CPU, in fixed steps. Inside each container runs a JVM with its own heap limit, which must be smaller than the container.
So my 384 MB was rounded up by YARN, and Kabir's job ran out of heap inside the JVM?
And I can't just set the heap to 2 GB, because then it won't fit in the container, or the container won't fit on the node.
Exactly. Today we measure both on our cluster: how requests are rounded, how many containers fit, what happens to CPU, and how a heap that is too small kills a job. Then we size a real 128 GB server the same way.
YARN memory and CPU tuning
Memory and CPU tuning in YARN is the configuration of how much of each node's resources YARN may allocate and how large each container and its process may be. Each NodeManager advertises a capacity (yarn.nodemanager.resource.memory-mb, yarn.nodemanager.resource.cpu-vcores), which must leave room for the operating system and co-located daemons. Applications request containers of a given memory and number of virtual cores; the scheduler normalizes each request up to a multiple of yarn.scheduler.minimum-allocation-mb and rejects requests above the maximum allocation. A resource calculator decides which dimensions are compared: the DefaultResourceCalculator uses memory only, while the DominantResourceCalculator uses memory and CPU (Dominant Resource Fairness). Inside a container, the JVM heap (-Xmx, set via mapreduce.map.java.opts or a heap-to-container ratio) must be smaller than the container, leaving room for off-heap memory; exceeding the heap raises an OutOfMemoryError, while exceeding the container's physical memory causes the NodeManager to kill it when memory checks are enabled.
Decide how much of each machine YARN may hand out, what size the pieces are, and how big the Java program inside each piece may grow. Pieces too big: few tasks run at once. Pieces too small: tasks crash.
Words you'll meet in this lesson
From our lab (Hadoop 3.5.0, 3 workers × 2048 MB / 2 vcores)
| Rounding | Map asks 300 → 512 MB, 384 → 512, 600 → 768, 1100 → 1280 MB (multiples of 256) |
|---|---|
| Packing | 24 maps of 384 MB: 12 containers running, 6144 of 6144 MB. Maps of 1024 MB: 6 containers, 5632 MB |
| CPU | Default calculator: 12 containers holding 12 vcores on 6 real vcores. DominantResourceCalculator: 6 containers, 3072 MB, 6 of 6 vcores |
| Heap | A map that keeps every line: -Xmx64m → FAILED, “Error: Java heap space”, Failed map tasks=10. -Xmx300m → succeeded, 169–235 MB used per map |
Architecture at a glance: the memory stack of one worker
Every number in a YARN cluster is one layer of this stack. Left: a typical production worker. Right: our lab worker.
Tuning at three levels
Beginner A hostel's rooms and beds
- A hostel building (node) has 2048 sq ft that the warden (YARN) may rent out; the rest is corridors and the warden's office (OS and daemons).
- Rooms come only in fixed sizes of 256 sq ft. Ask for 384 and you get 512: the room size is rounded up.
- Inside your room, your cupboard (heap) is smaller than the room. If you stuff more clothes than the cupboard holds, it bursts (Java heap space), even if the room has space.
- Bigger rooms mean fewer students fit in the building: 12 rooms of 512 vs 6 of 1024 in our cluster.
Intermediate The settings and how they connect
- Node:
yarn.nodemanager.resource.memory-mband…cpu-vcoressay what a NodeManager offers. Ours: 2048 MB, 2 vcores. - Scheduler:
yarn.scheduler.minimum-allocation-mb(step) and…maximum-allocation-mb(cap). A 1100 MB request became 1280 MB. - Job:
mapreduce.map.memory.mb(container) andmapreduce.map.java.opts=-Xmx…(heap). Rule of thumb: heap ≈ 80% of the container. Since Hadoop 3 you may set only the container and letmapreduce.job.heap.memory-mb.ratio(0.8) derive the heap. - AM:
yarn.app.mapreduce.am.resource.mb(ours 512) is a container too, and counts against the cluster.
Advanced CPU, fairness and safety nets
- With the DefaultResourceCalculator (the default for the Capacity Scheduler), vcores are recorded but not enforced in placement: our nodes got 12 containers with 1 vcore each on 6 real vcores.
- The DominantResourceCalculator compares each request on its dominant share (memory or CPU). Our cluster then ran only 6 containers and left half the memory free: CPU had become the limit.
- Placement is only half the story: to stop a process from using more than its share, NodeManagers use memory checks (
yarn.nodemanager.pmem-check-enabled) or Linux cgroups for memory and CPU. Our lab has the memory checks switched off. - Tiny jobs can run in uber mode (
mapreduce.job.ubertask.enable): all tasks inside the AM's container, saving container start-ups.
Exam tip: For “How do you configure memory for MapReduce on YARN?”, draw the stack (node RAM → yarn.nodemanager.resource.memory-mb → container mapreduce.map.memory.mb → heap -Xmx ≈ 80%), mention rounding to minimum-allocation-mb, and give the two failure modes: Java heap space (heap too small) and container killed for exceeding physical memory (heap or off-heap too big for the container).
The properties, and the two resource calculators
| Property | Where | Means | Our lab | Default |
|---|---|---|---|---|
yarn.nodemanager.resource.memory-mb | yarn-site | Memory a node offers to YARN | 2048 | 8192 (or detected) |
yarn.nodemanager.resource.cpu-vcores | yarn-site | vcores a node offers | 2 | 8 |
yarn.scheduler.minimum-allocation-mb | yarn-site | Step size of every request | 256 | 1024 |
yarn.scheduler.maximum-allocation-mb | yarn-site | Largest container | 8192 (capped by the 2048 MB nodes) | 8192 |
mapreduce.map.memory.mb / reduce | mapred-site or -D | Container size per task | 384 (→ 512) | -1 (derived) |
mapreduce.map.java.opts / reduce | mapred-site or -D | JVM options, e.g. -Xmx | -Xmx300m | from ratio |
mapreduce.job.heap.memory-mb.ratio | mapred-site | Heap = ratio × container, if -Xmx not set | (not used) | 0.8 |
yarn.app.mapreduce.am.resource.mb | mapred-site | ApplicationMaster container | 512 | 1536 |
mapreduce.map.cpu.vcores | mapred-site or -D | vcores per map | 1 | 1 |
Default vs Dominant resource calculator
DefaultResourceCalculator
Compares memory only. Simple, and fine when all jobs have similar CPU needs. Our lab: 12 containers on 6 vcores, CPUs oversubscribed 2×.
memory onlyDominantResourceCalculator
Each request is judged by its largest share of the cluster. Mixes of CPU-heavy and memory-heavy jobs are shared fairly. Our lab: 6 containers, CPU full, memory half used.
memory + CPUFewer, bigger containers
Room for big heaps (joins, sorts, caches) but fewer tasks at once and more waste if tasks are small.
trade-offMore, smaller containers
More parallelism and better packing, but each JVM has fixed overhead and heap-hungry tasks fail.
trade-offAnimated example: sizing a telecom billing job
A telecom operator processes call detail records (CDRs) every night on 20 workers with 96 GB each for YARN. Follow one wrong guess and its fix.
- The developer asks for 3000 MB map containers with a 2400 MB heap.
- With a 1024 MB minimum allocation, YARN rounds 3000 up to 3072 MB.
- Each 96 GB node fits 32 such containers: 640 maps run at once on 20 nodes.
- The maps load a big lookup table and fail with “Java heap space”: the heap, not the container, is too small.
- The fix raises the container to 4096 MB and the heap to 80% of it.
- The job succeeds with 24 containers per node. Bigger containers mean fewer at once.
Size containers from the task's real needs, not from the node. Look at “Peak Map Physical memory” and GC time in the job counters (Lesson 8.1 showed about 290 MB for a 4.7 MB file), add headroom, and round to your minimum allocation.
Measuring containers, CPU and heap on our cluster
All of these use the Module 8 lab. Parts 1–3 use Hadoop's built-in sleep job (tasks that only sleep), so the only thing that changes is the request. The full script, tuning_lab.sh, is in “Run it on your PC”.
Ask for odd sizes and see what YARN grants
Every request is normalized up to a multiple ofyarn org.apache.hadoop.mapreduce.SleepJob -D mapreduce.map.memory.mb=300 -m 1 -mt 20000 -r 1 -rt 1000 curl -s http://resourcemanager:8088/ws/v1/cluster/apps/<app> # allocatedMB while it runsmap asks 300 MB -> app holds 1024 MB = AM 512 + map 512 MB map asks 384 MB -> app holds 1024 MB = AM 512 + map 512 MB map asks 600 MB -> app holds 1280 MB = AM 512 + map 768 MB map asks 1100 MB -> app holds 1792 MB = AM 512 + map 1280 MByarn.scheduler.minimum-allocation-mb(256 MB here). 300 and 384 both become 512. With the common production minimum of 1024 MB, all four would have become 1024, 1024, 1024 and 2048.Count how many maps fit at once
6144 MB ÷ 512 = 12 containers (1 AM + 11 maps). At 1024 MB: one node holds the 512 MB AM plus one map (1536 MB, 512 MB left unused), the other two hold 2 maps each: 6 containers, 5632 MB. 24 maps then need about 5 waves instead of 3.# 24 maps of 30 s each, first with 384 MB maps, then with 1024 MB maps yarn org.apache.hadoop.mapreduce.SleepJob -D mapreduce.map.memory.mb=384 -m 24 -mt 30000 -r 1 -rt 1000 yarn org.apache.hadoop.mapreduce.SleepJob -D mapreduce.map.memory.mb=1024 -m 24 -mt 30000 -r 1 -rt 1000maps of 384 MB: running containers 12 | memory 6144 of 6144 MB | vcores 12 of 12 maps of 1024 MB: running containers 6 | memory 5632 of 6144 MB | vcores 6 of 6Notice CPU being ignored
With the DefaultResourceCalculator only memory decides placement, so 12 one-vcore containers ran on 6 vcores. (The metric's total is allocated + available, which is why it shows “of 12”.) Lesson 11.1 saw the same thing per node: “3 vcores used” on a 2-vcore worker.12 containers × 1 vcore = 12 vcores 3 nodes × 2 vcores = 6 vcores offeredSwitch to the DominantResourceCalculator (and crash the RM first)
The apache/hadoop image writesCAPACITY-SCHEDULER.XML_yarn.scheduler.capacity.resource-calculator=org.apache.hadoop.yarn.util.resource.DominantResourceCalculatorCaused by: java.lang.IllegalStateException: Queue configuration missing child queue names for rootcapacity-scheduler.xmlfrom the env keys you give it, so the file contained only our one key and no queues: the ResourceManager refused to start. The fix repeatsroot.queues=defaultandroot.default.capacity=100in the same file.Run the same 24 maps under DRF
Now every container's dominant share is CPU (1 of 2 vcores is 50% of a node, 512 of 2048 MB only 25%). Each node takes 2 containers, half the memory stays free. Safer for CPU-heavy work, but half as many tasks at once here.bash rm.sh drf yarn org.apache.hadoop.mapreduce.SleepJob -D mapreduce.map.memory.mb=384 -m 24 -mt 30000 -r 1 -rt 1000calculator default: running containers 12 | memory 6144 of 6144 MB | vcores 12 of 12 calculator drf: running containers 6 | memory 3072 of 6144 MB | vcores 6 of 6Make a map run out of heap, then fix it
HeapHog keeps every line in a list (a common bug: collecting instead of streaming). A 32 MB split holds about 21 lakh lines, roughly 100 bytes each once they are Java objects. With 64 MB of heap only the small last split survived; each big split failed 4 times and the job was failed. 300 MB was enough: the container (512 MB) had room for it.hadoop jar mr11.jar HeapHog -D mapreduce.map.java.opts=-Xmx64m \ -D mapreduce.input.fileinputformat.split.maxsize=33554432 /user/hadoop/data/orders_x30.csv /out/hog64 hadoop jar mr11.jar HeapHog -D mapreduce.map.java.opts=-Xmx300m \ -D mapreduce.input.fileinputformat.split.maxsize=33554432 /user/hadoop/data/orders_x30.csv /out/hog300-Xmx64m: failed with state FAILED 7 Error: Java heap space Failed map tasks=10 Launched map tasks=15 kept 415,675 lines; heap used 54 MB of max 64 MB -Xmx300m: completed successfully Launched map tasks=5 kept 2,147,952 lines; heap used 222 MB of max 300 MB
Raising -Xmx without raising the container is a trap. In our lab the memory checks are off, so a too-big heap would simply eat into other containers' memory. On a production cluster with pmem-check or cgroups, the NodeManager kills the container (“is running beyond physical memory limits … Killing container”, exit code 143). We did not reproduce that message here.
Requests, rounding, packing and heap
Six steps from our lab: what the terminal shows, and what YARN and the JVM do.
- A 600 MB map request comes back as 768 MB.
- With 384 MB maps, 12 containers run and all 6144 MB are used.
- With 1024 MB maps, only 6 run and 512 MB is stranded.
- Under DRF the same 384 MB maps run 6 at a time: CPU is the limit.
- A map that keeps every line fails with a 64 MB heap.
- With a 300 MB heap the same job succeeds.
- The scheduler normalizes the 600 MB map to 768 MB and places it next to the 512 MB AM.
- Memory-only packing puts four 512 MB containers on each node, though each node has only 2 vcores.
- 1024 MB containers leave 512 MB unusable on the node that hosts the AM.
- The DominantResourceCalculator sees CPU as the bigger share and stops at 2 containers per node.
- Inside a map container the 64 MB heap fills up and the JVM throws OutOfMemoryError.
- A 300 MB heap holds the data and still leaves off-heap room inside the 512 MB container.
Tuning at work
1. Telecom: nightly billing on fixed-size containers
Billing jobs join call records with tariff tables. Platform teams set a standard map size (e.g. 4 GB, heap 80%) and let only a few approved jobs ask for more, so the cluster packs predictably.
2. Hospital analytics: CPU-heavy and memory-heavy jobs together
Image-feature extraction burns CPU; patient-record joins need memory. With the DominantResourceCalculator neither kind can starve the other by grabbing only one resource.
3. E-commerce sale day: watching GC before it hurts
Before a big sale, data teams replay a day of clickstream and check “GC time elapsed” and peak memory counters. Jobs spending more than about 10% of task time in GC get bigger heaps or smaller splits.
4. Spark on YARN: executor memory plus overhead
A Spark executor asks YARN for spark.executor.memory plus spark.executor.memoryOverhead (by default 10%, at least 384 MB), then rounded up. An 8 GB executor becomes about a 9 GB container: forget the overhead and the sizing math is off.
5. Banking reports: many tiny jobs
Hundreds of small reconciliation jobs each read a few MB. Uber mode, or moving them out of MapReduce entirely, avoids paying a container start-up per task.
Simplified; organisations differ in tools and policies.
A container sizing calculator
This reproduces our lab: round each request up to the minimum allocation, then pack containers onto nodes by memory only (DefaultResourceCalculator) or by memory and CPU (DominantResourceCalculator). The AM goes first, as in YARN.
import math
MIN_MB = 256
NODES = [{"mb": 2048, "vc": 2} for _ in range(3)]
def normalize(mb):
return math.ceil(mb / MIN_MB) * MIN_MB # always rounded UP
def pack(map_mb, n_maps, am_mb=512, use_cpu=False):
free = [dict(n) for n in NODES]
asks = [normalize(am_mb)] + [normalize(map_mb)] * n_maps
placed = 0
for ask in asks:
for n in free:
if n["mb"] >= ask and (not use_cpu or n["vc"] >= 1):
n["mb"] -= ask
n["vc"] -= 1
placed += 1
break
used = sum(2048 - n["mb"] for n in free)
return placed, used
for mb in [300, 384, 600, 1100]:
print(f"map asks {mb:4} MB -> granted {normalize(mb)} MB")
print("384 MB maps, memory only:", pack(384, 24))
print("1024 MB maps, memory only:", pack(1024, 24))
print("384 MB maps, DRF (CPU too):", pack(384, 24, use_cpu=True))return math.ceil(mb / MIN_MB) * MIN_MB # always rounded UPyarn.scheduler.minimum-allocation-mb is a step size: 300 and 384 both become 512.if n["mb"] >= ask and (not use_cpu or n["vc"] >= 1):The DefaultResourceCalculator only checks memory; the DominantResourceCalculator also needs a free vcore.print("384 MB maps, DRF (CPU too):", pack(384, 24, use_cpu=True))Lab: 6 containers and 3072 MB under DRF, against 12 containers and 6144 MB with memory only.
Run it on your PC: containers, CPU and heap
This uses the Module 8 lab (mr-lab folder). rm.sh (from Lesson 11.3) recreates only the ResourceManager with another configuration; HDFS and the NodeManagers keep running. Parts 1–3 use Hadoop's built-in sleep job from the MapReduce tests jar. Each part runs for a minute or two, so the whole script takes about 10 minutes.
Rounding, packing and the CPU calculator
# tuning_lab.sh: how YARN sizes containers, how many fit, and what happens when the heap is too small
# Run on your PC in the mr-lab folder (it switches the ResourceManager's calculator in part 3).
T=/opt/hadoop/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-3.5.0-tests.jar
sleepjob() { # extra args...
docker exec -d lab8-namenode-1 bash -c "export HADOOP_CLASSPATH=$T; yarn org.apache.hadoop.mapreduce.SleepJob -libjars $T $* -r 1 -rt 1000 > /tmp/tune.log 2>&1"
}
metrics() { docker exec lab8-namenode-1 curl -s http://resourcemanager:8088/ws/v1/cluster/metrics | python3 -c '
import sys, json; m = json.load(sys.stdin)["clusterMetrics"]
print(" running containers %2d | memory %4d of %d MB | vcores %d of %d" % (m["containersAllocated"], m["allocatedMB"], m["totalMB"], m["allocatedVirtualCores"], m["totalVirtualCores"]))'; }
wait_job() { until docker exec lab8-namenode-1 grep -qE "completed successfully|failed with state" /tmp/tune.log 2>/dev/null; do sleep 3; done; }
bash rm.sh default > /dev/null 2>&1
echo "== 1. a request is rounded UP to a multiple of the minimum allocation (256 MB here)"
for mb in 300 384 600 1100; do
sleepjob -D mapreduce.map.memory.mb=$mb -m 1 -mt 20000
sleep 14
app=$(docker exec lab8-namenode-1 grep -o "application_[0-9_]*" /tmp/tune.log | head -1)
docker exec lab8-namenode-1 curl -s http://resourcemanager:8088/ws/v1/cluster/apps/$app | python3 -c '
import sys, json; a = json.load(sys.stdin)["app"]
print("map asks %4s MB -> app holds %4d MB = AM 512 + map %d MB" % (sys.argv[1], a["allocatedMB"], a["allocatedMB"] - 512))' $mb
docker exec lab8-resourcemanager-1 yarn application -kill $app > /dev/null 2>&1; sleep 3
done
echo "== 2. how many maps run at once: 384 MB maps vs 1024 MB maps (24 maps of 30 s each)"
for mb in 384 1024; do
sleepjob -D mapreduce.map.memory.mb=$mb -m 24 -mt 30000
sleep 25; echo " maps of $mb MB:"; metrics
app=$(docker exec lab8-namenode-1 grep -o "application_[0-9_]*" /tmp/tune.log | head -1)
docker exec lab8-resourcemanager-1 yarn application -kill $app > /dev/null 2>&1; sleep 3
done
echo "== 3. CPU: the default calculator ignores vcores; DominantResourceCalculator counts them"
for calc in default drf; do
bash rm.sh $calc > /dev/null 2>&1
sleepjob -D mapreduce.map.memory.mb=384 -m 24 -mt 30000
sleep 25; echo " calculator $calc:"; metrics
app=$(docker exec lab8-namenode-1 grep -o "application_[0-9_]*" /tmp/tune.log | head -1)
docker exec lab8-resourcemanager-1 yarn application -kill $app > /dev/null 2>&1; sleep 3
done
bash rm.sh default > /dev/null 2>&1
# rm.sh (same folder) switches the ResourceManager configuration:
# # rm.sh fifo|capacity|capacity-preempt|capacity-ulf|capacity-ulf-preempt|fair|drf|default : recreate ONLY the ResourceManager with another scheduler
# # (run in the mr-lab folder; HDFS and the NodeManagers keep running)
# mode=$1
# envs="./config"
# case $mode in
# fifo) envs="$envs, ./sched-fifo.env" ;;
# capacity) envs="$envs, ./sched-capacity.env" ;;
# capacity-preempt) envs="$envs, ./sched-capacity.env, ./sched-capacity-preempt.env" ;;
# capacity-ulf) envs="$envs, ./sched-capacity.env, ./sched-capacity-ulf.env" ;;
# capacity-ulf-preempt) envs="$envs, ./sched-capacity.env, ./sched-capacity-ulf.env, ./sched-capacity-preempt.env" ;;
# fair) envs="$envs, ./sched-fair.env" ;;
# drf) envs="$envs, ./sched-drf.env" ;;
# default) ;;
# esac
# cat > rm-override.yaml <<Y
# services:
# resourcemanager:
# env_file: [$envs]
# volumes: [./fair-scheduler.xml:/opt/fair-scheduler.xml:ro]
# Y
# docker compose -p lab8 -f docker-compose.yaml -f rm-override.yaml up -d resourcemanager 2>&1 | tail -1
# until [ "$(docker exec lab8-resourcemanager-1 yarn node -list 2>/dev/null | grep -c RUNNING)" = 3 ]; do sleep 3; done
# docker exec lab8-resourcemanager-1 bash -c 'echo "scheduler: $(grep -o "scheduler\.[a-z]*\.[A-Za-z]*Scheduler" /opt/hadoop/etc/hadoop/yarn-site.xml | tail -1)"; mapred queue -list 2>/dev/null | grep -E "Queue Name|Scheduling Info" | paste - - | sed "s/Scheduling Info : //"'
# Run, in the mr-lab folder:
# bash tuning_lab.shReal output. The vcore metric adds allocated and available vcores, so with the memory-only calculator it reports “of 12” although the three nodes offer 6.
DominantResourceCalculator: the configuration that works
# Setting ANY capacity-scheduler key through env makes the image write a capacity-scheduler.xml
# with only those keys, so the queue list must be repeated here (without it the RM fails to start:
# "Queue configuration missing child queue names for root").
CAPACITY-SCHEDULER.XML_yarn.scheduler.capacity.root.queues=default
CAPACITY-SCHEDULER.XML_yarn.scheduler.capacity.root.default.capacity=100
CAPACITY-SCHEDULER.XML_yarn.scheduler.capacity.resource-calculator=org.apache.hadoop.yarn.util.resource.DominantResourceCalculator
# Our first version had only the last line. The ResourceManager did not start:
# docker logs lab8-resourcemanager-1 | grep "missing child queue"Real log line from the failed first attempt. Setting any CAPACITY-SCHEDULER.XML_ key makes the apache/hadoop image write capacity-scheduler.xml with only those keys, so the queue list must be repeated.
A map that keeps every line in memory
// HeapHog.java: a deliberately memory-hungry map-only job. Each map task keeps EVERY line it reads
// in a list (a common real-world bug: collecting instead of streaming), then writes how many it kept.
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
public class HeapHog {
public static class M extends Mapper<LongWritable, Text, NullWritable, Text> {
private final List<String> kept = new ArrayList<>();
@Override
protected void map(LongWritable k, Text line, Context ctx) { kept.add(line.toString()); }
@Override
protected void cleanup(Context ctx) throws IOException, InterruptedException {
Runtime r = Runtime.getRuntime();
String msg = String.format("kept %,d lines; heap used %d MB of max %d MB", kept.size(),
(r.totalMemory() - r.freeMemory()) >> 20, r.maxMemory() >> 20);
System.err.println(msg);
ctx.write(NullWritable.get(), new Text(msg));
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
String[] rest = new GenericOptionsParser(conf, args).getRemainingArgs();
Job job = Job.getInstance(conf, "heap hog");
job.setJarByClass(HeapHog.class);
job.setMapperClass(M.class);
job.setNumReduceTasks(0);
job.setOutputKeyClass(NullWritable.class);
job.setOutputValueClass(Text.class);
FileInputFormat.addInputPath(job, new Path(rest[0]));
FileOutputFormat.setOutputPath(job, new Path(rest[1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
// In the mr-lab folder:
// docker exec lab8-namenode-1 mkdir -p /tmp/mr11
// docker cp HeapHog.java lab8-namenode-1:/tmp/mr11/
// docker exec lab8-namenode-1 bash -c "cd /tmp/mr11 && mkdir -p c && javac -cp \$(hadoop classpath) -d c HeapHog.java && jar cf mr11.jar -C c ."
// Then run hog.sh below.Real output of hog.sh. With 64 MB of heap only the small last split (415,675 lines) survived; every 32 MB split failed 4 times and the job failed. The “7” counts distinct error lines the client printed, not attempts.
The heap experiment script
echo "== 4. a map that keeps every line in memory: heap 64 MB vs 300 MB (orders_x30.csv cut into 32 MB splits)"
for heap in 64 300; do
docker exec lab8-namenode-1 bash -c "cd /tmp/mr11; hdfs dfs -rm -r -f -skipTrash /out/hog$heap > /dev/null 2>&1
hadoop jar mr11.jar HeapHog -D mapreduce.map.java.opts=-Xmx${heap}m -D mapreduce.input.fileinputformat.split.maxsize=33554432 /user/hadoop/data/orders_x30.csv /out/hog$heap > /tmp/hog.log 2>&1
echo \" -Xmx${heap}m: \$(grep -oE 'completed successfully|failed with state [A-Z]+' /tmp/hog.log)\"
grep -oE 'Error: Java heap space|OutOfMemoryError: [A-Za-z ]+|GC overhead limit exceeded' /tmp/hog.log | sort | uniq -c | head -2
grep -E 'Failed map tasks|Launched map tasks' /tmp/hog.log | sed 's/^\s*/ /'
hdfs dfs -cat /out/hog$heap/part-m-* 2>/dev/null | sed 's/^/ /'"
done
# Run, in the mr-lab folder: bash hog.shReal output (same run as above). orders_x30.csv is the 140 MB file from Lesson 8.1.
Normalizing three requests
Watch each request being rounded up to the next multiple of the minimum allocation.
Worked examples: real problems, solved and traced in memory
Four sizing problems from different industries, each solved with the same memory-stack rules.
How many containers does a bank's worker give?
Problem statement
A bank's Hadoop worker has 128 GB RAM and 32 cores. Reserve 16 GB for the OS, the DataNode and the NodeManager. Map containers are 4 GB with 1 vcore. How many maps run at once per node, and on a 25-node cluster?
128 GB − 16 GB · 4 GB maps · 32 cores (keep 2)
by memory 28, by CPU 30 -> 28 per node, 700 on 25 nodes
Approach
- YARN memory = RAM − reserve.
- By memory: pool ÷ container size.
- By CPU: usable vcores. The smaller number wins (with DRF).
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
What does a 3000 MB request really cost?
Problem statement
A telecom job asks for 3000 MB maps on a cluster whose minimum allocation is 1024 MB and whose NodeManagers offer 96 GB. How much memory does each map really hold, and how much is wasted per node compared with asking for exactly 3072 MB?
ask 3000 · min 1024 · 96 GB pool
granted 3072 MB, 32 per node, 2304 MB per node granted but never asked for
Approach
- Round up to a multiple of 1024.
- Containers per node = pool ÷ granted.
- Waste = granted − asked, per container.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Sizing Spark executors on YARN
Problem statement
A sale-day Spark job uses executors with spark.executor.memory=8g. Overhead is 10% of that, at least 384 MB. The YARN minimum allocation is 1024 MB, and nodes offer 112 GB. What container does each executor get, and how many fit per node?
8 GB heap + max(384 MB, 10%) · step 1024 · 112 GB
overhead 819 MB -> container 9216 MB, 12 executors per node
Approach
- Overhead = max(384, 0.10 × 8192).
- Request = heap + overhead, rounded up to 1024.
- Executors per node = pool ÷ container.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Which resource limits a hospital cluster?
Problem statement
A hospital's 10-node cluster offers 64 GB and 16 vcores per node to YARN. Job A (image features) uses containers of 2 GB / 2 vcores; job B (record joins) uses 8 GB / 1 vcore. With the DominantResourceCalculator, what is each container's dominant share of one node, and how many of each fit on an empty node?
node 64 GB / 16 vcores · A 2 GB/2 · B 8 GB/1
A: memory 3.1%, CPU 12.5% -> dominant CPU, 8 per node B: memory 12.5%, CPU 6.2% -> dominant memory, 8 per node
Approach
- Share of memory and share of CPU per container.
- Dominant share = the larger of the two.
- Fit = min(memory fit, CPU fit).
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Check yourself
Quick revision
Lesson 11.4 in one screen
- Stack: physical RAM → reserve for OS/daemons →
yarn.nodemanager.resource.memory-mb→ containers → JVM heap (-Xmx≈ 80%) + off-heap. - Requests round UP to
yarn.scheduler.minimum-allocation-mb: lab 300/384 → 512, 600 → 768, 1100 → 1280 MB. - Packing: 384 MB maps → 12 containers (6144 MB); 1024 MB maps → 6 (5632 MB, 512 MB stranded).
- DefaultResourceCalculator = memory only (12 vcores handed out on 6); DominantResourceCalculator = memory + CPU (6 containers).
- “Java heap space” → raise -Xmx (and the container) or stop collecting data in memory; “beyond physical memory” → bigger container or smaller heap.
- Setting any CAPACITY-SCHEDULER.XML_ key in the apache/hadoop image rewrites the whole file: repeat the queues.
- Spark on YARN: container = executor memory + overhead (max 384 MB, 10%), rounded up.
Interview and viva questions
How do you calculate the number of containers per node in YARN?
Take the memory the NodeManager offers (yarn.nodemanager.resource.memory-mb) and divide by the container size after rounding it up to a multiple of yarn.scheduler.minimum-allocation-mb. If the DominantResourceCalculator is used, also divide the node's vcores by the vcores per container and take the smaller number. Remember the ApplicationMaster also occupies a container.
What is the difference between mapreduce.map.memory.mb and mapreduce.map.java.opts?
mapreduce.map.memory.mb is the size of the YARN container for a map task: what YARN reserves and enforces. mapreduce.map.java.opts sets JVM options such as -Xmx, the maximum heap inside that container. The heap must be smaller than the container, typically about 80% of it, to leave room for off-heap memory.
What is the DominantResourceCalculator in YARN?
A resource calculator for the Capacity Scheduler that considers both memory and CPU. Each request is compared on its dominant share, the larger of its memory and CPU fractions, following Dominant Resource Fairness. The default DefaultResourceCalculator compares memory only, so CPU can be oversubscribed.
Why does YARN give a container more memory than requested?
The scheduler normalizes every request up to a multiple of yarn.scheduler.minimum-allocation-mb (and a similar increment for the Fair Scheduler). In our lab, with a 256 MB minimum, a 384 MB request became 512 MB and 600 MB became 768 MB.
How do you fix “Container is running beyond physical memory limits”?
The task process (heap plus off-heap) used more memory than its container. Either increase the container size (mapreduce.map.memory.mb or mapreduce.reduce.memory.mb) while keeping the heap around 80% of it, or reduce the heap or off-heap use (smaller buffers, less data held in memory). For Spark, raise spark.executor.memoryOverhead.
Practice problems
Sizing and tuning exercises using the real numbers from our lab.
Round it
Round 384, 513 and 1025 MB up to multiples of 256 MB.
three requests
[512, 768, 1280]
Show solution and explanation
import math
print([math.ceil(x / 256) * 256 for x in (384, 513, 1025)])How it works: [512, 768, 1280]: one MB over a boundary costs a whole extra step.
Lab cluster total
Three NodeManagers offer 2048 MB each. How many 512 MB containers fit, and how many maps run once the 512 MB AM is placed?
3 × 2048 MB
12 containers, 11 maps at once
Show solution and explanation
total = 3 * 2048 // 512
print(total, "containers,", total - 1, "maps at once")How it works: 12 containers, 11 maps: exactly what the lab measured (running containers 12).
Heap ratio
Our maps asked for 384 MB with -Xmx300m but were granted 512 MB. What fraction of the request and of the grant is the heap?
300 / 384 · 300 / 512
78% of the request, 59% of the grant
Show solution and explanation
print(f"{300 / 384:.0%} of the request, {300 / 512:.0%} of the grant")How it works: 78% of what we asked, 59% of what we got: rounding left extra off-heap room.
Stranded memory
Each node has 2048 MB. With a 512 MB AM on one node and 1024 MB maps, how much memory can no map use?
AM 512 · maps 1024
512 MB stranded
Show solution and explanation
node = 2048
left_on_am_node = node - 512 - (node - 512) // 1024 * 1024
print(left_on_am_node, "MB stranded")How it works: 512 MB, which is why the lab used 5632 of 6144 MB.
Pick a container size
A worker offers 96 GB to YARN. Compare 2, 3, 4 and 6 GB maps: maps per node and leftover memory.
96 GB pool
2 GB maps: 48 per node, 0 MB left 3 GB maps: 32 per node, 0 MB left 4 GB maps: 24 per node, 0 MB left 6 GB maps: 16 per node, 0 MB left
Show solution and explanation
pool = 96 * 1024
for gb in (2, 3, 4, 6):
c = gb * 1024
print(f"{gb} GB maps: {pool // c:2d} per node, {pool % c} MB left")How it works: All divide evenly here; on a 100 GB pool 3 GB would strand 1 GB. Choose sizes that divide the pool.
Heap for a lookup table
A map loads a lookup of 12 lakh entries at about 250 bytes each, plus 200 MB for the framework and buffers. What heap and what container (heap = 80%, step 1024 MB)?
12 lakh × 250 B + 200 MB
heap 486 MB -> container 1024 MB
Show solution and explanation
import math
heap = 1_200_000 * 250 / 1024**2 + 200
container = math.ceil(heap / 0.8 / 1024) * 1024
print(f"heap {heap:.0f} MB -> container {container} MB")How it works: About 486 MB of heap, so a 1024 MB container. Measure first; object overhead often doubles estimates.
Bigger containers or more nodes?
A nightly job fails with heap errors at 2 GB maps. Option A: 4 GB maps (half as many at once, job 3 h instead of 2 h). Option B: add 10 nodes at ₹40,000 each per month to keep 2 h. Which is cheaper for a job that has a 4-hour window?
3 h vs 2 h · 10 × ₹40,000
A fits the window: True | B costs Rs 400000 per month
Show solution and explanation
window_h, a_hours = 4, 3
print("A fits the window:", a_hours <= window_h, "| B costs Rs", 10 * 40000, "per month")How it works: A fits the 4-hour window at no extra cost (illustrative numbers); B only makes sense if the window shrinks or other jobs need the capacity.
DRF on our lab
Simulate DRF packing on our 3 nodes (2048 MB, 2 vcores) for an AM of 512 MB/1 vcore and 24 maps of 384 MB/1 vcore (rounded to 512). How many containers and how much memory are used?
3 nodes · 1 AM + 24 maps
6 containers, 3072 MB
Show solution and explanation
nodes = [[2048, 2] for _ in range(3)]
placed = used = 0
for mb in [512] + [512] * 24:
for n in nodes:
if n[0] >= mb and n[1] >= 1:
n[0] -= mb; n[1] -= 1; placed += 1; used += mb
break
print(placed, "containers,", used, "MB")How it works: 6 containers, 3072 MB: exactly the lab's “running containers 6 | memory 3072 of 6144 MB”.
Real-world scenario questions (asked in company interviews)
Memory and CPU sizing questions come up in every Hadoop and Spark platform interview, usually with a failure story attached.
“What is the difference between container memory and heap memory?”
A standard opening question for YARN.
The interviewer is checking: the two limits and how they relate.
Show a strong answer
- Container memory (mapreduce.map.memory.mb) is what YARN reserves on a node and can enforce.
- Heap (-Xmx in mapreduce.map.java.opts) is the JVM\'s object space inside that container, about 80% of it.
- Heap too small → OutOfMemoryError; heap plus off-heap too big → the NodeManager kills the container.
Jobs fail with “Java heap space” after a data volume increase
Nothing changed in the code; input doubled after a new region was added.
The interviewer is checking: diagnosis before throwing memory at it.
Show a strong answer
- Find which tasks fail (map or reduce) and why: a reducer with a hot key, or code that collects values into a list.
- Fix the code if it buffers data; reduce split size or add reducers to spread load.
- If memory is really needed, raise the container and the heap together, keeping the 80% ratio.
Size YARN for a new 40-node cluster of 256 GB / 48-core machines
Each node also runs a DataNode and an HBase RegionServer with a 32 GB heap.
The interviewer is checking: reserves, steps and CPU.
Show a strong answer
- Reserve OS (~8 GB), DataNode (~4 GB), NodeManager (~2 GB) and RegionServer (32 GB + overhead): about 200 GB for YARN.
- Pick a minimum allocation (e.g. 2 GB) and standard container sizes that divide the pool; keep 2–4 cores for daemons.
- Use the DominantResourceCalculator if CPU-heavy jobs share the cluster; enable cgroups for isolation.
“The ResourceManager UI shows more vcores used than the cluster has. Is it broken?”
A junior admin sees 12 vcores in use on a 6-vcore cluster.
The interviewer is checking: resource calculators.
Show a strong answer
- Not broken: the Capacity Scheduler\'s DefaultResourceCalculator places containers by memory only, so vcores are recorded but not limited.
- Switch to the DominantResourceCalculator so CPU is respected; in our lab that cut 12 containers to 6.
- For real CPU isolation, enable the LinuxContainerExecutor with cgroups.