Take a Break
5:00
Inhale…
Give your mind a break — no phone, no music, just idle time or a quick walk.
Streaming Data Architecture and Spark Streaming
9.1 Streaming Data Architecture: The Four Layers
Why do we need four separate layers in a streaming system? Think about a single application that must collect data from millions of devices, analyze it as it arrives, save the results, and show them on a live dashboard. No single program can do all four jobs well — each job has a different speed, different storage need, and different failure mode. Splitting the work into layers lets each layer be built, scaled, and replaced independently.
9.1.1 Components of a Streaming Architecture
A streaming data architecture ends up with the following components:
- The collection layer collects the data from the sources — the web servers, sensors, and machines that generate events.
- The data flow layer sits between the collection layer and your processing. It is an intermediate layer, and as we will see, its job is to absorb the differences in speed between the fast data sources and the slower processing stage.
- The storage layer holds the results after processing. You may keep them in memory (main memory, for fast access), persist them on a hard drive for later use, or hand them to the delivery layer.
- The delivery layer passes the results onward — to downstream layers, or to the end user who is looking for the results or the analysis.
A useful mental picture: collection is the "ears" of the system, data flow is the "throat" that regulates how much can pass, processing is the "brain", storage is the "memory", and delivery is the "voice" that reports back. Each of these four roles can be scaled out — more machines, more capacity — without redesigning the others.
9.1.2 The High-Level Flow and the Feedback Loop
Here is the high-level generalized architecture, in the order data moves through it:
- The collection tier collects data from web browsers, sensors, and automated machines — whatever the sources are. That is its only job: collect the data.
- Data from the collection tier is forwarded to the analysis tier, where it will be processed. It has to go through the data flow layer on the way.
- After the analysis, you store the data in memory — memory means main memory — or you persist it in persistent storage.
- From storage, you push the data to the delivery tier.
- Finally, the results are displayed on web interfaces, mobile interfaces, and the dashboards on which we are tracking metrics.
One more thing may happen: from the delivery tier you send something back to the analysis tier. Why would you do that? Because it may be an iterative application — an application that refines its answer over several rounds. Some results feed back into the analysis tier, the analysis tier runs again, returns to memory, and the loop continues until the computation converges on a final answer. Clustering is the classic example: assign points to clusters, update the cluster centers, reassign, and repeat until the centers stop moving. Without this feedback loop, such iterative algorithms could never run on streaming data.
Visual intuition: Picture the architecture as a horizontal flow diagram. On the left, draw a fan of source arrows (web browsers, sensors, machines) entering a box labelled Collection. A single thick arrow leaves Collection and enters the Data Flow box — a narrow funnel shape, because the funnel's job is to smooth the flow. From the funnel, an arrow enters the Analysis box (a cluster of worker circles). Analysis connects down to a Memory/Storage box (a cylinder, the database symbol). From storage, an arrow rises to the Delivery box, which fans out again into dashboards, mobile screens, and alerts. Finally, a curved arrow returns from Delivery back to Analysis, closing the feedback loop. The one-sentence takeaway: data flows left to right in one direction, with a single feedback arc reserved for iterative algorithms.
9.1.3 The Collection Layer
The collection tier collects the data from your sources. Those sources could be web browsers, automated machines, or sensors. How does the collection happen? The communication between the collection tier and the sources runs over a TCP/IP network using HTTP. Sensors can post data to an HTTP service periodically — each post carries the latest readings.
The original use case for this tier was website log analysis. The idea: build a strong log analysis system where you want to detect certain errors or deviations in what users are doing. The initial format in which logs were collected was the W3C standard log data format — a fixed, text-based format for web server logs. Nowadays newer formats are available, and the collection tier supports them too: JSON, Avro, and Thrift are all common today. Avro and Thrift are interesting because they are compact binary formats with a schema — the format tells you what each field means, which makes later processing much easier.
Collection happens at specialized servers called edge servers — machines placed close to the data sources. The collection process is usually application-specific: there are specific adapters that talk to your data sensors or to the source machines where the data is coming from. One adapter might speak the protocol of a temperature sensor; another might poll a database; a third might tail a web server's log file. The edge server runs the right adapter for each source.
Real-world: It is quite common today to have an IoT system with edge processing: the edge server collects data from IoT devices and pushes the data deeper into the back end, into the cloud system. The collection tier also integrates with modern data flow systems directly. Older servers may or may not integrate directly with those data flow systems — for them, the edge server remains the bridge.
9.1.4 The Data Flow Layer
The data flow tier is a separation between the collection tier and the processing layer. Why is this required? Because the two systems may work at different rates: the source may generate data very fast, and the processing tier may not be able to process at that speed.
The professor's analogy — impedance mismatch. The term comes from electrical engineering: two circuits have matched impedance when they can pass energy between them efficiently; if their impedances differ, energy bounces back or is lost. Here, the "impedance" of each tier is its data rate. The source layer keeps sending data at its own rate; the processing tier keeps retrieving and processing at its own rate; and the intermediary maintains a buffer of the data between them. If there is an impedance mismatch — a difference between the speeds of the two ends — the buffering brings the two tiers to the same impedance, and no data is lost. Where the analogy breaks: an electrical circuit cannot store energy indefinitely, but a message buffer can hold data for seconds or minutes until the consumer catches up.
So the responsibilities of the intermediate layer are:
- Accept the messages and events from the collection layer.
- Provide those messages and events to the processing layer.
- Act as a real-time interface to both sides — to the producer (the data source) and to the consumer (your processing tier).
- Guarantee at-least-once semantics: we do not want to lose any data, so every event must reach processing at least once.
Why is the buffer needed even when nothing is broken? Because of spikes. Data sources at times spike — if you are analyzing real-time streaming data on an e-commerce website, big sale days cause the event rate to explode. Without a buffer, you would lose data during those spikes: you want real-time streaming, but your processing tier is not fast enough to cope with the burst. So you put an intermediary layer that holds the messages and guarantees at-least-once semantics.
A note on delivery semantics — there are several you can read about:
- At-least-once: every event is delivered one or more times; nothing is lost, but duplicates are possible.
- At-most-once: every event is delivered at most once; nothing is duplicated, but events can be dropped.
- Exactly-once: every event is delivered exactly once — neither lost nor duplicated. This is the hardest to guarantee and the most expensive.
Spark supports configurable delivery semantics; you will see receivers and acknowledgements again in Section 9.7, where these concepts return in concrete form.
Real-world: This buffering role is exactly what message-queue systems like Kafka play in industry — an intermediary between producers and consumers that absorbs rate mismatches and spikes so no events are lost. Kafka is a distributed, partitioned, replicated commit log: producers append events to it, consumers read from it at their own pace, and the log itself survives machine failures.
9.1.5 The Processing Layer
Third comes the processing — the analytical tier. You are analyzing something, and the processing tier does that. These distributed processing layers are built on the concept of locality of reference.
The professor's intuition — locality of reference. The principle says: move the code to the location of the data, because moving code costs less than moving the data. The data is very large, so moving it incurs a very high network cost — a data transfer cost. Sending a few megabytes of program code to the machine that already holds terabytes of data is cheap; copying the terabytes across the network to the code would be ruinously slow. Every big-data framework you have studied — Hadoop, MapReduce, Spark — is built on this single idea.
What do we need in practice? There is a cluster. We must:
- Partition the data so that parallel processing is possible — split the dataset into chunks, one chunk per node (or per partition).
- Schedule the job — decide which node runs which task, and in what order.
- Manage the jobs with some coordinators — a resource manager that hands out memory and CPU.
You have seen all of this in Hadoop — this is exactly the part YARN does. The good news: the framework takes care of almost everything. The heavy lifting of data partitioning, job scheduling, and managing certain jobs is done for you; you write the transformations and the framework worries about where they run.
Which streaming framework do you use? Several are available:
- Apache Storm — a classic continuous-operator stream processor (you will meet that model in Section 9.2).
- Apache Spark Streaming — with Spark you do batch processing and you do stream processing also, on the same engine.
- Apache Kafka Streaming — good if you only want to do certain lightweight computations, directly inside the message system.
A practical split: heavy processing is always done on Spark; certain lightweight computations on streaming data can be done with Kafka Streaming. So the processing layer is typically the cluster that exists, takes the data, partitions it onto a number of nodes, carries out certain transformations, carries out certain actions, and then passes the data into memory.
9.1.6 The Storage Layer
Then comes the storage tier. It keeps the processed data in memory — or you can persist the data permanently, that is also possible. Usually the data stays in memory, because the data is processed once: there is no need to write it to disk if no one will read it again.
But there are use cases where events or outcomes need to be persisted as well. For example, you process something now so that later, some historical analysis can be carried out on the accumulated results. That is the case where you persist the data on the storage tier.
For permanent data, NoSQL data stores are becoming the more popular choice — MongoDB, Cassandra, and the other NoSQL data stores we have covered in detail elsewhere. There is a decision to make: look at your use case and choose an appropriate NoSQL database. Ask what the use case wants:
- Consistency — do reads need to see the latest writes immediately? (MongoDB is strong in document stores with tunable consistency; HBase offers strong consistency.)
- Wide-columnar data — large, sparse tables with many attributes per row? (Cassandra and HBase are the wide-column stores.)
- JSON documents — semi-structured records that vary from row to row? (MongoDB stores BSON documents directly.)
There is not a single database that fits all use cases. The relational database management system (RDBMS), which follows ACID semantics, is rarely used here — its strict transactional guarantees are a poor fit for high-volume, append-only streaming outcomes.
Real-world: In industry, the permanent storage for streaming outcomes is typically a NoSQL store chosen per use case — consistency, wide-column, or document-oriented — rather than an ACID RDBMS. This matches what you have seen in the NoSQL lectures: pick the store by the shape of the data and the guarantees you need.
9.1.7 The Delivery Layer
The next component is the delivery layer. Data is processed and stored in memory; from memory you go to the delivery layer. It is usually a web-based interface, and nowadays mobile interfaces are also becoming popular. There are dashboards built with streaming visualization — they get continuously updated as the underlying events are processed; the dashboards update automatically. You can use standard web technologies to create these interfaces and keep them updated: HTML, CSS, and JavaScript, with WebSockets for pushing updates from the server to the browser without the browser asking. HTML5 is quite popular for building the interfaces. You can also export the outcomes as PDF or SVG formats — that is also a possibility, for reports that must be shared outside the dashboard.
And it is not only dashboard metrics being maintained. There are other use cases where the delivery tier sends alerts: when a certain thing happens, an alert is sent. Say you have log streaming coming in and you are looking for an error keyword; the delivery tier can send an alert whenever that error is recognized. The delivery tier also feeds the data to downstream applications — a fraud-checking service, a recommendation engine, a billing system. So the delivery tier is not just about maintaining dashboards.
Real-world: Real-time dashboards with WebSockets and HTML5, alerting on error keywords in streaming logs, and PDF or SVG export of outcomes are standard delivery-tier patterns in production streaming systems.
Exam note: The post-mid-semester portion of the comprehensive exam shifts focus to the NoSQL stores — MongoDB, HBase, Cassandra — and to writing queries, including Pig queries typed into the answer text box without any execution environment. Also remember that reliability, availability, Hadoop, and MapReduce remain part of the comprehensive exam.
9.1.8 Where This Architecture Lives in Practice
Putting the four layers together end to end: a click stream from an e-commerce site is collected at edge servers (collection), routed through Kafka where bursts are buffered (data flow), processed in Spark Streaming which computes, say, per-product page-view counts (processing), the counts persist into Cassandra for later analysis (storage), and live charts on the operations dashboard update within seconds while a rule engine raises an alert if error rates exceed a threshold (delivery).
Common pitfalls:
- Skipping the data flow layer. Teams often connect the collector straight to the processor to save a hop. When a flash sale or a viral post arrives, the processor cannot keep up, and events are silently dropped. The buffer layer exists precisely for these bursts.
- Confusing the layers' responsibilities. Collection must only collect; it must not analyze. Delivery must only deliver; it must not store. When a layer takes on another layer's job, scaling becomes impossible — you cannot independently grow what is entangled.
- Choosing one database for every storage need. Expecting an ACID RDBMS to absorb high-velocity streaming outcomes, or expecting a document store to serve wide-column analytics, ends in poor performance. Match the store to the access pattern.
- Forgetting the feedback loop. Iterative analytics (clustering, model retraining) will stall if the architecture has no path from delivery back into analysis. The loop is a deliberate design element, not an afterthought.
Recap + bridge: A streaming system has four layers — collection, data flow, storage, and delivery — connected by a pipeline that also supports a feedback loop for iterative applications. The data flow layer's buffering (the impedance-mismatch fix) and the processing layer's locality-of-reference principle are the two ideas that will explain most of what comes next. In Section 9.2 we look at how traditional streaming systems actually processed records — the continuous operator model.
9.2 The Continuous Operator Model
9.2.1 How Traditional Streaming Processing Was Realized
We have seen the components of a streaming system. Now: how was streaming processing actually realized? It was realized as a continuous operator model — the traditional way to process a stream, and the model against which Spark Streaming later positions itself.
Why "continuous"? The idea is that the processing never stops. In a batch system, a job runs, produces a result, and ends. In the continuous operator model, operators are long-running processes that stay alive for weeks or months, consuming records from their input and emitting records to their output, forever. The program is not "run once over the data"; the program is the pipeline.
In the traditional streaming processing diagram, there is a source operator which receives the streaming data from the data sources — live logs, telemetry data, IoT devices. You can use some Kafka-style data injection to bring the data in — Kafka sits in front and feeds the stream in. Data is then processed in parallel across the cluster:
- The source operator sprays the records over the cluster — it distributes each incoming record to one of the worker nodes.
- The continuous operators process the data — you take the data once, apply map and reduce transformations, and generate the output. This is done on a bunch of records, one record at a time.
- A sync operator collects the processed records.
- Finally, the results are given to the downstream systems — HBase or Cassandra are typical examples.
Real-world: Kafka is a typical injection front-end for this model, and HBase or Cassandra are the typical downstream stores that receive the processed results.
9.2.2 Fine-Grained Record-Level Processing
Look at the granularity: one dot in the diagram is one record, so the model works at a very, very fine level — the granularity is at the record level. This is the defining property of the continuous operator model, and it is worth pausing on.
The cluster has a set of worker nodes, each of which runs one or more continuous operators. Each continuous operator processes the streaming data one record at a time — this is the fine-grained control — and forwards the records to the other operators in the pipeline. If one transformation is being done here, maybe some other continuous operators after this apply further transformations; the operators are set up as a pipeline. Finally, the sync operator collects from the pipeline and sends the data to downstream applications like HBase, Cassandra, and so on.
So the kind of system used is: you get the data from the data sources through the source operators; the source operator sprays the data across the cluster record by record; continuous operators do a certain sort of processing — it may be a pipeline of continuous operators; then the sync operator sends the data to the downstream applications.
The operator pipeline at a glance:
- Purpose: process a live stream record-by-record with the lowest possible latency per record, feeding results to downstream systems.
- Inputs: one or more input streams injected via a front-end like Kafka; a topology (the graph) of operators connected to each other.
- Steps:
- Source operator ingests records from the input stream(s).
- Source operator partitions and sprays records to worker nodes.
- Each continuous operator applies its transformation (map, reduce, filter, etc.) to one record at a time.
- Operators forward processed records to the next operator in the pipeline.
- Sync operator collects all processed records and writes them to downstream systems (HBase, Cassandra, dashboards).
- Outputs: the processed stream delivered to downstream stores or applications.
Visual intuition: Picture the diagram as a conveyor belt running around a cluster. At the head, a spray nozzle (the source operator) sits above the belt; each dot that lands is a single record. The belt passes under a row of worker stations — the continuous operators — and at each station a worker picks up one dot, applies one transformation, and places the transformed dot back on the belt. At the end of the belt, a collection hopper (the sync operator) gathers the finished dots and pours them into downstream stores. The takeaway: every record is handled individually, immediately, as it passes — nothing is grouped, nothing waits.
Where the record-level model gets uncomfortable — a preview:
- Any node failure is a live-data problem. A worker dies, and records that were mid-flight are gone unless the operator was replicated; there is no stored intermediate result to fall back on.
- Load cannot be rebalanced on the fly. The operators are assigned to nodes once, statically. If one node's input floods, that node becomes a straggler while the others idle.
- Fault recovery is not "recompute". The stream is transient — records that passed through are not stored by default, so you cannot simply re-run a transformation over the lost records.
9.2.3 Why We Care About This Model Today
The continuous operator model is not a museum piece — it is the model behind real systems like Apache Storm, and the model that Spark's designers explicitly contrasted with Spark Streaming's batch-based approach. Knowing exactly how it works is what makes the challenges of Section 9.3 and Spark's answers in Sections 9.4–9.6 intelligible: every advantage Spark claims (fast recovery, load balancing, unification) is defined relative to the record-at-a-time behavior you have just seen.
Recap + bridge: The continuous operator model runs a pipeline of long-lived operators — source operator sprays records, continuous operators transform them one at a time, sync operator collects and forwards the results downstream. The fine, record-level granularity is both its strength (low per-record latency) and the source of its weaknesses. In Section 9.3 we examine the three challenges these systems face: fast failure and straggler recovery, load balancing, and unifying streaming with batch and interactive workloads.
9.3 Challenges with the Continuous Operator Model
These systems face three challenges — and the third one is the most important for everything that follows.
9.3.1 Fast Failure and Straggler Recovery
The first thing we want from a streaming system is fast failure and straggler recovery. If there is a failure, we want to recover from it as fast as possible.
What is a straggler? A straggler is a system that runs slowly compared to the other systems and results in a bottleneck, even though the other systems are working fine. Picture a supermarket checkout: every till is quick except one, whose line snakes out into the aisle. The other tills are not the problem — that one slow till throttles the whole store's throughput. Maybe a node that was given more load becomes that bottleneck. So we need a straggler mitigation process: the system must quickly and automatically recover from failures and from stragglers, to keep producing results.
Why is fast recovery difficult to achieve in a continuous operator model? Because of the static allocation of continuous operators to the worker nodes. The continuous operators are given to the worker nodes to operate, and that allocation is static — it does not change and does not adapt to dynamic workloads. That is one challenge with the continuous operators.
Think about what a failure means here. In the continuous operator model the stream is live and transient: a node dies, and the records that node was holding at that instant are simply gone — there is no on-disk intermediate result to recompute from, because records were processed and forwarded as they arrived. And you cannot just wait out a straggler, because a straggler delays every record that must pass through it. The system must detect the slow or failed node and shift its work automatically, or the entire pipeline stalls.
9.3.2 Load Balancing
Load balancing is also a challenge. There is no dynamic adaptability of the resource allocation based on the workload. The operators are pinned to specific workers when the topology is deployed; the system does not re-examine the workload later and move operators around.
The consequence: an uneven allocation of the processing load between the workers can cause bottlenecks. If one worker's operator happens to sit at the hot spot of the stream — say it receives records for the most popular key in a reduce — that worker saturates while the others sit idle. In a system with static allocation, nothing redistributes the load; the hot worker simply becomes a straggler. This is the load-balancing problem in its purest form: the workload is dynamic, but the allocation is frozen at deployment time.
9.3.3 Unifying Streaming, Batch, and Interactive Workloads
The third point — very, very important — is the unification of streaming, batch, and interactive workloads. The kind of system we have seen works at a very fine-grained, record-by-record level, and these systems might be good for querying the streaming data as it flows. But what about interactive query processing, or batch processing over static datasets? These systems might pose a challenge.
The system might have requirements to:
- Query the streaming data interactively — ask a question and get an answer quickly, not as a pre-wired pipeline.
- Combine the stream with static datasets — join live sensor readings with the static product catalog stored in a database.
- Run batch processing — heavy analyses over the full history, using the same logic as the streaming query.
Interactive query processing and batch processing are difficult tasks in a continuous operator system which is not designed for ad hoc queries. An operator topology is compiled for a fixed pipeline; an interactive query arrives at runtime and does not fit any of the installed operators. This requires a single engine that can combine batch, streaming, and interactive processing — and that is where we find that Spark does exactly that.
Exam note: The unification point was flagged as very, very important — it is the central motivation for the whole rest of the discussion. Expect to be asked why a single engine that handles streaming, batch, and interactive workloads is hard to build on the continuous operator model.
9.3.4 Advanced Analytics and a Common Abstraction
There are also complex workloads that require more than simple transformations:
- Continuous learning and updating of data models — the model must be retrained or updated as new stream records arrive, not once on a fixed dataset.
- Querying the streaming data with SQL — the stream should behave like a table you can query.
We will see the unbounded table: a stream of records is coming, you put it in a structured format using a DataFrame, and once that table is updated automatically you can query it using SQL. Whether it is SQL or PySpark or something else, we should have a common abstraction across these analytic tasks — one mental model, one API family — that makes the developer's job much easier.
The common-abstraction wish. Imagine a developer who must write three programs: one streaming filter, one batch aggregation, and one interactive SQL dashboard. In a fragmented world they need three different systems, three different APIs, three different mental models. The common-abstraction requirement says: one abstraction (RDDs, DataFrames) that all three workloads share, so the streaming program and the batch program are recognizably the same kind of code. This is the requirement that no continuous-operator system satisfied, and it is the doorway through which Spark enters.
These were the challenges that existed with respect to the streaming systems. The next section shows how Spark answers each of them.
Recap + bridge: Continuous operator systems face three challenges: fast failure and straggler recovery (hard because operators are statically allocated and the live stream is transient), load balancing (impossible when allocation cannot adapt), and the unification of streaming, batch, and interactive workloads (the most important — impossible when a fixed operator topology cannot answer ad hoc queries). The rest of the lecture shows how Spark's batch-based approach meets all three. Section 9.4 goes through Spark's answers one by one.
9.4 How Spark Meets the Challenges
We have already started with Spark, and we know there is fast recovery from failures and stragglers. Spark addresses these challenges one by one.
9.4.1 Fast Recovery with RDDs and DAGs
We know RDDs and DAGs — we have studied RDDs and distributed shared memory systems, and we said there is a process to deal with the stragglers. There is a DAG being created out of those transformations, and in case an RDD partition is lost you can always reconstruct it.
Why does that solve the recovery problem? Because of the lineage idea:
- Every RDD remembers the transformations that created it — its lineage.
- The sequence of transformations forms a DAG: each RDD is a node, each transformation is an edge.
- If any partition is lost — a node crashed, a straggler fell behind — Spark walks back up the DAG to the surviving inputs and recomputes only the lost partition.
This is the crucial contrast with the continuous operator model. There, a failed node means lost records; the stream is transient and there is nothing to recompute from. In Spark, a failed partition is not lost data — it is a small recomputation. The output is a function of the DAG, and the DAG still exists. And because recovery happens at the granularity of partitions (chunks of a batch), not individual records, the recomputation is fast: only the affected chunks are rebuilt, in parallel across the surviving nodes.
9.4.2 Dynamic Load Balancing and Resource Allocation
Second, there is better load balancing and better use of resources, because Spark has dynamic resource allocation and dynamic load balancing.
In the continuous operator model the operators were pinned to nodes forever. In Spark, tasks are short-lived units of work — one task processes one partition — and the scheduler decides where each task runs. There is no permanent binding between a chunk of data and a machine. If one node becomes loaded, new tasks can be placed elsewhere; if a node idles, work can flow to it. The scheduler watches the actual resource utilization across the cluster and redistributes work accordingly. That is the dynamic load balancing.
There is a second dimension of dynamism: data. Data from the streaming sources can also be combined with a very large range of static data sources available through Spark SQL. So the streaming pipeline is not sealed off from the rest of the data estate — the live stream and the historical tables live under one roof and can be joined in the same job.
9.4.3 One Engine for Every Workload
Third, Spark provides a unification of the different data processing capabilities. Whether you want:
- streaming data,
- streaming with interactive data,
- streaming as well as batch processing,
- or interactive and batch alone —
everything runs on one engine. It is a unified engine that natively supports both batch and streaming workloads, which makes it very easy for developers to use a single framework to satisfy all the processing needs. Whether it is batch, interactive, or streaming, you can use Spark.
Why is one engine such a big deal? Think of the developer's life in the continuous-operator world: streaming requires Storm, batch requires Hadoop MapReduce, interactive requires a query engine — three systems, three deployment models, three programming models, and the awkward task of moving results between them. With a unified engine, the streaming query and the batch query are the same code; the interactive dashboard reads the same tables the batch job wrote. One cluster, one programming model, one place to monitor. This unification was difficult to achieve with the continuous operator models.
9.4.4 Native Libraries for Machine Learning, Graphs, and SQL
Spark also has native integration with advanced libraries, as we have seen:
- Machine learning — you can build and run ML models with Spark's MLlib, right on the streaming or batch data.
- Graph processing — you can integrate graph processing with Spark (GraphX): PageRank, connected components, community detection on large graphs.
- SQL — you want to run SQL queries on top of Spark; that is also possible with Spark SQL.
So Spark gives you a unified engine to process different kinds of workloads, which was difficult to achieve with the continuous operator models. The libraries are not add-ons bolted on the side — they operate directly on the same RDD/DataFrame abstraction the streaming and batch pipelines use, so a model trained on historical data can score live stream records without leaving the engine.
Real-world: This is why Spark became the default platform for many analytics teams: one skill set (Spark) covers the nightly batch warehouse job, the real-time fraud-scoring stream, the ad hoc SQL exploration, and the ML model training — and the team maintains one cluster instead of three.
Recap + bridge: Spark meets the three challenges with three mechanisms: RDD lineage and DAG reconstruction give fast failure recovery; short-lived tasks and a dynamic scheduler give load balancing and resource allocation; and one engine natively supports batch, streaming, and interactive workloads, with native ML, graph, and SQL libraries. In Section 9.5 we see the concrete streaming component — Spark Streaming and its discretized streams.
9.5 Spark Streaming and Discretized Streams
9.5.1 What Spark Streaming Is
Spark Streaming is an extension of the core Spark API that enables scalable, high-throughput, fault-tolerant stream processing of the live data streams. It is the streaming face of the unified engine from Section 9.4 — the same core, the same fault-tolerance machinery, the same programming model, but tuned for data that keeps arriving.
Data can be ingested from many sources: data can come from Kafka, from Flume, you can have data in an S3 bucket or HDFS, a Kinesis stream, or a Twitter stream being created — data can come from any of these sources. Note that HDFS and S3 are largely about batch processing: if your "stream" is actually a directory of files being written, you can still stream over it, but the file-based sources are naturally batch-flavored. The genuinely live sources — Kafka, Flume, Kinesis, Twitter — are where streaming shines.
The data can be processed using complex algorithms expressed with very high-level functions like map, reduce, join, and window. We typically use the map transformation, reduceByKey, and count — count is an action. There is a certain set of transformations we have seen in batch: filter, and all those things. The key point: we just need to know the function. You call map, that is all — you do not know how it is implemented. You call filter and pass a condition saying on what basis we should filter — that is all you are doing. The window operation we will see with the help of an example today (Section 9.9).
The processed data can be pushed out to file systems, databases, and live dashboards:
- You want to store the data in a file system — good for that.
- You want to store it in a particular NoSQL data store — that is also possible.
- You want something sent to the dashboard so that real-time metric tracking is done — also possible.
And you can apply Spark's machine learning and graph processing algorithms on the data streams as well. This is what Spark offers: the streaming layer reuses everything the rest of Spark offers.
9.5.2 Discretized Streams: From Records to Batches
These streams are termed discretized streams — DStreams — where the live input data streams are divided into batches.
The naming matters. Discretized means "made into separate pieces". A continuous stream has no natural breaks — records flow endlessly. A DStream imposes breaks: the stream is sliced into a sequence of finite batches at regular time intervals. Each slice is a separate, self-contained dataset. That single idea — slicing time — is what converts streaming into something Spark's batch machinery can handle.
In the diagram there is a receiver; the records are being fed. Now, instead of records being fed to the cluster one at a time, what we are preparing is a batch of RDDs, and we are sending that to the cluster. It is not record-level: each RDD may contain multiple records as well. So we batch a certain number of records and send them. These batches are then processed to generate the final stream of results in batches.
A DStream is a high-level abstraction which represents a continuous stream of data. It can be created either from the input streams from the sources — Flume, Kinesis, and so on — or you can apply high-level operations on other DStreams. Internally, a DStream is represented as a sequence of RDDs. So a DStream is not a new data structure — it is the familiar RDD, repeated in time:
DStream = [ RDD_0, RDD_1, RDD_2, ... ] (one RDD per batch interval)
So if we compare with the continuous operator model, which was very fine-grained, record-by-record processing, this is a little coarse-grained processing — processing a batch of RDDs. We prepare certain batches and send those batches across the place. Records are processed in batches with short tasks; each batch is an RDD — a partitioned dataset.
Why is batching good for the challenges of Section 9.3? Because each batch is a regular, finite, partitioned dataset: the DAG scheduler can plan it, the task scheduler can balance it, and if a task fails it can be relaunched or recomputed. You will see these benefits in detail in Section 9.6.
Real-world: The ingestion front-ends here are exactly the systems named above — Kafka, Flume, Kinesis, S3, HDFS, and Twitter streams — the standard set of producers for streaming analytics in production. In practice, Kafka is by far the most common of these: Spark Streaming's Kafka connector consumes from Kafka topics, and the same Kafka cluster frequently doubles as the data flow layer from Section 9.1.
Exam note: Spark is a major module — like Hadoop and MapReduce was — and a good portion of the comprehensive exam is from Spark, but it is not going to be very tough: simple things, whatever we have covered, will be there.
Common pitfalls:
- Treating DStreams as records. A DStream is a sequence of RDDs, not a stream of individual records. Operations apply per batch (per RDD), so an operation on "the stream" is really an operation on every batch in turn.
- Confusing sources and receivers. HDFS and S3 are batch-flavored sources; Kafka, Flume, Kinesis, and Twitter are live sources. Choosing the wrong source type for your use case means either re-processing files repeatedly or missing live events.
- Forgetting that count is an action. In the example list, map and reduceByKey are transformations (lazy), while count triggers actual computation. Mixing the two up leads to code that appears to do nothing when run.
Recap + bridge: Spark Streaming ingests live data from many sources and processes it as discretized streams — DStreams, each internally a sequence of RDDs created by slicing the stream into batches. This is coarse-grained, batch-based processing, in deliberate contrast to the record-level continuous operator model. In Section 9.6 we see why batching is worth it: dynamic scheduling, faster failure recovery, and easy interoperability of batch, stream, and interactive analysis.
9.6 The Benefits of Batching
Batching provides dynamic scheduling of tasks, faster failure recovery, and easier interoperability of batch, stream, and interactive analysis. Each of these answers one of the challenges from Section 9.3.
9.6.1 Dynamic Scheduling Instead of Static Allocation
In the traditional system there is static scheduling of the continuous operators to the nodes, which causes bottlenecks. Consider what happens with an uneven partition of the stream: you are sending certain streams here, certain streams there, but more load lands on one node. Because of the static resource allocation, whatever the capacity is, you keep sending more load to that node — so that node may become a bottleneck (a straggler, in the vocabulary of Section 9.3).
When it comes to Spark Streaming, more load on a partition means longer tasks. But the batch model makes the schedule dynamic. The diagram shows dynamic scheduling of the tasks: say the orange node is getting highly loaded. The scheduler dynamically looks at the resource usage rate and the resource allocation, and it shifts some of the tasks to the other nodes, based on the available resources.
How dynamic scheduling works — the professor's picture: Static allocation may give you uneven partitions, but if you look at the resource usage across different nodes, when the scheduler finds that one node is getting overloaded, it redistributes the load across other nodes rather than letting it become a straggler. The key mechanism: in Spark, tasks are short-lived and placeable — each batch produces a fresh set of tasks, and the scheduler assigns them to wherever capacity is free, watching utilization as it goes. Nothing is permanently pinned to a machine.
Why is this impossible in the continuous operator model? Because there the "tasks" are long-lived operators wired to specific nodes at deployment time. A batch system creates new tasks for every batch, every few seconds — each scheduling round is a fresh chance to balance the load.
9.6.2 Failure Recovery at Coarse Grain
If a node fails, the failed tasks are relaunched on the other nodes. There is also faster recovery by using multiple nodes for recomputation.
This is possible at a coarse-grain level of processing, not at a fine-grain level. The reason:
- In the record-level model, a failure at a node destroys the in-flight records held by that node — transient data with no backup.
- In the batch model, the failed node held partitions of an RDD. Those partitions are recoverable: their lineage (the DAG of transformations that produced them) is still intact, and their inputs still exist on other nodes.
So recovery is a matter of relaunching the failed tasks and recomputing the lost partitions — and because the recomputation happens in parallel across all surviving nodes (multiple nodes for recomputation), it is fast. These are the benefits of batching that Spark offers.
9.6.3 Interoperability of Batch, Stream, and Interactive Analysis
Batching gives easier interoperability of batch, stream, and interactive analysis. You can run anything, or a combination of these. The DStream — the discretized stream — is a series of RDDs, and every RDD operation you know from batch processing applies to it. Concretely:
- Join a DStream with a static RDD — the live stream can be combined with historical or reference data as if both were ordinary RDDs.
- Convert a DStream's RDDs into a DataFrame — a DataFrame is structured data.
- Query the structured data interactively using SQL — now your live stream is a queryable table (the unbounded table of Section 9.10).
The stream is not a separate universe. Because it is built from the same RDD abstraction as batch, every batch tool — joins, SQL, DataFrames, ML — works on streaming data with no bridge layer. That is the unification promised in Section 9.4, delivered at the data-structure level.
9.6.4 Machine Learning on Streams
You can also apply machine learning library functions on the data stream's RDDs. For example, you can apply a k-means model to label the streaming data: streaming data is coming, you want to label those records, you want to group those records — k-means clustering — you can do that. The MLlib functions accept the stream's RDDs directly, because those RDDs are just RDDs.
Real-world: This is how real-time personalization is built: a model is trained offline on historical data (batch), then loaded and applied to the live stream — scoring each incoming record against the trained model, or updating the model continuously as new labeled records arrive. Fraud detection, recommendation ranking, and IoT anomaly detection all follow this shape: batch-train, stream-score.
Pitfalls to avoid:
- Expecting fine-grained recovery semantics. Recovery in Spark Streaming is per partition and per batch — you get exactly the fault tolerance of the batch model, not record-level checkpoints. If your application requires per-record exactly-once state across failures, that is a stronger guarantee than plain batching gives you by itself.
- Forgetting that each batch is a scheduling event. If the batch interval is tiny, tasks are relaunched constantly and scheduling overhead grows; if it is huge, results lag. The batch interval is a tuning knob that balances latency against overhead.
- Treating the stream as un-joinable. Because a DStream is a sequence of RDDs, joining with static data is trivial — teams that do not realize this build awkward custom lookup pipelines.
Recap + bridge: Batching delivers the three benefits — dynamic scheduling (fresh tasks per batch, load redistributed away from overloaded nodes), faster failure recovery at coarse grain (relaunch tasks, recompute lost partitions in parallel via lineage), and interoperability (join DStreams with static RDDs, convert to DataFrames, query with SQL, apply ML). In Section 9.7 we look at where streaming data actually enters the system: sources and receivers.
9.7 Spark Streaming Sources and Receivers
9.7.1 Sources: Basic and Advanced
Every input DStream is associated with a receiver object, which receives the data from the source and stores it in Spark's memory for processing. The receiver is the connection point: it is the object that actually listens to the source and hands the data to Spark.
There are two families of sources:
- Basic sources — directly available in the StreamingContext API:
- the file system — read as a stream of files (with HDFS and S3 being batch-flavored, as noted in Section 9.5),
- socket connections — a TCP stream of text lines,
- and sources created directly through the StreamingContext API.
- Advanced sources — Kafka, Flume, Kinesis:
- available through extra utility classes (connector libraries),
- these require linking against extra dependencies — you add the connector artifact to your build, and often extra configuration (like the Kafka brokers) at runtime.
The distinction matters practically: basic sources work out of the box, while advanced sources need the matching connector dependency and its configuration.
9.7.2 Reliable and Unreliable Receivers
There are two kinds of receivers:
- A reliable receiver sends an acknowledgement back to the source when the data is received and stored in Spark with replication. The ack tells the source "this record is safely in Spark; you do not need to keep it." If Spark fails before the ack, the source can resend — nothing is lost.
- An unreliable receiver is one that does not send an acknowledgement to the source. It receives the data, but the source has no way to know whether the data arrived and was stored.
Why the distinction matters — connecting to the semantics of Section 9.1: The reliable receiver is how at-least-once semantics become real. The acknowledgement handshake with the source means that anything the source has not received an ack for can be retransmitted after a failure. With an unreliable receiver, the source assumes delivery once it hands the record over — if the receiver dies before storing, that record is lost. So the receiver choice is a direct trade-off: the reliable receiver costs a little overhead (the ack) and buys the no-loss guarantee; the unreliable receiver is cheaper and weaker. This is also why receiver storage uses replication, as the reliable receiver stores "in Spark with replication" — a second copy on another node — so a single node failure does not lose the received data.
9.7.3 How Data Moves from Source to Cluster
Q: After the data has been received from the source — let us say some sensor — the collection tier has collected it. Now it has to be sent to the cluster. Whether the nodes will pull it, or somebody is going to send it?
A: This is being taken care of by the receiver. The receiver receives the records, it will prepare the batch of RDDs, and then it is going to send it. All those things — how to partition it, a batch of RDDs being prepared, partitioning across a cluster — the framework takes care of all these things. At a high level, a stream of records is coming; instead of processing at the record level, we build a discretized stream — a batch of RDDs — and divide it across a cluster, which gives us coarse-grain processing, not a very fine-grain processing, with the benefits we have talked about.
So the answer to the student's question: nobody pulls, and nobody pushes per record. The receiver is the active agent. It ingests the stream from the source, organizes the records into batches, materializes each batch as an RDD, and hands the batch to the cluster. The partitioning of the RDD across nodes and the scheduling of tasks are framework responsibilities — the developer never writes that code. (Several students asked a version of this question; the receiver is the single answer for all of them.)
9.7.4 Delivery Semantics
At-least-once semantics, at-most-once semantics, exactly-once semantics — all those things can be maintained. Which one you get depends on the combination of choices:
- the receiver type (reliable or unreliable),
- whether the source supports acknowledgements and replay,
- the output operations and whether they are idempotent.
With a reliable receiver and a replay-capable source you can achieve at-least-once, and with the appropriate output transactionality, effectively exactly-once.
Exam note: the three delivery semantics (at-least-once, at-most-once, exactly-once) are exam-relevant vocabulary. Know the definition of each and which receiver choice supports the no-loss guarantee.
Pitfalls to avoid:
- Assuming a reliable receiver means exactly-once. It gives at-least-once: duplicates are possible after retransmission. Exactly-once additionally requires deduplication or transactional outputs.
- Using an unreliable receiver for critical data. Any failure between source and Spark is silently lost — no ack, no retry, no record.
- Forgetting the connector dependency. Advanced sources (Kafka, Flume, Kinesis) are not bundled with the core Spark distribution — missing the extra dependency is the classic "why does my program not compile" trap.
Recap + bridge: Every input DStream has a receiver that pulls records from a source, prepares the batch of RDDs, and hands it to the cluster; sources are basic (file system, sockets, StreamingContext API) or advanced (Kafka, Flume, Kinesis — extra dependencies); receivers are reliable (ack + replication, at-least-once support) or unreliable (no ack). The receiver answers the pull-vs-push question: the framework does everything. In Section 9.8 we see what you can do with the DStream once it exists — transformations and output operations.
9.8 Transformations and Output Operations on DStreams
9.8.1 Transformations
Spark transformations allow modification of the data from the input stream. Transformations on DStreams mirror the RDD transformations we saw in batch processing. We saw certain transformations on RDDs in batch processing; the similar kind of transformations are possible on an input DStream too: you have map, filter, union, count, reduce, and countByValue — those are possible on the discrete stream.
The key principle: because a DStream is a sequence of RDDs, a DStream transformation is just the corresponding RDD transformation applied to every RDD in the sequence. There is no new machinery — the batch vocabulary carries over unchanged.
A quick reminder of what the familiar ones do:
- map(func) — pass each element of each batch through a function func; returns a new DStream of the results.
- filter(func) — keep only the records on which func returns true.
- union(otherStream) — merge the elements of two DStreams into one.
- count() — count the elements in each RDD of the source DStream; produces a DStream of single-element RDDs.
- reduce(func) — aggregate the elements within each RDD using an associative and commutative function func.
- countByValue() — for a DStream of elements of type K, produce a DStream of \((K, \text{Long})\) pairs where the value of each key is its frequency within each RDD of the source.
There are also the stream-specific building blocks you will see again in later sections: reduceByKey(func) aggregates the values for each key within each batch (Section 9.11 shows exactly why this one costs a shuffle), transform(func) applies an arbitrary RDD-to-RDD function to each RDD of the source DStream — this is the escape hatch that lets you use any batch RDD operation inside a streaming job — and updateStateByKey(func) maintains a state per key across batches, updating the state for each key by applying a function to the previous state and the new values. The window-level transformations (countByWindow, reduceByWindow, reduceByKeyAndWindow, countByValueAndWindow) are covered with the window operations in Section 9.9.
9.8.2 Output Operations
There are also output operations that allow our DStream data to be pushed out to external systems like a database or a file system:
- print() — prints the first ten elements of each batch of data on the driver node running the streaming application; useful for development and debugging.
- saveAsTextFiles(prefix, suffix) — saves the DStream contents as text files; the file name at each batch interval is generated based on prefix and suffix.
- saveAsObjectFiles(prefix, suffix) — saves the DStream contents as SequenceFiles of serialized Java objects.
- saveAsHadoopFiles(prefix, suffix) — saves the DStream contents as Hadoop files.
- foreachRDD(func) — the most generic output operator: it applies a function func to each RDD generated from the stream; that function pushes the data to an external system — for example, writing it over the network to a database. That kind of function you can always write.
Each batch's output is written independently, with the batch timestamp embedded in the file name (the prefix-TIME_IN_MS-suffix pattern), so the outputs of different batches never collide.
So, answering the natural questions: it is not only that the data stays in memory — memory is fast, and we have seen how failures are dealt with. Can we do batch processing with this? We have seen that. Can we do interactive query processing? We have seen that. The certain transformations we saw with the batch processing — can we do the same with a stream of data? That is what we are seeing now. And if we want to save the file on a persistent storage, that is also possible using output operations.
Q: Do transformations on a DStream behave just like the transformations on an RDD from batch processing?
A: Yes — that is the design. A DStream is a sequence of RDDs, so a DStream transformation is the same RDD transformation applied batch by batch. What differs is the addition of stream-flavored operations: the window operations (Section 9.9) that aggregate across several batches, and updateStateByKey that carries state across batches. The output side is also new: output operations like print, saveAsTextFiles, saveAsHadoopFiles, and the generic foreachRDD push each batch to external systems, which a pure batch pipeline never needs.
Pitfalls to avoid:
- Writing output operations that block the stream. Output operations (especially foreachRDD writing to a database) run as part of the streaming job — a slow sink slows the whole stream. The recommended pattern is to send writes to an external service and return quickly.
- Forgetting that print shows only the first ten elements. print() is a debugging tool; a dashboard feed is a real output operation.
- Confusing count() with countByValue(). count() gives the number of elements in each RDD; countByValue() gives the frequency of each distinct value. Both are actions in the streaming context.
Recap + bridge: DStream transformations mirror the batch RDD transformations (map, filter, union, count, reduce, countByValue, reduceByKey, plus the stream-specific transform and updateStateByKey), and output operations push each batch to external systems (print, saveAsTextFiles, saveAsObjectFiles, saveAsHadoopFiles, foreachRDD). The stream is thus fully interoperable with batch and interactive processing. In Section 9.9 we add the dimension that streaming alone needs: window operations that aggregate across several batches.
9.9 Window Operations
9.9.1 Windows and Sliding Windows
I talked about the window, so let us look at it. Window operations apply over batches of RDDs: there is an incoming data stream coming; the RDDs are indexed. We make a window of RDDs and want to apply an operation on a window. If three RDDs are there in one window:
\[W_t = \{R_t, R_{t+1}, R_{t+2}\}\]
where \(W_t\) is the window at position \(t\), and \(R_t, R_{t+1}, R_{t+2}\) are the three consecutive RDD batches inside it. The index \(t\) counts batch intervals: \(R_t\) is the batch produced for the time interval \(t\), \(R_{t+1}\) the next one, and so on. The window length is three batches, so a window always spans three consecutive batch intervals.
There is also the concept of a sliding window. This is one size of the window — three RDDs — and now I want to slide it by two. I have included this one and this one, so:
\[W_{t+2} = \{R_{t+2}, R_{t+3}, R_{t+4}\}\]
The slide says how far the window moves each time. Sliding by two from position \(t\) lands at position \(t+2\), and the new window again covers three consecutive batches — but note the overlap: \(R_{t+2}\) is shared between \(W_t\) and \(W_{t+2}\), because the window (three wide) moved only two steps. The window size and the slide are independent choices: window size controls how much history you look at; slide controls how often you recompute.
If you want to analyze certain metrics like a moving average kind of thing, you can always decide how much to move the window — the window size and the slide are configurable. These operations are possible using window operations.
The professor's intuition — moving averages: A moving average at time \(t\) is the average of the last \(N\) values; every new observation, the average slides forward. A sliding window is exactly that, in batch form: the window is the "last N batches", the slide is how often you emit a fresh average. You choose the window size (how many batches of history) and the slide (how far the window jumps each time) to match the sensitivity you want: a long window smooths noise but reacts slowly; a short window reacts fast but is noisy. Where the analogy differs: a moving average typically slides by one observation at a time, while a streaming window may slide by several batches at once — the slide is your choice, not fixed at one.
Worked example — window of three RDDs slid by two, computing moving-average-style sums.
Suppose a click-stream is batched every second, and each batch RDD holds the click counts for a product. The batches arrive as:
| Batch | R_t | Contents (clicks per product) |
|---|---|---|
| t | R_t | A: 10, B: 20 |
| t+1 | R_{t+1} | A: 12, B: 18 |
| t+2 | R_{t+2} | A: 15, B: 25 |
| t+3 | R_{t+3} | A: 11, B: 22 |
| t+4 | R_{t+4} | A: 14, B: 30 |
Step 1 — build the first window. Window length 3, starting at \(t\):
\[W_t = \{R_t, R_{t+1}, R_{t+2}\} \quad\Rightarrow\quad \text{sum}(A) = 10 + 12 + 15 = 37,\quad \text{sum}(B) = 20 + 18 + 25 = 63\]
Step 2 — slide by two. The window moves from position \(t\) to position \(t+2\), dropping \(R_t\) and \(R_{t+1}\), keeping \(R_{t+2}\), and adding \(R_{t+3}\) and \(R_{t+4}\):
\[W_{t+2} = \{R_{t+2}, R_{t+3}, R_{t+4}\} \quad\Rightarrow\quad \text{sum}(A) = 15 + 11 + 14 = 40,\quad \text{sum}(B) = 25 + 22 + 30 = 77\]
Step 3 — sense-check. Between the two windows, product A's total rose from 37 to 40 and product B's from 63 to 77: the later three batches contain more clicks than the earlier three, so both totals rising is exactly what the raw counts show. If you divide each total by the window length (3), you get the moving average: A moved from 12.3 to 13.3 clicks per batch, B from 21.0 to 25.7 — a smooth, per-product trend line over time. This is the "moving-average-style metric" the lecture refers to: the window length sets the smoothing, the slide (2) sets how often the trend is updated.
9.9.2 Window-Level Transformations
There are transformations on windows: countByWindow, reduceByWindow, reduceByKeyAndWindow, countByValueAndWindow — all those things which we have seen with the others are also available with these window operations. When you create a batch of RDDs and put them into one window, you can apply an operation at the window level.
The standard window API takes two parameters, expressed in batch intervals:
- windowLength — the duration of the window: how many batches (or seconds) of history each window covers.
- slideInterval — the interval at which the window operation is performed: how often a new window is formed and the operation re-run.
The window-level transformations:
- window(windowLength, slideInterval) — creates a new DStream of windows; the raw material for all the others.
- countByWindow(windowLength, slideInterval) — counts the elements inside each window.
- reduceByWindow(func, windowLength, slideInterval) — reduces the elements within each window using an associative, commutative function func.
- reduceByKeyAndWindow(func, windowLength, slideInterval, numTasks) — the keyed version: reduces the values per key across the whole window; with an inverse function form (reduceByKeyAndWindow(func, invFunc, ...)) it can reuse the previous window's result for efficiency — subtract the expiring batches, add the new ones — instead of recomputing the whole window.
- countByValueAndWindow(windowLength, slideInterval) — the frequency of each distinct value within each window.
The canonical textbook pattern for this is a windowed word count: a socket text stream is split into words, mapped to (word, 1) pairs, and reduced with reduceByKeyAndWindow over a window of 60 seconds sliding every 10 seconds — giving the word frequencies for the last minute, updated every ten seconds.
Visual intuition: Picture a timeline of batch boxes marching left to right, one box per batch interval. A bracket (the window) encloses three consecutive boxes. Every slide step, the bracket jumps right by two boxes and re-closes, always covering exactly three. Where the bracket sits at a given moment is \(W_t\); after the jump it is \(W_{t+2}\). The one-sentence takeaway: the bracket's width is the window length, its jump size is the slide, and both are under your control.
Pitfalls to avoid:
- Confusing window length with slide. Length = how much history you look at; slide = how often you recompute. Setting them equal gives non-overlapping windows (like a plain batch partition); a slide smaller than the length gives overlapping windows with shared batches.
- Forgetting that window parameters are in batch intervals. A windowLength of 60 with a 1-second batch interval means 60 RDDs per window. Change the batch interval and the same numbers mean different durations.
- Recomputing whole windows wastefully. For sliding windows with large overlaps, the incremental reduceByKeyAndWindow form (with the inverse function) avoids re-aggregating batches that were already in the previous window — the naive form recomputes nearly everything on every slide.
Recap + bridge: Window operations aggregate over several consecutive batches: a window \(W_t\) covers the last three RDDs, sliding by two moves to \(W_{t+2}\), and windowLength plus slideInterval are fully configurable — the basis for moving-average-style metrics. Window-level transformations (countByWindow, reduceByWindow, reduceByKeyAndWindow, countByValueAndWindow) apply the familiar aggregations at the window level. In Section 9.10 we move from DStreams to the DataFrame-based view of streaming: Structured Streaming and the unbounded table.
9.10 Structured Streaming: The Unbounded Table
9.10.1 From RDDs to DataFrames to an Unbounded Table
As I mentioned earlier, there is also structured streaming, which works on the Dataset/DataFrame API and turns the stream into a structured form. Let us retrace the progression: we talked about RDDs — an RDD is basically unstructured data: it is a bag of objects, with no schema attached. After that we said that to store structured data you go for the datasets; if you want type checking, you go over the DataFrame. So you build a DataFrame out of the stream and then you say: okay, this is my structured data. You do not have to do batching of that: the stream is continuously coming, and you keep converting it to a DataFrame, which gives it structure, and the data is processed.
The mental model:
- RDD — unstructured. Rows have no enforced schema; the programmer knows what is inside, the framework does not.
- Dataset / DataFrame — structured. Columns have names and types; the engine can check types, optimize queries, and run SQL.
- Structured streaming — take the stream and build a DataFrame from it continuously. Every incoming record is appended to the DataFrame as a new row, in structured format, with no manual batching step.
Once you have given it a structure, it becomes an unbounded table: the stream will come, and you will keep on appending the records to it — you can see the new rows are coming and being added in structured format. So this becomes a kind of an unbounded table — a table that has no fixed end, because new rows keep arriving.
The professor's intuition — a table that never ends: A normal table has a fixed number of rows when you query it. An unbounded table is a table where the rows keep appearing while you are looking at it: query the table now and you see the rows that have arrived so far; query it again a second later and more rows have been appended. The stream does not need to be partitioned into manual batches for this to work — the stream is continuously coming and rows are appended in structured format automatically. That automatic append is what makes it "unbounded": the only limit is how long you keep listening, not any fixed end in the data itself.
Visual intuition: Picture a spreadsheet that grows on its own. Columns are the fields of your records (say, timestamp, device, metric, value). Rows appear at the bottom continuously, one or several per second, as the stream delivers them. There is no last row until you stop watching. That spreadsheet is the unbounded table; every new event simply appends a new row in the defined column format.
9.10.2 SQL on Streaming Data
Now what is possible: you can use SQL and query on the streaming table. The streaming data can be queried because you converted a stream to a DataFrame — a DataFrame is a kind of structured data — you did not have to do batching, it has become an unbounded table, and you query it using SQL to find out certain analytics. That is running SQL on the streaming data.
So a query like "what is the average metric value per device in the last hour?" can be written in plain SQL and executed against a live stream — the engine re-runs it incrementally as new rows arrive, without the developer writing any streaming code. Whether it is SQL or PySpark or something else, the same structured abstraction serves them all — the common abstraction promised in Section 9.3.4.
Q: Is there any upper bound on the unbounded table?
A: It depends on how much is the size of the main memory, because you are keeping the data in the memory. The table can only be as big as what the memory can hold. Once the memory fills, older rows must be evicted or summarized — which is exactly the kind of decision the window operations of Section 9.9 and the cache eviction policies of Section 9.12 manage.
This is an important practical point: "unbounded" means unbounded in time — the stream has no fixed end — but not unbounded in space. The table lives in the cluster's memory, so its size is bounded by memory. Applications that need long histories handle this by keeping only summaries (windows, aggregations) in the table and writing raw records to persistent storage.
Exam note: you can get a question where you have to write queries, ranging across all the NoSQL data stores — and SQL on streaming data, as shown here, is part of that query-writing family.
Pitfalls to avoid:
- Assuming the unbounded table can grow forever. The table is bounded by main memory; plan eviction or aggregation or the engine will drop or spill data.
- Forgetting that an RDD carries no schema. Structured streaming works because the DataFrame adds names and types; querying an RDD directly gives you none of the SQL or type-checking benefits.
- Thinking batching is required for structure. The stream is continuously converted to a DataFrame — no manual batching step is needed; the engine handles the incremental append.
Recap + bridge: Structured streaming builds a DataFrame continuously from the stream: every record is appended as a row, forming an unbounded table — unlimited in time, bounded by memory — which you can query with SQL. This realizes the common-abstraction and SQL-on-streaming requirements from Section 9.3.4. In Section 9.11 we look under the hood at the most expensive operation in all of this — the shuffle.
9.11 Shuffles: The Costly Wide Transformation
9.11.1 What ReduceByKey Does
Shuffle is one of the operations that is actually a costly operation. Costly means: an operation which needs the data from multiple nodes — a wide transformation. Let us take the example of reduceByKey. What reduceByKey does: it generates a new RDD where all the values for a single key are combined into one tuple, and the key and the result of executing a reduce function against all the values is associated with that key.
The professor's bridge to MapReduce: This is the same reduce idea we used in MapReduce, applied to a key-value dataset. In MapReduce, the shuffle-and-sort step grouped all values of a key together and handed them to a single reduce task; here, reduceByKey performs the same grouping-and-combining inside Spark, with a reduce function you supply — addition in the example below. One familiar idea, one new engine.
Let us look at an example — we have done this earlier, and it is similar to the reduce operation we did in MapReduce. We have a key-value pair dataset:
\[(1, 2), \quad (3, 6), \quad (3, 4)\]
I want to reduce by key. I will collect all the points with key 1 — it is only one point, so it will be one single thing. I will collect all the points with key 3 — there are two points, so I will combine them. I want to combine the values, and here I am combining them like adding those values: \(6 + 4 = 10\). So the result will be:
\[(1, 2), \quad (3, 10)\]
Worked example — reduceByKey with addition on (1,2), (3,6), (3,4).
Dataset: \(D = \{(1, 2), (3, 6), (3, 4)\}\), with key \(k\) as the first element and value \(v\) as the second.
Step 1 — group by key. Partition the tuples by their first element:
- key \(1\): the values are \(\{2\}\) — one tuple.
- key \(3\): the values are \(\{6, 4\}\) — two tuples.
Step 2 — apply the reduce function per key. The reduce function is addition: combine the values of each key with \(+\):
- key \(1\): only one value, so the reduced value is \(2\) (no combination needed).
- key \(3\): \(6 + 4 = 10\).
Step 3 — emit one tuple per key. The result is:
\[(1, 2), \quad (3, 10)\]
Sense-check: the result contains exactly one tuple per distinct key in the input — key 1 and key 3 — and each value is the sum of all input values for that key. The total value mass is preserved: input values \(2 + 6 + 4 = 12\), output values \(2 + 10 = 12\). The same idea applies with any associative reduce function — max, min, product, concatenation — which is why reduceByKey can run partially in parallel on each partition before the shuffle.
9.11.2 Why a Shuffle Happens
What is the bigger challenge in doing this kind of operation? Not all the values for a single key necessarily reside on the same partition. This \((3,4)\) may be on another partition; this \((3,6)\) may be on another partition — same key, different partitions. To compute the result we must locate them; we must bring them together to compute the result — only then is it possible. That triggers a shuffle operation to occur, and when a shuffle occurs, a lot of data transfer happens.
The shuffle is Spark's mechanism for redistributing the data so that it is grouped differently across partitions. This typically involves copying the data across executors and machines, which makes the shuffle a complex and costly operation. The data is not generally distributed across partitions in the place needed for a specific operation: during computation, a single task will operate on a single partition. So to organize all the data for a single reduceByKey reduce task to execute, Spark needs to perform an all-to-all operation: it must read the data from all the partitions to find out all the values for all the keys, then bring together those values corresponding to a common key and compute the final result. This is what we call a shuffle, and it is a very costly operation — we will optimize certain things in this.
Why exactly is it so costly?
- All-to-all data movement. Every source partition may hold a piece of every key, so each reduce task can need input from every map-side partition. The data crosses the network between executors — the slowest resource in the system.
- Spill to local disk. Even for in-memory RDDs, the shuffle implementation writes its output to partitioned files on local disk between the stages, and the next stage fetches those files.
- Serialization and fetch. Data is serialized, written, read back, deserialized, and transferred — CPU and I/O costs on every byte.
- Loses locality. The data locality principle from Section 9.1 is violated: the data has to move to where the reduce tasks run, instead of the tasks moving to the data.
9.11.3 Operations That Trigger Shuffles
Operations which can cause a shuffle include:
- repartition() — repartitioning redistributes data to a different number of partitions, which requires moving records across nodes.
- reduceByKey() — grouping values by key brings same-key records together (as just shown).
- groupByKey() — the same grouping, without the reduce step: all values for each key are gathered into an iterable.
- join() — joining two datasets on a key must bring matching keys together across both inputs.
All of these share the same root cause: records that belong together are scattered across partitions, and the engine must gather them before the operation can proceed. Contrast with the narrow transformations — map, filter, flatMap — where each output partition depends only on its own input partition: those never shuffle.
9.11.4 Working Around Shuffles with DAG Stages
How do we optimize? Remember the DAG is computed in stages. You carry out certain transformations, and you place certain transformations in certain stages: transformations which can be run parallelly across the nodes should be put in one stage. Whenever the shuffle happens — the data transfer happens — you put the transformations on the other stage. All transformations which can be carried out without shuffling the data can be run parallelly. And the shuffle, if at all we can postpone it, let us postpone it — it should be put in a second stage if at all possible. That is how we optimize, because if we do the data transformation multiple times that will be very, very costly.
The stage structure is Spark's answer to shuffle cost:
- The DAG of transformations is divided into stages at shuffle boundaries.
- All transformations inside one stage are pipelined: each task does its full chain of map/filter/etc. on its partition, in parallel, with no data movement between them.
- A shuffle forces a stage boundary: the stage before the shuffle writes its partitioned output; the stage after fetches it and continues.
A job like word count — textFile → map(flatMap to words) → reduceByKey → count — becomes two stages: the flatMap and map side run entirely in parallel in stage 1 (the reduce function even runs as a combiner on the map side), and only the reduceByKey grouping needs the shuffle into stage 2. By keeping every non-shuffling transformation in the same stage, the engine guarantees the data is touched once, not repeatedly serialized and re-fetched.
Exam note: expect questions on which operations cause shuffles and why the shuffle is costly — the reduceByKey walkthrough above is the canonical example. Also, given a use case, you may have to write map and reduce programs for it; the things will not be very complex.
Pitfalls to avoid:
- Using groupByKey where reduceByKey works. groupByKey shuffles all values for a key and then aggregates; reduceByKey aggregates locally on each partition first (like a combiner) and shuffles far less data. Same result, very different cost.
- Believing shuffle only matters for big jobs. Every all-to-all movement — even on small data — costs serialization, disk, and network; the per-record overhead is what makes pipelines with repeated shuffles slow.
- Forgetting that map and filter never shuffle. A pipeline of only narrow transformations runs in a single stage at full parallelism — a good target state to design toward.
Recap + bridge: A shuffle is an all-to-all redistribution triggered when records that belong together (same key) sit on different partitions — reduceByKey, groupByKey, repartition, and join all cause one. It is costly because data moves across the network, spills to local disk, and loses locality. The DAG optimizer responds by cutting stages at shuffle boundaries: all parallel, non-shuffling transformations share one stage, and shuffles are pushed into later stages. In Section 9.12 we look at the other pillar of Spark performance — keeping data in memory with persistence and storage levels.
9.12 RDD Persistence and Storage Levels
9.12.1 Persisting and Caching an RDD
One of the most important capabilities in Spark is persisting or caching a dataset in memory. When you persist an RDD, each node stores any partitions of it that it computes in memory and reuses them in the other actions on the dataset, which allows future actions to be much faster. You can mark an RDD to be persisted using the persist() or cache() methods. The first time it is computed in an action, it will be kept in the memory on the nodes.
Two clarifications that make the behavior concrete:
- persist() and cache() do not compute anything by themselves. They mark the RDD with a flag: "when this RDD is first computed, keep it in memory". The marking is lazy; the storage happens when the first action forces the computation.
- The benefit is reuse. A transformed RDD used by several downstream actions — say, a cleaned dataset feeding both an aggregation and a join — is computed once and served from memory afterwards, instead of recomputed from its lineage on every action. This is the same reason MapReduce is at a disadvantage here: each MapReduce job reloads its input from disk, while Spark's cross-cluster in-memory cache keeps the working set resident.
The Spark cache is fault tolerant: if any partition of an RDD is lost, it will automatically be computed again using the transformations. It is the same thing as with the DAG: if any partition of an RDD is lost, we can always recreate it using the transformations that originally created it. Persistence and lineage are complementary: persistence makes reuse fast, lineage makes loss survivable — a lost cached partition is simply recomputed from its parents in the DAG, not fetched from a backup.
When would you want this? Suppose spillover is happening — certain RDDs are spilling to disk and you do not want that — or you know you need the same RDD again later. Then you persist: you use a persist function or a cache function, and the first time it is computed in an action it will be kept in the memory on the nodes.
Visual intuition: Picture a printer (the lineage DAG) that reprints any page on demand. Persisting is like pinning a page to the wall: the first time it is printed you keep it, and everyone who needs it reads it from the wall instead of waiting for another print run. If the page falls off the wall (a node dies), the printer can still reprint it — that is the fault tolerance — but the fast path of reading from the wall is what persistence buys.
9.12.2 The Storage Levels
Each persistent RDD can be stored using a different storage level, allowing you to persist the dataset on the disk, or to persist it in memory as serialized Java objects. When you store data as serialized Java objects it requires less space; however, the retrieval may be a little time-consuming. These levels are set by passing a StorageLevel object to persist. The cache() method is a shorthand for using the default storage level — StorageLevel.MEMORY_ONLY.
The storage levels:
- MEMORY_ONLY — store the RDD as deserialized Java objects in memory. This is generally the fastest, most CPU-efficient option: the objects are already in their usable form, so retrieval has no serialization cost.
- MEMORY_ONLY_SER — store in memory as serialized Java objects. This is generally more space efficient than deserialized objects, especially with a fast serializer, but it is more CPU intensive to read — every read must deserialize the byte arrays back into objects. Each partition is stored as one byte array, which also reduces garbage-collection pressure.
- MEMORY_AND_DISK — store as deserialized Java objects in memory; if partitions do not fit in memory, store the partitions on disk and read them whenever they are needed.
- MEMORY_AND_DISK_SER — similar to MEMORY_ONLY_SER, but spill the partitions that do not fit in memory to disk instead of recomputing them on the fly each time they are needed: store in memory in a serialized fashion, and if spillover happens, go to disk.
Resolving the "default level" question: The lecture states, right after describing MEMORY_AND_DISK, that "this is the default level". The documented default, however, is different: in Spark, the default storage level for cache() and persist() is MEMORY_ONLY (this is what the Spark reference states, and it is the level cache() is defined to use). What the recording likely refers to is the receiver's default: Spark Streaming's input receivers (as in the socketTextStream example) default to MEMORY_AND_DISK_SER, since received batches must survive memory pressure. For your exam: cache() = MEMORY_ONLY default; MEMORY_AND_DISK_SER is the receiver-oriented, spill-tolerant choice.
Each storage level offers a different trade-off between memory usage and CPU: storing serialized objects saves space but costs CPU to read and write.
| Level | Where stored | Representation | Space | CPU | Spill to disk? |
|---|---|---|---|---|---|
| MEMORY_ONLY | memory | deserialized objects | high | low | no (recompute if lost) |
| MEMORY_ONLY_SER | memory | serialized byte array | low | medium | no (recompute if lost) |
| MEMORY_AND_DISK | memory, then disk | deserialized objects | medium | low | yes |
| MEMORY_AND_DISK_SER | memory, then disk | serialized byte array | low | medium | yes |
The serialized levels also benefit from a fast serialization library: Kryo serialization is typically smaller and faster than the default Java serialization for RDD partitions, and compressing serialized partitions (spark.rdd.compress) saves more space at the cost of CPU.
9.12.3 Choosing a Storage Level
Which storage level to choose? If you do not know your data, probably this would be the last one — MEMORY_AND_DISK_SER. The recommended process:
- If your RDD fits comfortably with the default storage level (MEMORY_ONLY), leave it the way it is. Retrieval is fast, it is the most CPU-efficient option, and the operations on the RDD run as fast as possible because your whole data is in memory.
- If it does not fit, try MEMORY_ONLY_SER and select a fast serialization library, to make the objects more space efficient but still reasonably fast to access.
- Do not spill it to disk unless the functions that computed your dataset are expensive, or they filter a large amount of data. If the functions are expensive and something is lost, you have to do all the functions again; if they filter a very large amount of data, the filtration process will happen again. So do not spill to disk unless one of these conditions holds.
- Use the replicated storage levels if you want fast fault recovery.
The professor's rule on spilling to disk (exam-favourite): Do not spill to disk unless (a) the functions that computed the dataset are expensive, or (b) they filter a large amount of data. Reason: if a spilled (disk-resident) partition is lost, it is recomputed — and a partition that spilled to disk was already failing to fit in memory, so recomputation may thrash again; meanwhile, with expensive functions or heavy filters, recomputation costs real work — all the expensive functions run again, or the large filtration happens again. Spilling trades memory for I/O; use it only when recomputation is worse.
Exam note: storage levels and the recommended choice process are classic exam material — know which level is space-efficient vs CPU-efficient and when to spill to disk. Answers should be crisp and straightforward, giving all the reasoning, not large story-writing answers.
9.12.4 Fault Tolerance and Replication
All the storage levels provide full fault tolerance by recomputing the lost data. The replicated ones let you continue running the task on the RDD without waiting to recompute a lost partition. If you have replicated an RDD across multiple nodes, even if a partition is lost, the task — or the transformation — can be shifted to the node where the copy still exists.
So there are two tiers of protection:
- Recomputation (available everywhere): any lost partition is rebuilt from its lineage. This costs time — the DAG path to that partition must re-run.
- Replication (replicated levels only): a second copy lives on another node, so a lost partition is served from the copy instantly, and tasks simply move to the node holding the replica. You pay extra memory for the copy; you gain speed of recovery.
9.12.5 Eviction and Manual Removal
Spark automatically monitors the cache usage on each node and drops out the data partitions in a least recently used (LRU) fashion: the dataset or RDD which was used least recently is dropped; the most recently used stays in memory.
The professor's intuition — LRU and spatial locality: The eviction policy maintains the concept of spatial locality: if you have used this data at this moment, you tend to use this data and nearby data. A dataset you used a while ago — from the time frame you are not using it, you are working in another region — becomes least recently used. When any new dataset comes in, the LRU one will be removed and the new dataset will be placed. Think of a desk: the papers you are actively working with stay out; the ones from last week get filed; when something new arrives, the file cabinet swallows whatever you have touched least recently.
You also have the option to manually remove an RDD instead of waiting for it to fall out of the cache: the unpersist() method is there, which you can use to drop an RDD from the main memory. This is useful when you know an RDD is finished — freeing memory immediately for the next dataset rather than waiting for LRU pressure.
Pitfalls to avoid:
- Persisting everything. Memory is finite; over-persisting triggers LRU eviction of exactly the RDDs you wanted kept. Persist only datasets reused by multiple actions or stages.
- Using MEMORY_ONLY for data that does not fit. The RDD will not fail — it will simply be recomputed on every reuse, which is usually the worst of both worlds. Switch to MEMORY_ONLY_SER or the AND_DISK levels.
- Ignoring serialization choice. MEMORY_ONLY_SER with default Java serialization can be disappointingly large and slow; a fast serializer (Kryo) is the intended companion.
- Confusing eviction with deletion. LRU eviction only removes partitions from memory — the RDD's lineage remains, so evicted data is recomputable, whereas unpersist() explicitly drops the RDD from the cache.
Recap + bridge: Persisting an RDD stores its computed partitions in memory for reuse; storage levels trade memory space against CPU (MEMORY_ONLY fastest, MEMORY_ONLY_SER more compact, MEMORY_AND_DISK and MEMORY_AND_DISK_SER spill to disk); choose MEMORY_ONLY if it fits, then MEMORY_ONLY_SER with a fast serializer, and spill to disk only when recomputation is expensive or filters are heavy; replication gives fast fault recovery, and the cache evicts least-recently-used partitions with unpersist() as the manual option. This closes the loop with Section 9.4: persistence is what makes the DAG's fast-recovery promise practical.
Exam Guidance Summary
Weightage
EC1 is 30 percent, EC2 is 30 percent, and EC3 — the comprehensive exam — is 40 percent weightage, the highest out of all. The EC1 components — the quizzes and assignments — are evaluated and frozen now. Assignment 2 carries 15 percent weightage, and the marks will be scaled.
Focus Areas
- Overall focus: the comprehensive exam could be similar to the mid-semester exam, but the focus would be more on the part after the mid-semester — more focus on the NoSQL stores and writing certain queries.
- NoSQL: the focus is on all the NoSQL stores covered, starting from MongoDB, HBase, and Cassandra. Dynamo was not covered in detail; whatever was covered related to it will be based on that.
- Queries: you can get a question where you have to write queries. The queries could range from all the NoSQL data stores. Also, given a use case, you may have to write map and reduce programs for it — the similar thing which we covered in the mid-semester. The things will not be very complex.
- Pig: maybe you might be asked to write Pig queries — Pig Latin — for a particular thing, or how Pig runs queries. You do not have to execute anything. Do not use any of the execution environments — not the remote labs, not something installed on your laptop — and do not paste a screenshot. Everything should be typed in the answer text box: you write the query, and that is what will be seen.
- Spark: Spark is a major module — like Hadoop and MapReduce was a major one — and similarly, quite a good portion of the comprehensive exam would be from Spark. But it is not going to be very tough: simple things, whatever we have covered, will be there.
- Earlier material: whatever is there for reliability, availability, Hadoop, and MapReduce will still be a part of the comprehensive exam. But the larger focus is on the post-mid-semester part.
- Architecture: if there is something on the architecture level, whatever architecture we have covered is the focus. From the range of databases, look into what was covered — everything may not have been covered. The labs covered were MongoDB, MapReduce, and Spark only; the Pig lab part was not covered. Study whatever is covered in the class and pay attention to that.
Answer Style and Instructions
- Answer style: answers should not be very, very large, story-writing kinds of things. Straightforward answers should be there, as crisp as possible, giving all the reasoning. Sometimes what I have found is: if the students know one word — they have heard about that word — they will write two pages about that. That is not the expectation.
- Instructions: read the instructions carefully and then follow them.
Q: Can we get a question where we have to write queries?
A: Yes, you can get a question where you have to write queries. The queries could range from all the NoSQL data stores. Also, given a use case, how will you write map and reduce programs for it — again, the similar thing which we covered in the mid-semester. Also maybe certain things like: how will you do this particular thing in this particular NoSQL data store. But the things will not be very complex.
Key Industry Applications
- Buffering message buses: the data flow tier's impedance-mismatch buffering and its at-least-once guarantee are exactly the role industry message-queue systems like Kafka play between producers and consumers, absorbing sale-day spikes on e-commerce sites without losing events.
- Edge computing and IoT: edge servers collecting data from IoT devices and pushing it deeper into backend cloud systems is the real deployment pattern for the collection tier.
- Log analysis at scale: the original use case for streaming collection was website log analysis to detect errors and deviations, with the format evolving from the W3C standard log format to JSON, Avro, and Thrift.
- Dashboards and alerting: delivery tiers in industry ship continuously updated dashboards (HTML5, WebSockets), alert on error keywords in streaming logs, export reports as PDF or SVG, and feed downstream applications.
- NoSQL for permanent storage: MongoDB and Cassandra are the popular choices for persisting streaming outcomes for later historical analysis; the choice depends on the use case — consistency, wide-columnar data, or JSON documents.
- Streaming engines in practice: Apache Storm and Spark Streaming for heavy processing, Kafka Streaming for lightweight computations, with ingestion from Kafka, Flume, Kinesis, S3, HDFS, and Twitter streams.
- Machine learning on live streams: k-means and other ML library functions applied to stream RDDs — for example, labeling and grouping records as they arrive.
- Fraud detection and recommendations: streaming engines in production run fraud detection on online transactions, click analysis for online recommendations, push notifications for location-based retail offers, and emergency-service alerts on abnormal measurements in healthcare — all following the architecture of this lecture: collect, buffer, process, store, deliver.
- Real-time sentiment and market analysis: real-time feeds from social media and stock exchanges are used for sentiment analysis and future price prediction, with tweets pulled in real time, processed, and loaded into persistent storage.
BDS Lecture 9 notes
Sections Breakdown
The four layers of a streaming architecture, the feedback loop, and each layer's role.
The traditional continuous operator model: source, continuous, and sync operators at record-level granularity.
Fast failure and straggler recovery, load balancing, and unifying streaming with batch and interactive workloads.
How Spark answers the three challenges: RDD lineage, dynamic scheduling, one unified engine, and native libraries.
Spark Streaming and discretized streams: DStreams as sequences of RDDs from batched input.
Dynamic scheduling, coarse-grain failure recovery, and interoperability of batch, stream, and interactive analysis.
Basic and advanced sources, reliable and unreliable receivers, and delivery semantics.
DStream transformations and output operations that mirror batch RDD operations.
Window length and slide interval, window-level transformations, and moving-average-style metrics.
Structured Streaming and the unbounded table: continuous DataFrames queried with SQL.
The shuffle as a costly all-to-all wide transformation and DAG stage optimization.
RDD persistence, storage levels, choosing a level, replication, and cache eviction.
Exam weightage, focus areas, and answer style guidance.
Real-world streaming applications of the architecture in industry.
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.
Streaming Data Architecture: The Four Layers
Must-know: Four layers: collection (edge servers, TCP/IP/HTTP, W3C/JSON/Avro/Thrift), data flow (buffer, impedance mismatch, at-least-once), processing (locality of reference: move code to data; YARN-style partitioning and scheduling), storage (in-memory or NoSQL by use case), delivery (dashboards, WebSockets/HTML5, alerts, PDF/SVG). Feedback loop supports iterative analytics.
⚠️ Top pitfall: Omitting the data flow buffer causes data loss during spikes; one database cannot fit all use cases; forgetting the feedback loop breaks iterative analytics.
Self-check: Why must code move to the data rather than the data to the code?
Connects to: 9.2
The Continuous Operator Model
Must-know: Continuous operator model: source operator sprays records; continuous operators on worker nodes process one record at a time (fine-grained, record-level granularity); sync operator collects results and sends to downstream systems (HBase, Cassandra). Operators are long-running processes — processing never stops.
⚠️ Top pitfall: Thinking the model batches records — it does not; each record is processed individually, which is what makes failure recovery and load balancing hard.
Self-check: Which operator sprays records over the cluster, and which operator collects the processed records?
Connects to: 9.3
Challenges with the Continuous Operator Model
Must-know: Three challenges: (1) fast failure and straggler recovery — a straggler is a slow node that bottlenecks the pipeline; static allocation of operators makes recovery hard; (2) load balancing — no dynamic adaptability, uneven load causes bottlenecks; (3) unification of streaming, batch, and interactive workloads — the most important; fixed operator topologies cannot answer ad hoc queries.
⚠️ Top pitfall: Treating stragglers as an infrastructure-only concern — a node with heavier load becomes the bottleneck even when all nodes function; recovery must be automatic and fast because stream records are transient.
Self-check: Why is interactive query processing difficult in a continuous operator system?
Connects to: 9.4, 9.10
How Spark Meets the Challenges
Must-know: Spark's three answers: (1) RDDs + DAGs — lineage lets Spark reconstruct any lost partition by recomputing only that partition; (2) dynamic resource allocation and load balancing — short-lived tasks are scheduled by utilization, work moves off overloaded nodes; (3) one unified engine for batch, streaming, interactive workloads, with native ML (MLlib), graph (GraphX), and SQL libraries.
⚠️ Top pitfall: Confusing recomputation with duplication — Spark does not store every intermediate; it recomputes a lost partition from its lineage in the DAG.
Self-check: How does Spark recover a lost RDD partition, and why is this fast?
Connects to: 9.3, 9.5
Spark Streaming and Discretized Streams
Must-know: DStream = discretized stream = sequence of RDDs; the live stream is divided into batches (each batch is one RDD, each RDD may hold many records). Sources include Kafka, Flume, Kinesis, S3, HDFS, Twitter. High-level ops: map, reduceByKey, count (action), filter, window. Outputs: file systems, NoSQL stores, dashboards. Coarse-grained vs record-level.
⚠️ Top pitfall: Thinking a DStream processes records one at a time — it is coarse-grained: operations apply per batch RDD.
Self-check: Internally, what is a DStream represented as?
Connects to: 9.6, 9.9
The Benefits of Batching
Must-know: Three benefits of batching: (1) dynamic scheduling — scheduler watches resource usage and shifts tasks off overloaded nodes rather than letting them become stragglers; (2) faster failure recovery at coarse grain — relaunch failed tasks, recompute lost partitions using multiple nodes; (3) interoperability — join DStream with static RDD, convert to DataFrame, query with SQL, apply ML (e.g., k-means to label streaming records).
⚠️ Top pitfall: Confusing dynamic scheduling with static allocation — static allocation pins work to nodes and causes straggler bottlenecks; dynamic scheduling reassigns tasks each batch.
Self-check: What does the scheduler do when one node gets overloaded?
Connects to: 9.3, 9.10
Spark Streaming Sources and Receivers
Must-know: Every input DStream has a receiver: it receives records, prepares the batch of RDDs, and sends it; partitioning and scheduling are framework responsibilities. Basic sources: file system, socket connections, StreamingContext API. Advanced sources: Kafka, Flume, Kinesis (extra utility classes + dependencies). Reliable receiver: acks source when data received and stored with replication; unreliable receiver: no ack. Semantics: at-least-once, at-most-once, exactly-once.
⚠️ Top pitfall: Thinking nodes pull data themselves — the receiver pulls, prepares the batch, and hands it to the cluster; the framework partitions it.
Self-check: What is the difference between a reliable and an unreliable receiver?
Connects to: 9.1, 9.8
Transformations and Output Operations on DStreams
Must-know: DStream transformations mirror RDD transformations: map, filter, union, count, reduce, countByValue, reduceByKey; stream-specific: transform (arbitrary RDD-to-RDD function per batch), updateStateByKey (state per key across batches). Output operations: print (first ten elements), saveAsTextFiles, saveAsObjectFiles, saveAsHadoopFiles, foreachRDD (push each RDD to external system).
⚠️ Top pitfall: Treating count() (elements per RDD) as the same as countByValue() (frequency of each value) — they answer different questions.
Self-check: Which output operation applies an arbitrary function to each RDD of the stream for pushing data to external systems?
Connects to: 9.9, 9.11
Window Operations
Must-know: Window W_t = {R_t, R_{t+1}, R_{t+2}} spans three consecutive RDD batches; sliding by two gives W_{t+2} = {R_{t+2}, R_{t+3}, R_{t+4}} (windows overlap). Window length = history; slide = how often to recompute; both configurable for moving-average-style metrics. Window transformations: countByWindow, reduceByWindow, reduceByKeyAndWindow, countByValueAndWindow.
\[W_t = \{R_t, R_{t+1}, R_{t+2}\},\quad W_{t+2} = \{R_{t+2}, R_{t+3}, R_{t+4}\}\]
⚠️ Top pitfall: Confusing window length with slide — length is how much history, slide is how often the window moves.
Self-check: If a window of three RDDs slides by two, which batches does the next window contain?
Connects to: 9.10
Structured Streaming: The Unbounded Table
Must-know: Structured streaming: stream → DataFrame continuously; rows appended in structured format; becomes an unbounded table (no fixed end, no manual batching) queryable with SQL. Upper bound: main memory size — the table can only hold what memory can hold.
⚠️ Top pitfall: Assuming the unbounded table has no size limit — it is bounded by main memory.
Self-check: Is there any upper bound on the unbounded table?
Connects to: 9.3, 9.12
Shuffles: The Costly Wide Transformation
Must-know: Shuffle = all-to-all redistribution: data copied across executors/machines, grouped differently across partitions; very costly. reduceByKey: (1,2),(3,6),(3,4) → group per key, add values (6+4=10) → (1,2),(3,10). Triggers: repartition, reduceByKey, groupByKey, join. Optimization: DAG stages — parallel transformations in one stage, shuffle forces a stage boundary and is postponed if possible.
\[(1, 2),\ (3, 6),\ (3, 4) \ \Rightarrow \ (1, 2),\ (3, 10)\]
⚠️ Top pitfall: Using groupByKey instead of reduceByKey — groupByKey shuffles all values per key; reduceByKey combines locally (combiner) before shuffling, moving much less data.
Self-check: Which operations trigger a shuffle, and why is the shuffle costly?
Connects to: 9.12
RDD Persistence and Storage Levels
Must-know: persist()/cache() mark an RDD to be stored when first computed; cache() = default level MEMORY_ONLY. Levels: MEMORY_ONLY (fastest, deserialized in memory), MEMORY_ONLY_SER (serialized, space-efficient, CPU-heavy), MEMORY_AND_DISK (deserialized, spill to disk), MEMORY_AND_DISK_SER (serialized + spill). Do not spill unless functions are expensive or filter large data. Replicated levels = fast fault recovery. LRU eviction + unpersist().
⚠️ Top pitfall: Spilling to disk unnecessarily — recomputation of lost spilled partitions re-runs expensive functions; spill only when functions are expensive or filter a large amount of data.
Self-check: Which storage level is cache() shorthand for, and when should you spill to disk?
Connects to: 9.4, 9.10
Exam Guidance Summary
Must-know: EC1 30%, EC2 30%, EC3 comprehensive 40% (highest). Focus: post-mid-semester NoSQL stores and queries; Pig queries typed in the answer box (no execution environment, no screenshots); map/reduce programs for use cases (not very complex); Spark is a major module but not tough; reliability/availability/Hadoop/MapReduce still included. Crisp answers with reasoning, not story-writing.
⚠️ Top pitfall: Writing long story-style answers — the expectation is crisp, straightforward answers with all the reasoning.
Self-check: Which exam carries the highest weightage, and where does the comprehensive exam focus?
Key Industry Applications
Must-know: Industry applications: Kafka buffers (impedance mismatch, at-least-once); edge servers collect IoT data into the cloud; log analysis from W3C format to JSON/Avro/Thrift; dashboards (HTML5, WebSockets) and alerting on error keywords; MongoDB/Cassandra for permanent storage by use case; Storm/Spark Streaming for heavy processing, Kafka Streaming for lightweight; k-means and ML on stream RDDs; fraud detection, recommendations, sentiment analysis, stock prediction.
⚠️ Top pitfall: None specific — application-level content.
Self-check: What role does Kafka play between producers and consumers in a streaming architecture?