Unit 4 · 3 hrs · CSUE301 Big Data Analytics
In-Memory
Data Processing
Three separate reports, all starting from the same cleaned-up order log. Chapter 3's DAG model means each one can be expressed as its own chain of transformations — but nothing stops all three chains from re-reading and re-cleaning the exact same rows from scratch. This chapter asks: what if the cleaned rows just... stayed where they were?
One filter, three different reports
Same starting point, three destinations
Does "clean the data" have to happen three times?
Revenue by hub, orders by restaurant, average delivery time — all three reports need the same veracity-filtered 15 rows first. Section 4.2 asks what happens if that shared step runs once instead of three times.
Not durable. Not free. Still worth it.
A cache is a bet, not a guarantee
What's the difference from Chapter 2's replication?
A replicated HDFS block survives a node failure outright. A cached result, held only in memory, is simply gone if its machine dies — Section 4.3 works out exactly what happens next.
Three reads instead of one, at any scale
The saving is a multiplier, not a constant
More reports on the same base data means more savings, not less.
At sixteen rows the redundant reads are nearly free. At citywide volume, skipping two full re-reads of 46,080 rows is the entire difference between a report that runs once and one that runs three times as long for no new information.
The Same Bottleneck, Named Precisely
Section 3.5 showed chained MapReduce jobs paying a disk write-then-read between every step. How expensive is that hop, really — and what changes if it never touches disk at all?
Reading a megabyte sequentially from memory takes roughly 250 microseconds. Reading the same megabyte from a spinning disk takes roughly 20 milliseconds — about 80× slower. This isn't a Spark-specific number; it's one of the oldest, most-cited facts in systems engineering (traced to Jeff Dean and Peter Norvig's latency table), and it's the entire reason "keep it in memory" is a strategy at all rather than a marketing phrase.
Worked Example · Four Supersteps, Two Ways
Section 1.3's BSP shortest-path computation (Kitchen → Hub N/S/E → Zone Z) took 4 supersteps to converge. Between each pair of consecutive supersteps, the current best-known distances have to be handed forward — 3 handoffs in total for a 4-superstep job.
Chained-job style (disk between steps)
Each superstep runs as its own job; the updated distances get written to HDFS and the next superstep reads them back in — Section 3.5's "HDFS write + read" box, three times over. 3 disk round trips, each paying roughly the 20ms-per-megabyte tax (scaled down for our tiny distance table, but the same ratio applies).
In-memory style (Spark)
The distance table stays in memory across all 4 supersteps as one RDD, updated in place logically (a new RDD each superstep, lineage-linked to the last). 0 disk round trips for the handoffs themselves — each one costs roughly the 250µs-per-megabyte memory rate instead, about 80× less per hop.
Four supersteps barely shows this — but a real iterative algorithm (PageRank over millions of edges, gradient descent over thousands of iterations) multiplies "3 handoffs" into "thousands of handoffs," and every one saved is another 80× avoided. Chapter 6's machine learning algorithms are exactly this kind of workload, which is precisely why they run on Spark rather than chained MapReduce in practice.
"in-memory" doesn't mean "never touches disk, ever"
The original data still has to be read from HDFS at least once (Section 2.1's durable, replicated blocks) — nothing about in-memory processing changes where data is stored at rest. What changes is what happens between processing steps, once the data is already loaded: it can be handed from one stage to the next as an object reference in RAM, instead of being written out and read back in.
pitfalls
- "Spark is just MapReduce, but faster hardware." Same cluster, same disks, same network — the speedup in the worked example above comes entirely from avoiding disk round trips the chained-job model forced, not from anything running on faster machines.
- "In-memory means the data never existed on disk." The source data is still durably stored via Chapter 2's replication. In-memory processing is about intermediate results between processing steps, not about the original dataset's resting state.
- "80× applies to every operation, always." It's the memory-vs-disk ratio for sequential reads of comparable size. Actual speedup for a real job depends on how many disk round trips were avoidable and how big each one was — a job with one cheap step has little to gain no matter how the ratio looks on a chart.
Practice
-
If Ch1.3's BSP job needed 4,000 supersteps instead of 4 (a much larger graph), how many disk round trips does the chained-job model pay, and how does that compare to the in-memory model?
Solution
3,999 disk round trips (one between each pair of consecutive supersteps) versus 0 for the in-memory model — the same ratio as the 4-superstep case, just with a thousand times more handoffs, meaning a thousand times more accumulated disk-round-trip cost for the chained-job approach specifically.
-
A job reads its input from HDFS exactly once, does one map and one reduce, and writes its output once. Does the memory-vs-disk distinction from this section meaningfully change its runtime?
Solution
Barely — there's no intermediate handoff to avoid. This section's savings come specifically from avoiding repeated disk round trips between chained steps; a single-pass job was never paying that cost more than once, so there's little for in-memory processing to eliminate.
-
Using the 250µs-vs-20ms figures, roughly how many memory-speed handoffs could happen in the time ONE disk-speed round trip takes?
Solution
Roughly 80 — 20ms ÷ 250µs = 80. In the time a single chained-job disk hop completes, an in-memory pipeline could have already finished roughly 80 equivalent handoffs.
-
Section 3.6 noted that Spark's lineage-based recomputation can cost MORE than a MapReduce task retry if a long chain has to be replayed uncached. Does that observation weaken this section's case for in-memory processing?
Solution
Not really — it's a distinct trade-off. This section is about the cost of the HAPPY PATH (no failures) for repeated handoffs; Section 3.6 is about the cost of the UNHAPPY PATH (a failure occurs) for recomputation. In-memory processing can be strictly faster when everything succeeds and still have a recovery cost worth planning for when something doesn't — both things are true at once, which is exactly why Section 4.3 treats caching as a bet rather than a free win.
Compute It Once, Read It Three Times
Revenue by hub. Orders by restaurant. Average delivery time by hub. Three reports, three separate action calls — and, by default, three separate re-runs of the exact same veracity filter.
Section 3.5 was precise about laziness: nothing runs until an action is called. What it didn't dwell on is what happens when multiple actions are called on the same chain of transformations. Without intervention, Spark reruns the whole chain from source, every single time — caching is the intervention.
Worked Example · Three Reports, With and Without
The shared filter step, run once or three times
15 veracity-filtered rows · 3 downstream reportsSee Both DAGs
RDD default vs DataFrame default, and why they differ
An RDD's default (.cache() → MEMORY_ONLY) is the more aggressive choice: fast, but a partition that doesn't fit is simply dropped and recomputed later, never touching disk. A DataFrame's default (.cache() → MEMORY_AND_DISK) is more conservative: Spark's own structured APIs are typically used on larger, less predictable data volumes, where silently falling back to disk instead of dropping data outright is the safer default behaviour.
pitfalls
- "Calling .cache() caches the data immediately." It's still lazy — .cache() only marks the RDD/DataFrame. The actual computation and storage happen on the next action, exactly like any other transformation.
- "Caching helps every job." It only pays off when the SAME RDD/DataFrame is used by more than one action, or more than one downstream branch. Caching something used exactly once adds the cost of storing it for no reuse benefit at all.
- "Cache and persist are different features." Cache is just persist called with the type's default storage level baked in — not a separate mechanism, just a shorter spelling of the common case.
Practice
-
A fourth report is added, also built on the same veracity-filtered 15 rows. With caching already in place from the worked example, how many additional disk reads does this fourth report need?
Solution
Zero — it reads from the same cached result Reports 2 and 3 used. Caching's benefit scales with the number of consumers of the cached data; a fourth consumer is free, just like the second and third were.
-
If only Report 1 ever runs, and Reports 2 and 3 are hypothetical future additions that haven't been written yet, was caching before Report 1 worth it?
Solution
No — with only one consumer, caching added the cost of storing the 15 rows with zero reuse to offset it. Caching should be added when a shared computation actually has multiple consumers, not speculatively "just in case" one shows up.
-
An RDD is cached with the default level, and its data doesn't fit entirely in the available memory. What happens to the partitions that don't fit, and what happens the next time they're needed?
Solution
Under MEMORY_ONLY (the RDD default), partitions that don't fit are simply not cached at all — not an error, just silently absent. The next time they're needed, Spark recomputes them from lineage (Section 3.6), exactly as if they'd never been marked for caching in the first place.
-
Section 4.1 showed avoiding disk round trips between BSP supersteps. Is caching the SAME mechanism as keeping an RDD "in memory across supersteps," or a different one?
Solution
Same underlying mechanism, different trigger. Section 4.1's supersteps pass results forward automatically as part of one connected computation. Caching is the same "keep it in memory" idea applied deliberately, by the programmer, to a specific RDD that will be reused by multiple separate later actions rather than passed forward once within a single DAG.
The Cache Is a Bet, Not a Promise
CityCourier's cluster can hold two cached results comfortably. It's about to be asked to hold three.
Memory is finite in a way disk usually isn't. When a new cached result needs room and none is free, Spark evicts something — using LRU (Least Recently Used): whichever cached block hasn't been touched in the longest time goes first, on the theory that the least recently used data is the least likely to be needed next.
Worked Example · Three Results, Two Slots
Trace the cache through five operations
memory holds 2 results at a timeStep Through the Cache
Watch LRU decide what stays
2 memory slots · 3 competing resultsMEMORY_ONLY vs MEMORY_AND_DISK, revisited
The trace above assumed MEMORY_ONLY: B is evicted and simply gone, forcing a full recompute from source. Under MEMORY_AND_DISK (Section 4.2's DataFrame default), step 4 would instead spill B to local disk rather than dropping it. Step 5 would then read B back from disk — Section 4.1's 20ms-per-megabyte rate, not a full recompute, and not the 250µs memory rate either. Spilling is a middle option: slower than staying in memory, faster than starting over.
pitfalls
- "Eviction is an error condition." It's routine memory management, the same way an operating system evicts pages. Nothing crashes; the evicted result is simply not there next time, and gets rebuilt on demand.
- "LRU always makes the objectively right choice." LRU bets that recent access predicts near-future access. If B were about to be needed five more times and A never again, evicting B (as recency alone dictated in the worked example) is the wrong call in hindsight — LRU has no way to know that in advance.
- "Caching more things is always safer than caching fewer." More cached results competing for the same fixed memory means more eviction pressure and more surprise recomputes elsewhere — over-caching can make the specific result you most need get evicted sooner, not later.
Practice
-
Redo the worked example, but skip step 3 (no re-access of A before caching C). Which result gets evicted now?
Solution
A — without the step-3 refresh, A and B were cached in that order (A first), making A the least recently used when C needs room. Whichever of the two was touched longest ago is what LRU removes; the refresh in the original trace is precisely what changed the outcome.
-
With 3 memory slots instead of 2, does caching A, B, and C in sequence cause any eviction at all?
Solution
No — three results fit in three slots with room to spare. Eviction only happens when a new cached result needs space and none is free; enough capacity for every live cached result removes the pressure entirely.
-
Under MEMORY_AND_DISK instead of MEMORY_ONLY, does step 4 of the worked example still evict something, or does the eviction itself go away?
Solution
Eviction from the memory tier still happens — there are still only 2 memory slots. What changes is the CONSEQUENCE: B spills to disk rather than disappearing, so step 5's "cache miss" becomes a disk read instead of a full recompute from source.
-
Section 2.1 discussed the NameNode's re-replication after a DataNode failure. Is an evicted cache entry's "recomputation via lineage" the same kind of repair, or a different kind?
Solution
Different kind, same underlying idea of "restore what's missing on demand." Section 2.1's re-replication restores a DURABLE guarantee (a replicated block) after an actual failure, coordinated by the NameNode without being asked. Cache recomputation restores a PERFORMANCE optimization (a cached result) after a routine, expected memory-management decision, triggered only when something later asks for the missing result — nothing proactively "repairs" an evicted cache entry the way the NameNode proactively repairs a lost block.
Let the Engine Read the Query First
"Give me Hub E's orders" reads completely differently depending on whether Spark sees it as raw code to run in order, or as a query it's allowed to rewrite before running.
An RDD is a set of instructions Spark executes essentially as written. A DataFrame (or a Dataset, or a SQL query) is a step higher: a structured, schema-aware description of what you want, which Spark's Catalyst optimizer is free to rewrite into a more efficient how before anything runs.
Worked Example · Filtering to Hub E, Two Ways
Recall Section 2.6: if CityCourierDB partitions its storage by hub, Hub E's 5 orders (#4, #7, #10, #13, #16) live entirely in one shard, separate from Hub N's and Hub S's 11.
RDD: .filter(hub=="Hub E")
Spark has no visibility into what the filter function does until it runs. It reads every partition — all 16 rows, all 3 shards touched — into memory, then applies the filter row by row, keeping 5 and discarding 11.
DataFrame: .filter(df.hub=="Hub E")
Catalyst sees a structured predicate on a known column, recognises the storage is partitioned by that exact column, and pushes the filter down to the storage layer itself. Only Hub E's shard is ever read: 5 rows, 1 shard touched, the other 2 skipped entirely.
Both return the identical 5 rows. The RDD version paid for reading 16; the DataFrame version paid for reading 5 — the exact "1 node instead of 3" saving Section 2.6 measured for hub-partitioned selective queries, now happening automatically because Catalyst could see and rewrite the query, instead of manually because a programmer chose the right partitioning-aware code path by hand.
this only works because Catalyst can see the plan
Predicate pushdown requires Catalyst to know, structurally, what the filter condition is and which column the storage is partitioned by — both visible in a DataFrame's declarative .filter(df.hub=="Hub E"). Wrap the same logic in an opaque user-defined function, or drop to raw RDD code, and Catalyst has nothing to analyse: it's just a black box it must run over every row, same as the RDD case above.
pitfalls
- "DataFrames are just RDDs with a different name." They're built on top of RDDs internally, but the schema and structured operations are what let Catalyst analyse and rewrite the plan. An RDD's arbitrary Python or Scala functions give the optimizer nothing to look inside.
- "Catalyst makes RDDs faster too." Catalyst optimizes DataFrame/Dataset/SQL query plans specifically. Code written directly against the RDD API bypasses it entirely, by design, regardless of how the underlying cluster is configured.
- "Predicate pushdown works on any filter, on any storage." It requires the storage layer to actually support skipping data by that condition — partitioned storage (Section 2.6) or a columnar format with stored statistics (Parquet's per-block min/max values, say). A filter on an un-partitioned, unstatisticked plain-text file has nothing for Catalyst to push down to.
Practice
-
If CityCourierDB instead partitioned by hash-of-order-id (Section 2.3) rather than by hub, would the DataFrame filter in the worked example still skip 2 of 3 shards?
Solution
No — Section 2.3 and 2.6 already established that hash-of-order-id partitioning scatters each hub's rows across multiple shards with no relationship to the hub field. Catalyst can only push a predicate down to skip partitions when the partitioning scheme actually aligns with the filtered column; under hash partitioning, all 3 shards would still need reading regardless of DataFrame vs RDD.
-
A DataFrame query filters on a column wrapped inside a custom user-defined function:
.filter(myCustomCheck(df.hub)). Does Catalyst's pushdown still apply?Solution
Generally no — a UDF is opaque to Catalyst, just like an RDD's arbitrary function. The optimizer can't see inside
myCustomCheckto know it's equivalent to a simple equality check on a partitioned column, so it falls back to reading everything and filtering in Spark, exactly the RDD-style cost. -
Combine this with Section 4.2: does caching a DataFrame after Catalyst has already applied predicate pushdown still make sense for repeated queries?
Solution
Yes, and the two stack: pushdown reduces what gets read on the first pass (5 rows instead of 16); caching that already-filtered 5-row result means a second identical query reads 0 rows from anywhere, not even the pushdown-optimised 5. They solve different problems — one shrinks a single read, the other eliminates repeat reads entirely.
-
A team writes their whole pipeline in raw RDDs "for full control," on hub-partitioned storage, running the same Hub E filter thousands of times a day. What's the accumulated cost of skipping DataFrames, in this section's terms?
Solution
Every one of those thousands of runs reads all 16 rows (all 3 shards) instead of the 5 a DataFrame query would read via automatic pushdown — the "full control" bypassed the one optimization (Catalyst) that would have exploited the partitioning scheme's own design without requiring any manual partition-aware code at all. The RDD API isn't wrong to use, but it trades this specific automatic optimization away.
Chapter 4, One Page
4.1 · Why Memory Matters
Memory read ~250µs/MB · disk read ~20ms/MB · roughly 80× apart
Savings come from avoiding REPEATED disk round trips between chained steps
Source data still lives durably on disk (Ch2) — only handoffs change
4.2 · Caching
.cache() = .persist(default level) · RDD default MEMORY_ONLY, DataFrame default MEMORY_AND_DISK
Both lazy — nothing stored until the first action
Only pays off with 2+ consumers of the same computation
4.3 · Eviction & Spill
Memory full → LRU evicts the least-recently-touched cached block
MEMORY_ONLY: evicted = gone, recompute via lineage. MEMORY_AND_DISK: evicted = spilled, re-read from disk
Eviction is routine, not an error
4.4 · DataFrames & Catalyst
RDD = run as written · DataFrame/SQL = optimizable plan
Predicate pushdown skips whole partitions when storage layout matches the filter
UDFs and raw RDD code are opaque to Catalyst — no pushdown
Mixed Review
Eight questions, deliberately out of section order.
- An RDD is cached but only ever consumed by one action, once. Was caching it worth the memory it used?
Solution
No — caching pays off across multiple consumers of the same computation. A single consumer gets no reuse benefit, only the cost of storing the result.
- What's the default storage level behind a plain
.cache()call on an RDD, and what happens to a partition that doesn't fit?Solution
MEMORY_ONLY. A partition that doesn't fit is simply not cached — not an error — and gets recomputed from lineage the next time it's needed.
- A cached DataFrame's partition gets evicted under MEMORY_AND_DISK. Where does Spark look for it next time, before falling back to full recomputation?
Solution
Local disk first — MEMORY_AND_DISK spills evicted blocks to disk rather than dropping them outright, so the next access reads from disk (Section 4.1's slower but far cheaper-than-recompute rate) instead of rebuilding from source.
- Why doesn't Catalyst's predicate pushdown help a query filtering on a column wrapped in a user-defined function?
Solution
Because a UDF is opaque to the optimizer — Catalyst can't inspect what it does, so it can't prove the filter is safe to push down to storage. It falls back to reading everything and filtering afterward, same as plain RDD code.
- Section 4.1's memory-vs-disk ratio is about 80×. Does that mean an in-memory Spark job is always 80× faster than an equivalent chained-MapReduce job?
Solution
No — 80× is the ratio for one hop of comparable size. Real speedup depends on how many disk round trips were actually avoidable and how large each was; a job with few or no chained steps has little disk-hop cost to remove in the first place.
- LRU evicts block B to make room for block C. Two minutes later, B is requested again and C is not. Did LRU make the right call, looking back?
Solution
No, in hindsight — but LRU only has past access patterns to go on, not future ones. It bet that recency predicts near-future use, and this particular case went against that bet, which can always happen with any recency-based heuristic.
- Is reading the original order log from Chapter 2's replicated HDFS blocks affected by anything in this chapter?
Solution
No — this chapter is entirely about what happens to INTERMEDIATE results after the first read: caching them, evicting them, or letting Catalyst skip reading irrelevant parts of them sooner. The durable, replicated source data and its storage mechanics are untouched, exactly Section 4.1's "still reads from disk at least once" note.
- A team caches a DataFrame, then writes the rest of their pipeline using only raw RDD transformations on top of it. Do they still get Catalyst's pushdown benefits for those later RDD steps?
Solution
No — once the pipeline drops to the RDD API, Catalyst is no longer in the picture for those steps, regardless of what happened before. Caching and Catalyst optimization are separate mechanisms; using one doesn't guarantee the other applies to code that bypasses the API it depends on.
If You Want the Long Version
- Spark: The Definitive Guide — Chambers & Zaharia. The authoritative treatment of caching, storage levels, and the Catalyst optimizer's rewrite rules behind every section of this chapter.
- Designing Data-Intensive Applications — Martin Kleppmann. Its chapters on batch and stream processing frame why in-memory intermediate state matters beyond Spark specifically, useful context before Chapter 5's streaming material.
- Hadoop: The Definitive Guide — Tom White. Read alongside this chapter for the contrast: MapReduce's original disk-backed-everything design is the baseline this chapter's savings are measured against.
Chapter 4 of 7 · CSUE301 Big Data Analytics · builds on Chapter 3's DAG model (Section 3.5) and lineage-based fault tolerance (Section 3.6).
Next → Chapter 5, Streaming Analytics and Real-Time Data Processing.