Everything in Chapters 3 to 9 was batch analysis: complex computations over large amounts of stored data, whose results arrive after seconds, minutes or hours. Big data is not just that. It is also about analyzing data streams: simpler computations over smaller sets of continuously incoming data, with results produced near real-time.
Streaming data processing is a big deal in big data for two business reasons:
Despite this business-driven surge of interest, batch systems are generally more mature than their streaming brethren — which has resulted in a lot of active development in recent times. Keep that asymmetry in mind: most of the theory in this chapter is old (the algorithms date to the 1970s–2000s), while the engineering (the tools in sections 15 to 19) is still being stabilized.
A system for data streaming is a type of data processing engine that is designed with infinite datasets in mind. The emphasis is on designed for: batch engines can be (and have been) used to process infinite datasets, and streaming engines can be (and have been) used to process finite datasets. The distinction is about what the engine assumes about its input, not about the input itself.
| Domain | What streams, and why |
|---|---|
| Operational monitoring | Performance tracking of physical systems: processors’ temperature, fans’ speed. |
| Web analytics | Tracking activities on websites to provide real-time recommendations. |
| Online advertising | Real-time bidding amongst advertising agencies to decide which ad to show. |
| Social media | In 2013 Twitter reached spikes of 150,000 tweets/second; in 2023 X averaged 360,000 tweets/second. |
| Internet of Things | Mobile phones, wearables, health-monitoring systems, sensors, smart-*. |
The lecture fixes a notation before anything else. Consider:
What an item says about its element defines the data stream model. The three classical models differ in the relationship between consecutive items of the same element:
| Model | it represents… | Example |
|---|---|---|
| Time series | the new state of em at time t | stock prices, weather data |
| Cash register | an increment of em | packages sent to (received by) IP addresses, uptime of a device |
| Turnstile | an update of em (can be negative) | delta of people entering/exiting a subway station |
Separately, real-time systems are classified by their tolerance for delays. The three categories come from the embedded-systems vocabulary, with latencies measured on very different scales:
| Class | Examples | Latency measured in | Tolerance for delay |
|---|---|---|---|
| Hard | Pacemaker, anti-lock brakes, airplane sensors | µ-sec ~ m-sec | None — total system failure, potential loss of life |
| Firm | Booking system, online stock quotes | m-sec ~ sec | Low — but the result is useless if it misses a deadline |
| Soft | Interaction with applications, weather monitoring | sec ~ min | High — results are useful even if they are late |
Two classification axes, each with three names. Models: time series (item = new state), cash register (item = increment), turnstile (item = update, possibly negative). Real-time classes: hard (no tolerance, loss of life), firm (low tolerance, deadline-bound value), soft (high tolerance, still useful late). The anchoring facts: most data streaming applications fall in the soft category, and the firm and soft categories are sometimes referred to together as near real-time.
Three properties separate streaming from everything the previous chapters did. They are the constraints every design in this chapter must obey, so they are worth stating as a checklist:
Batch processing (Chapters 4–9) could always afford to re-read stored data: more passes, exact answers, unbounded working memory if the cluster had it. Streaming gives up all three. One pass, bounded memory, and approximate answers are not defects of a particular implementation — they are the definition of the problem. The algorithms of sections 10 to 14 exist precisely to make these constraints bearable.
A streaming system is a pipeline of tiers, from the data producer to the data consumer. The lecture names six components plus two storage tiers:
Each tier has a single responsibility:
The collection and access components are typically made of edge servers that either receive data from external sources or expose the analyzed data to the consumer — and, possibly, the producers and consumers are other streaming pipelines, which is how streaming systems compose into larger ones. The collection tier has three characteristics worth knowing:
The message queueing tier handles the transportation and exchange of data between tiers. Why dedicate an entire tier to moving data? Three reasons, all from the lecture:
The lecture compresses the evolution of message queueing into three generations, and the progression is the chapter’s thesis in miniature:
| Generation | Examples | Character |
|---|---|---|
| Naïve systems | ad hoc communication systems between specific tiers | Too burdensome. |
| Centralized log-based (2000s) | Apache ActiveMQ, RabbitMQ | Data stored in log files and sent in batches; enables complex message routing patterns. Slow but reliable. |
| Distributed event-based (early 2010s) | Apache Kafka, Apache Flume | Data sent in mini-batches; built to be distributable and support scaling; tunable level of delivery semantics; limited ordering semantics. |
The core concepts are the producer and consumer of data within the architecture, and the broker, which manages one or more queues of data. The broker is usually distributed on multiple nodes. Queues are identified by a topic (i.e., a type of message) and usually partitioned; when consumers are ready, they check the queues for new data.
Topics are the naming device that keeps unrelated traffic apart, and the lecture lists three uses for them:
Message queues must be durable, for two reasons:
| Semantics | Meaning | Where it is used / cost |
|---|---|---|
| Exactly once | A message is never lost and is read once and only once. | Required where data means money (financial/ad systems). Performance is sacrificed to provide safety mechanisms. |
| At most once | A message may get lost, but it will never be read twice. | Allowed where not all data is required (monitoring systems, down-sampling). Fast and trivial. |
| At least once | A message will never be lost, but it may be read twice. | Balances the two; easier to provide than exactly once. The consumer can still check for duplicates to adopt exactly once. |
Beware: guarantees depend on the chosen tools + application logic on the whole pipeline. A broker that promises at-least-once delivery does not stop your analysis tier from losing messages it failed to persist, nor does it stop your consumer from processing the same offset twice after a crash. The semantic is a property of the entire chain, not of the queue alone.
Pick a delivery semantic and see what the system is allowed to do — loss, duplicates, or neither.
The analysis tier is the heart of the architecture and handles the analysis of the data. Its three key features:
A continuous query is a query that is issued once and then is continuously executed against the data — in contrast, traditional queries are simply executed once when issued. A continuous query may need to maintain a state: an intermediate result that is continuously updated by the query. A query is stateless if each execution is independent from the others.
What makes continuous queries different from traditional ones is two constraints that batch queries never face:
Define a continuous query as issued once, continuously executed, and define its state as an intermediate result continuously updated by the query. Then give the two constraints that distinguish it from a traditional query, with the vocabulary attached: one-pass algorithms and little room for state (memory); load shedding and concept drift (time).
The distributed execution of the analysis is an architecture common to different frameworks. Two aspects matter:
State on disk is slower but survives a crash; state in memory is fast but dies with the worker. Replication burns machines so that a failure costs nothing; rollback recovery saves resources but pays a recovery cost when failure actually happens. Both choices are the same trade at different levels: spend resources in advance to make failure cheap, or spend them after failure to make success cheap. Streaming systems pick per state and per latency budget.
Windowing techniques allow analyses on a per-window basis instead of a simple per-item basis. In general, windows are defined by two parameters:
Sliding windows define both length and period in terms of stream time: items are assigned to windows based on the time at which they are collected. Depending on the comparison of length and period, three specialized versions appear:
| Variant | Condition | Example |
|---|---|---|
| Fixed windows | length = period | analyze the last 5 minutes of data every 5 minutes |
| Tumbling windows | special case of fixed, with length and period expressed in number of items | analyze every 100 messages |
| Overlapping windows | length > period | analyze the last 5 minutes of data every 2 minutes |
| Sampling windows | length < period | analyze the last 2 minutes of data every 5 minutes |
Set length, period and the current time on a stream of 12 items, and watch the window — and its classification — change.
“Eviction policy” and “trigger policy” are the same pair of knobs as “length” and “period” — eviction decides what leaves the window, trigger decides when the analysis runs. The vocabulary matters because Spark Streaming (section 18) exposes exactly these two knobs as windowLength and slideInterval.
Data-driven windows define the length in terms of the content that comes with the data: items are assigned to windows based on the values of some fields or attributes in the item itself. In this case, the period determines the update interval rather than the window extent.
The typical use case is sessions:
Sessions bring two problems, and the lecture is careful to attribute each one correctly:
Ideally, an event is processed exactly as it happens. In reality, there is a (potentially significant) difference between the time at which an event occurs and the time at which it enters the streaming system:
The time difference is called skew, and it may be due to hardware issues (network congestions or partitions) or software issues (contention, distributed system logic).
This sentence from the lecture is a trap and a key at once. A sliding window that assigns items by collection time is a pure function of the system clock. The moment you assign items by the time stamp they carry, the window’s membership is determined by the data, not by the clock — which is precisely the definition of a data-driven window. Keep that equivalence in mind for section 9.
Pros:
Cons:
Windowing by event time is sometimes referred to as the gold standard of windowing — the reference use case is analyzing user behavior on websites, where the business meaning lives in the event’s own time, not in the server’s. But it is expensive:
Systems that do support it must therefore define four things: watermarks, triggers, allowed lateness, and an accumulation strategy.
The watermark captures the progress of event-time completeness as processing time progresses. In practice, it is the amount of time that you need (or want) to wait to receive the data produced in a given time. Two kinds:
Triggers declare intervals of intermediate processing of windows. Each partial result determines a pane of the window. Triggers can be based on:
Since late data is realistic, a policy for allowed lateness must be defined — e.g., allow data that is 5 minutes late w.r.t. the watermark, and discard everything else. The higher the tolerance, the longer data must be buffered.
The accumulation strategy defines how intermediate results must be aggregated:
| Strategy | New values… | Useful when |
|---|---|---|
| Discarding | are independent of the previous ones | the consumer expects deltas and computes the aggregations itself |
| Accumulating | aggregate the previous ones | the consumer simply replaces old values with new ones |
| Accumulating and retracting | contain both the total and the delta | the consumer needs both to correctly re-aggregate the data |
The watermark determines the time frame in which triggers are active (if the trigger is not based on the watermark, the analysis determines intermediate results that are accumulated according to some strategy). After the watermark, window results are kept to allow late data — allowed lateness is a sort of ultimatum; most data is assumed collected within the watermark; a final computation determines the final result with the same accumulation strategy as before. After that, late data is discarded. And the lecture closes the loop: the same considerations for event-time windowing can be made for data-driven windowing in general.
Consider n, the space of events captured by the data stream. Streaming algorithms have the following requirements:
For instance, take ε = 0.02 and δ = 0.01: then you have a 99% probability that the obtained result R′ equals the real result ± 2%.
What can and cannot be done in one pass? The lecture gives two lists, and the boundary between them is examinable:
| Solvable by one-pass algorithms | Not solvable by one-pass algorithms |
|---|---|
|
|
Analyzing a data stream is challenging due to space constraints, time constraints, and algorithmic unfeasibility in one pass. One solution is to rely on approximated algorithms, which achieve approximation through two main concepts:
| Concept | Idea | Known results | Notes |
|---|---|---|---|
| Sampling | Keep a (random) subset of the stream and answer from it. Methods: distinct sampling, quantile sampling, reservoir sampling. | On time series and cash register models: number of distinct items, quantiles, frequent items. | Some systems (e.g., IP packet sniffers) already sample. But sampling is not a powerful primitive for many problems — too many samples needed for sophisticated analyses — and it is more challenging in the turnstile model. |
| Random projections | Dimensionality reduction by projecting along random vectors into a lower-dimensional space that approximately preserves distances between points. The projections are called sketches. | On (also) turnstile models: distinct elements, quantiles, most frequent items, wavelets, histograms, random subset sums, counting sketches, Bloom filters. | Robust where sampling struggles: the turnstile model, with negative updates, is where sketch families earn their keep. |
The rest of this half of the lecture is one case study per problem: reservoir sampling for the sampling side, and three sketches — HyperLogLog (distinct counting), Bloom filters (membership) and Count-Min Sketch (frequency) — for the random-projection side. All four share the same shape: hash the input, keep a tiny bounded summary, and answer with a probabilistic bound instead of an exact count.
Random sampling is a common technique in data streaming. The lecture’s use case is YouTube: 5 million videos are watched every minute (as of 2019), and the goal is a real-time statistical analysis of the watched videos. On the surface that seems pretty easy — but the data moves fast, never stops, and doesn’t fit into memory. A viable solution is to sample the stream as it is flowing. Two questions follow:
The classical answer is reservoir sampling (J.S. Vitter, 1985, “Random sampling with a reservoir”, ACM Transactions on Mathematical Software 11(1), 37–57), based on the notion of a reservoir: we hold a predetermined number of stream values. When a new value arrives, we probabilistically determine whether to add it to our collection (replacing an occupied slot) or to discard it.
In the lecture’s example with r = 15, the 16th item is added with P = 15/16: a random number α between 0 and 1 is generated; if α > 15/16 the item is discarded, if α ≤ 15/16 it is added and a random resident is evicted. The probability calculus checks out — the lecture works it for the 1st element across rounds: its probability of being discarded at step 16 is 1/16, and at step 17 it becomes 2/17 = 1/16 + (15/16)·(15/17)·(1/15), where the second term covers “was kept, the 17th item was chosen for insertion, and the 1st was chosen for removal”.
Step through a stream of 12 items with a reservoir of size r. Each item is accepted with probability r/i; when accepted, a random resident is evicted.
Consider xi the ith element and xo an older element. For i ≤ r, P(xi to be put in r) = 1. For i > r:
P(xi to be put in r) = r/i
P(xo to be already in r) = r/(i-1)
P(xo to be kept in r) =
P(xo already in) * ( P(xi not put) + P(xi put) * P(someone else evicted) )
= r/(i-1) * ( (1 - r/i) + (r/i) * ((r-1)/r) )
= r/(i-1) * (i-1)/i
= r/i
State the invariant: at any time, each item has the same probability of being in the reservoir — the algebra above shows it stays exactly r/i for every item of every age. Then be ready to quote the rule: first r items enter unconditionally; the ith item enters with probability r/i; eviction among residents is uniform. That is the whole algorithm, and it needs O(r) memory regardless of stream length.
The count distinct problem, with the lecture’s use case: Facebook wants to know how many users are connected to my website? from a connections table with (ConnID, Device, UserID) rows.
select count(distinct UserID) from tab_conn; — O(n) space and O(n) query complexity.Both are O(n) in space, and n is billions. The general requirement of both approaches to the count-distinct problem: use a hash function H to translate input items to a uniform distribution in some bounded domain, and study the hash values without storing them.
| Category | Idea | Examples |
|---|---|---|
| Bit-pattern-based (most common) | Observe patterns of bits in H(i) | LogLog, HyperLogLog, HyperLogLog++ |
| Order-statistics-based | Evaluate order statistics — i.e., the minimum value(s) in an ordered distribution. E.g., if min(H(i)) = x with x ∈ [0,1], then card ≈ 1/x: only the minimum must be stored | MinCount, Bar-Yossef |
HyperLogLog (Flajolet, Fusy, Gandouet & Meunier, 2007, “Hyperloglog: the analysis of a near-optimal cardinality estimation algorithm”, Discrete Mathematics and Theoretical Computer Science proceedings) rests on one observation: the cardinality of a set of uniformly distributed random numbers can be estimated by calculating the maximum number of leading (or trailing) zeros in the binary representation of each number in the set. Uniformly distributed random numbers are obtained from the input elements by applying a well-studied hash function.
Why leading zeros work: given a binary string, the probability of observing 1 initial zero is 1/2 (50%), 2 initial zeros is 1/4 (25%), 3 initial zeros is 1/8 (12.5%), and n initial zeros is 1/2n. The number of attempts required on average to generate a binary string with n trailing zeros is 2n. So if the maximum number of trailing zeros ever observed is n, it must have taken approximately 2n attempts.
The space requirement falls out of the arithmetic. With 32-bit hashes: the maximum number of initial zeros is 32 = log2(232), and the number of bits required to store “32” is 5 = log2(32) = log2(log2(232)). Hence O(log log n) space.
With only 4 hashes 11000, 00000, 10011, 10100, the maximum number of initial zeros is 5, so the estimate is 25 = 32 — but there are only 4 elements. The risk of a wrong estimate is high on small samples. The fix is to smooth the problem by splitting into buckets and averaging: use some bits to identify a bucket B, track the maximum number of zeros per bucket, and make the estimate from the average of all maxima. This is exactly the register structure below.
The space arithmetic is the examinable core: to count distincts up to 232, each bucket needs 5 bits because 5 = log2(log2(232)); the whole structure is O(log log n). Know the error as ε = k·1.04/√m with the 68/95/99.7 confidence ladder, and the merge property that makes it a distributed primitive.
The membership problem, with the lecture’s use case: YouTube has 1 billion videos — has a specific video been viewed today?
The old answer is the Bloom filter, conceived by Burton Howard Bloom in 1970 (“Space/time trade-offs in hash coding with allowable errors”, Communications of the ACM 13(7), 422–426). The one property that makes it a streaming tool:
A Bloom filter can return false positives, but no false negatives. If it says “it hasn’t been seen”, then it hasn’t been seen. If it says “it has been seen”, it may be a false positive — the price of the tiny footprint.
A Bloom filter is an array of m bits, with m > n (n = the number of items to keep track of — the videos). Each bit is a flag: 0 = not seen, 1 = seen. If m = 10n, the space occupation is about 1.3 GB for a billion items — but the deck’s punchline is better stated as: it is 10 bits per element, nothing more, whereas storing the keys would most probably require a lot more space.
Incoming items are processed by a series of hash functions: given item i, the bit h(i) is turned to 1 for every hash function. Ultimately, the answer to “has i been seen?” is given by AND(h1(i), …, hk(i)): all k bits must be 1.
Two sizing rules follow from the desired probability of false positives p:
m = 20 bits, k = 3 hash functions. Add words, watch the bits light up, then query — and try to produce a false positive.
Two facts to reproduce exactly: false positives possible, false negatives impossible; and the sizing for a 1% false-positive rate — k ≈ 7 hash functions and m = 10n bits (10 bits per element). The contrast with the perfect-hash option (134 MB but unbuildable) is the deck’s way of justifying why an error-prone structure is the practical choice.
The frequency problem, with the lecture’s use case: YouTube has 4 billion views per day — how many times has a specific video been viewed today? Again we want a space-efficient structure, and again the answer relies on hashes and builds on the concept of the Bloom filter. The most common algorithm is the Count-Min Sketch (Cormode & Muthukrishnan, 2005, “An improved data stream summary: the count-min sketch and its applications”, Journal of Algorithms 55.1, 58–75). Its name is its strategy: you first do a series of approximated counts, then you keep the minimum of those.
Each row is a lossy counter: collisions with other items inflate a counter, but never deflate it. So every row’s value for i is ≥ the true count of i. Taking the minimum picks the row least damaged by collisions. Therefore the approximation can overcount, but never undercount — the same one-sided-error shape as the Bloom filter, now on counts instead of membership.
Count-Min Sketch provides (ε, δ)-guarantees. Given the estimated result R′, the true result R and the number of elements n:
Sizing follows: the number of hash functions d (i.e., rows) depends on δ — for δ = 1%, d = 5. The number of counters w (i.e., columns) depends on ε — for ε = 1%, w = 272. The deck’s worked numbers for n = 1 billion videos: a table for the perfect count would need 64-byte IDs plus 4-byte counters ≈ 73 GB; the sketch with ε·n = 100 (i.e., ε = 9.3×10−8, w = 29M) is 5 arrays of 29M counters of 4 bytes each → 580 MB.
d = 4 rows, w = 10 counters. Click items to add them to the stream and watch the counters — and the minimum — move.
The Count-Min Sketch can be used to answer several questions beyond the point query:
Reproduce the two guarantees exactly: R′ ≥ R and R′ ≤ R + ε·n with probability 1 − δ. Then the sizing story: rows from δ (d = 5 for δ = 1%), columns from ε (w ≈ e/ε), and the 73 GB → 580 MB contrast as the punchline. And remember the family resemblance: Bloom filter is membership with one-sided error, Count-Min is frequency with one-sided error, both built from hashes into a bounded array.
The lab deck opens with two practical questions, one per tier of Plate 10.1: which tool for the message queueing tier, and which for the analysis tier. The answers are worth knowing as a landscape, because the rest of the chapter uses two of them.
Apache Kafka — perhaps the most famous and widely adopted. Mainly focused on the publish/subscribe messaging pattern. General purpose, mature, highly interconnected.
Apache Flume — capable of handling the whole pipeline, but tightly coupled with Hadoop and with complex scalability.
The lecture’s verdict: for the message queueing tier, Kafka is the default choice — which is why section 16 is entirely about it.
Apache Storm — perhaps the most famous; supported by Twitter until 2015, when they developed (and moved to) Apache Heron, which is Storm-compatible. Applications are designed as a topology in the shape of a DAG.
Apache Spark — streaming is an extension of the core engine. Not pure streaming: it processes mini-batches (sections 17–18).
Apache Flink — unifies streaming with batch analysis, which makes it easier to deploy a Kappa architecture; supports event time and advanced windowing — the machinery of section 9 as a native feature.
Other open-source projects: Samza, Akka, Flume, Kafka Streams. Commercial solutions: Amazon Kinesis, Google Dataflow.
Message queueing, other open source: Nats, ActiveMQ, … Commercial: Amazon MQ, Google Cloud Pub/Sub, KubeMQ, …
Analysis, other open source: Samza, Akka, Flume, Kafka Streams, … Commercial: Amazon Kinesis, Google Dataflow, …
The pattern behind the names: open-source engines cluster around the broker+engine split (Kafka + a DAG or mini-batch engine), while commercial offerings sell the same split as a managed service.
Storm is a true streaming engine (one item at a time, DAG topologies), Flink is a true streaming engine that absorbs batch, and Spark Streaming is a mini-batch engine: it chops the stream into tiny batches and runs the batch machinery of Chapters 6–8 on each one. Mini-batching buys Spark all of its existing ecosystem — RDDs, Catalyst, the tuning of Chapter 8 — at the price of the per-event latency a pure engine would have. Whether that trade is acceptable is exactly the firm vs soft question of section 2.
Usually you start from a simple interprocess communication channel — e.g., an application that sends some monitoring info to be displayed on some dashboard. But it can get out of hand in no time. The solution is a publish-subscribe messaging system: it reduces the complexity of the architecture, decouples applications, and lets the broker be specialized to handle multiple queues.
The unit of data within Kafka is called a message — similar to a row or a record. A message is optionally associated to a key (messages with the same key will end up in the same place), and its content can be in any format: JSON, XML, Avro. Messages are transferred in groups, called batches: batches contain messages bound for the same destination, their size is tunable — a direct tradeoff between latency and throughput — and they are typically compressed.
Messages are categorized into topics (i.e., the queues) — similar to a database table or a file system folder. Topics are broken down into partitions:
The three semantics of section 5 map onto Kafka in a specific way:
Three Kafka facts to hold together: ordering is guaranteed only within a partition, and the key decides the partition — so ordering by key is guaranteed only per key; each partition is owned by exactly one consumer per group, which is why scaling a group means adding partitions, not sharing them; and the at-most-once recipe (disable producer retries + commit offsets before processing) versus the exactly-once recipe (Kafka Streams, or careful offset management).
Spark Streaming is an extension of the core Spark API. It divides the streaming data into mini-batches: the high-level abstraction is called a discretized stream or DStream, and a DStream is represented as a sequence of RDDs. Everything Chapter 6 taught about RDDs applies to each batch — lineage, lazy evaluation, shuffles — with one second (or whatever interval you choose) of incoming data per RDD.
The StreamingContext object is the main entry point of all Spark Streaming functionality:
import org.apache.spark.streaming._
val ssc = new StreamingContext(sc, Seconds(1))
Note the connection to section 7: mini-batches are fixed windows with length = period = 1 second. Spark Streaming does not start from per-event processing; it starts from the simplest window of all.
After a context is defined, the job is structured as follows:
streamingContext.start().streamingContext.awaitTermination(); or stop manually using streamingContext.stop().stop() on the StreamingContext also stops the SparkContext! To stop only the StreamingContext, call stop(false).Spark Streaming provides two categories of built-in streaming sources:
Each RDD in a DStream contains data from a certain fixed interval, and any operation applied on a DStream translates to operations on the underlying RDDs. DStreams support many (though not all) of the transformations available on RDDs … plus some new operations, which are the subject of the next section:
updateStateByKey — accumulate values across mini-batches;transform — apply arbitrary RDD operations;Just like an RDD, a DStream is a description: the mini-batches it will produce do not exist until an output operation forces execution. The batch interval (Seconds(1)) fixes the granularity of the whole computation — a choice made once, at context creation, and never changed afterwards. That is why the lifecycle rules above are so strict: the context is the schedule.
By default, mini-batches are independent of each other: each one is computed as if the previous ones had never happened. To accumulate values across mini-batches, Spark Streaming introduces a concept of state: updateStateByKey associates a state to every key, and for every new mini-batch the state of a key is updated according to a specified function func.
def updateFunction(newValues: Seq[Int], oldValue: Option[Int]): Option[Int] = {
// returns the new state for the key
}
val cumulativeDs = currentDs.updateStateByKey(updateFunction)
In order to maintain the state, checkpointing must be set up: a streaming application must operate 24/7, and checkpointing makes it resilient to failures unrelated to the application logic (e.g., system failures, JVM crashes). Two types of data can be checkpointed:
ssc.checkpoint("hdfs:/user/egallinucci/streaming/checkpoint")
transform is used to apply any RDD operation that is not exposed in the DStream API — e.g., sort, or join with an RDD:
val sortedDs = ds.transform({ rdd => rdd.sortByKey() })
Each mini-batch corresponds to a fixed window — the last X seconds of data every X seconds. Other kinds of windowing (sampling and overlapping, from section 7) are obtained by creating a new DStream from the existing one and specifying:
For example: the last 3 seconds of data every 2 seconds — an overlapping window. Both parameters must be multiples of the source DStream’s interval.
| Operation | Meaning |
|---|---|
window(windowLength, slideInterval) | Return a new DStream whose batches are the windows. |
countByWindow(windowLength, slideInterval) | Count the elements in each window. |
reduceByWindow(func, windowLength, slideInterval) | Reduce the elements of each window. |
reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]) | Reduce by key within each window. |
reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]) | Same, with an inverse function for incremental computation across overlapping windows. |
countByValueAndWindow(windowLength, slideInterval, [numTasks]) | Count the distinct values within each window. |
With overlapping windows, re-computing every window from scratch would re-process the overlap every time. The invFunc overload of reduceByKeyAndWindow updates the previous window’s result incrementally: add what entered, subtract (via invFunc) what left. That is the eviction/trigger pair of section 7 turned into a runtime optimization.
Output operations actually allow the transformed data to be consumed by external systems. Similarly to actions for RDDs, they trigger the actual execution of all the DStream transformations. Available operations:
print() — prints the first ten elements of every batch of data in a DStream on the driver node;saveAsTextFiles(prefix, [suffix]), saveAsObjectFiles(prefix, [suffix]), saveAsHadoopFiles(prefix, [suffix]);foreachRDD(func) — the most generic output operator: applies a function to each RDD generated from the stream; the function func is executed in the driver process.Three rules to keep separate: window length and slideInterval must both be multiples of the source DStream’s interval (the mini-batch is the atomic unit); print() shows the first ten elements of every batch, on the driver; and foreachRDD runs its function on the driver — the place to write out to external systems, not to do per-record work. Alongside those: state requires updateStateByKey and checkpointing, with metadata checkpointing for driver recovery and data checkpointing for stateful operations.
Spark 2 introduced Structured Streaming to work with DataFrames and Datasets in the streaming scenario — the SQL machinery of Chapter 7 applied to streams. Its conceptual move is the unbounded table: every record from the stream is appended to a table, and the streaming query is a normal relational query over that ever-growing table.
Because the table never finishes, the query must declare how its results leave the system. Structured Streaming defines three output modes:
| Mode | Behavior |
|---|---|
| Append | The table is emptied after each trigger: only rows that can never change are emitted, once. |
| Complete | The table is never emptied and the query is re-run against the whole table every time. |
| Update | The table is never emptied, but the query runs only on new records — rows whose value changed are re-emitted. |
The same four triggers, the same running count by key — watch which rows each mode emits.
Con: long-running aggregations may easily fill the state. A Complete query keeps the whole result table alive forever and re-scans it on every trigger; an Update query keeps the state of every key even when only a few keys change. The unbounded table is unbounded in both directions — input and state.
Pro: enables event-time windowing with watermark. Section 9’s four requirements — watermarks, triggers, allowed lateness, accumulation strategy — are exactly what Structured Streaming exposes as first-class API concepts for the gold standard of event-time windows. The lecture’s theory (sections 7–9) and the lab’s engineering (sections 17–19) converge: the tools finally do natively what the theory demanded.
A system for data streaming is a type of data processing engine designed with infinite datasets in mind. The caveat is symmetrical: batch engines can be (and have been) used to process infinite datasets, and streaming engines can be (and have been) used to process finite datasets. The distinction is about what the engine is designed for, not about what it can be pointed at.
Latency — businesses crave ever more timely data, and switching to streaming is a good way to achieve lower latency. Workload balancing — processing data as it arrives spreads workloads out more evenly over time, yielding more consistent and predictable consumption of resources. Note also that batch systems are generally more mature than their streaming brethren, which is why streaming is under active development.
Given the stream S = […, it−1, it, it+1, …] of items describing elements in E: time series — it represents the new state of em at time t (stock prices, weather data); cash register — it represents an increment of em (packages sent to IP addresses, device uptime); turnstile — it represents an update of em (delta of people entering/exiting a subway station).
Hard (pacemaker, anti-lock brakes, airplane sensors; µ-sec ~ m-sec): no tolerance — total system failure, potential loss of life. Firm (booking system, online stock quotes; m-sec ~ sec): low tolerance — the result is useless if it misses a deadline. Soft (interaction with applications, weather monitoring; sec ~ min): high tolerance — results are useful even if late. Most data streaming applications fall in the soft category, and firm + soft are sometimes referred to as near real-time.
Infinite dataset — data is always being generated, with no control over the order of arrival. Infinite computation — the system must be always on and able to keep up with the data, with a plan to avoid overflowing (e.g., auto-scalability). Low-latency, approximate and/or speculative results — data can usually be processed a single time (one pass), only a fraction of the dataset can be kept in memory, and approximation may be required to accommodate the low-latency requirement.
Collection — receives and collects data. Message queueing — handles the exchange of data items between tiers. Analysis — runs algorithms on the data (the heart of the architecture). Data access — makes the data available (dashboards, API, etc.). In-memory storage — supports the analysis tier. Long-term storage — keeps the data for future batch analysis. Not all components are mandatory, but all of them have a serious reason to exist; collection and access are typically edge servers, and producers/consumers may themselves be other streaming pipelines.
Decoupling — by decoupling the pipeline of operations (collection, analysis, data access), each node in the cluster does one job only. Safe communication — message queueing provides a solid framework for safe communication between nodes; if the analysis tier fails, data would otherwise get lost. Funneling — it handles the funneling of n data streams to m consumers: the collection tier sends data to the queue, independently of how many analysis tiers want to access it.
Naïve systems — ad hoc communication systems between specific tiers; too burdensome. Centralized log-based systems (2000s) — e.g., Apache ActiveMQ, RabbitMQ: data stored in log files and sent in batches; complex message routing; slow but reliable. Distributed event-based systems (early 2010s) — e.g., Apache Kafka, Apache Flume: data sent in mini-batches; built to be distributable and support scaling; tunable level of delivery semantics; limited ordering semantics.
Queues are identified by a topic (a type of message) and usually partitioned. Topics distinguish different sources (Sensor-A, Sensor-B), different types of data items (Order, Payments, Inventory-update) or different statuses (Raw, Harbor, Access). Partitions provide scalability and redundancy; in Kafka, the message key determines the partition and ordering is guaranteed only within a partition. The broker managing the queues is usually distributed on multiple nodes.
Exactly once: a message is never lost and is read once and only once — required where data means money (financial/ad systems); performance is sacrificed. At most once: a message may get lost but will never be read twice — allowed where not all data is required (monitoring, down-sampling); fast and trivial. At least once: a message will never be lost but may be read twice — balances the two; easier than exactly once; the consumer can deduplicate to adopt exactly once. Beware: the guarantees depend on the chosen tools + application logic on the whole pipeline, not on the broker alone.
A continuous query is issued once and then continuously executed against the data, unlike a traditional query executed once. It may maintain a state: an intermediate result continuously updated by the query; a query is stateless if each execution is independent. The two constraints: memory (data cannot be processed altogether — one-pass algorithms; not much space for the state) and time (data that can’t be processed in time may be dropped — load shedding; algorithms may lose efficacy over time — concept drift).
Job replication: many nodes carry out the same job; needed when low latency is a hard requirement (e.g., intrusion detection systems). Rollback recovery: periodically store checkpoints and maintain a log of operations; lower resource overhead but more expensive recovery; used when fault tolerance matters and rare moderate latencies are acceptable. Alongside them, state management chooses where intermediate state lives — memory vs disk, i.e., a tradeoff between speed and reliability.
Windows are defined by the length (eviction policy — in terms of contained items) and the period (trigger policy — after which the items in the window are processed). Sliding windows define both in stream time. Variants: fixed windows when length = period (analyze the last 5 minutes every 5 minutes; tumbling windows are a special case with length and period in number of items); overlapping windows when length > period (last 5 minutes every 2 minutes); sampling windows when length < period (last 2 minutes every 5 minutes).
A session is a sequence of events terminated by a gap of inactivity greater than some timeout; the typical goal is determining the average amount of traffic generated by sessions. Problems: unknown arrival — data may be collected with some delay, true in every data-driven window; unknown length — the window length cannot be defined a priori (how can I know when the session has ended?), true only for session-based windows.
Stream time is defined by the system as the event enters the pipeline; event time is carried by the event itself. Their difference is the skew, due to hardware issues (network congestions or partitions) or software issues (contention, distributed system logic). Windowing by stream time: pros — extremely straightforward implementation, windows are always complete, and you can infer information about the source as it is observed (e.g., requests per second to a global-scale web service); cons — event time cannot play an important role, and even if the source sends events ordered by event time, they are not guaranteed to arrive immediately or in order (GPS data from cars entering a tunnel, phones going into airplane mode).
Because the business meaning lives in the event’s own time (the reference case: analyzing user behavior on websites). Its costs: impossible to precisely know when the window can be closed; extended window lifetime implies more buffering; most processing systems lack native support. It requires four things: watermarks (the progress of event-time completeness as processing time progresses), triggers (intervals of intermediate processing; each partial result is a pane — based on watermark progress, processing time progress, element counts, or record features such as EOF), allowed lateness (a policy; the higher the tolerance, the longer data must be buffered), and an accumulation strategy (discarding / accumulating / accumulating and retracting).
The watermark determines the time frame in which triggers are active; if the trigger is not based on the watermark, intermediate results are accumulated according to some strategy. After the watermark, window results are kept to allow late data — allowed lateness is a sort of ultimatum: most data is assumed collected within the watermark; a final computation determines the final result, with the same accumulation strategy as before. After that, late data is discarded. A perfect watermark guarantees no late data but at the expense of latency; a heuristic watermark estimates progress from the input stream. The same considerations hold for data-driven windowing in general.
Requirements: one-pass (once examined, items must be discarded); small space O(polylog(n)); fast update O(1) to O(polylog(n)); fast computation of answers; and approximated answers with (ε, δ)-guarantees: with probability at least 1 − δ, the algorithm outputs R′ such that (1 − ε)R ≤ R′ ≤ (1 + ε)R. Example: ε = 0.02, δ = 0.01 gives a 99% probability that the result equals the real one ± 2%. One-pass algorithms exist for counting elements, the nth element, k largest/smallest, sum/mean/variance, frequencies over a known alphabet, heavy hitters; they do not exist for the middle element, the median, the most frequent symbol of an unknown alphabet, or sorting an arbitrary list.
The reservoir of size r is filled with the first r values; the ith item is added with probability P = r/i, and if added, a resident is evicted uniformly at random (Vitter, 1985). The invariant: at any time, each item has the same probability of being in the reservoir — exactly r/i. Proof for an older element xo at step i > r: P(xo kept) = P(xo already in) · (P(xi not put) + P(xi put) · P(someone else evicted)) = r/(i−1) · ((1 − r/i) + (r/i)·(r−1)/r) = r/(i−1) · (i−1)/i = r/i. Memory: O(r), independent of stream length.
Hash the input to uniformly distributed numbers; the maximum number of leading (or trailing) zeros observed in the binary representations estimates the cardinality (if the max is n, about 2n attempts happened). Because single observations are unreliable, the hash bits are split: some identify one of m buckets, and each bucket tracks its own maximum ρ, B[j] = max(B[j], ρ); the final estimate is αm·m²·Z with Z = (Σj 2−B[j])−1, where αm corrects a systematic multiplicative bias. Space: with 32-bit hashes, the maximum leading-zero count is 32, storable in 5 bits, hence O(log log n); 2048 buckets → 1.5 KB at 98% accuracy. Error: ε = k·1.04/√m at confidence levels 68.3% (k=1), 95% (k=2), 99.7% (k=3). It is distributable: each node keeps its own register, then registers are merged.
A Bloom filter (Bloom, 1970) is an array of m > n bits, one flag per bit; each incoming item turns the bit h(i) to 1 for every one of k hash functions, and the membership answer is AND(h1(i), …, hk(i)). It can return false positives, but no false negatives: if it says “hasn’t been seen”, it hasn’t been seen. Sizing for a 1% false-positive rate: k ≈ 7 hash functions and m = 10n bits — 10 bits per element, nothing more (e.g., n = 109 → m = 1010), versus 64 bytes per key in a hash table.
Setup: d hash functions, each with an array of w counters (a d × w grid). Given item i, each hash function increments the counter at h(i); the estimate is count(i) ≈ min over the d rows. Because collisions only inflate counters, the approximation can overcount but never undercount: R′ ≥ R and R′ ≤ R + ε·n, the latter with probability 1 − δ. Sizing: d from δ (d = 5 for δ = 1%), w from ε (w = 272 for ε = 1%; w ≈ e/ε). Worked example: n = 1 billion videos, perfect count ≈ 73 GB, sketch = 5 arrays × 29M counters × 4 bytes ≈ 580 MB. It answers point queries (median-count in the turnstile model), range queries (with extra setup), inner-product/join-size queries (same parameters and hash functions on both streams), and heavy hitters (with a second supporting structure).
Message queueing: Apache Kafka — the most famous and widely adopted, mainly focused on publish/subscribe, general purpose, mature, highly interconnected; Apache Flume handles the whole pipeline but is tightly coupled with Hadoop and has complex scalability; others: Nats, ActiveMQ; commercial: Amazon MQ, Google Cloud Pub/Sub, KubeMQ. Analysis: Apache Storm — famous, DAG topologies (Twitter supported it until 2015, then moved to Heron); Apache Spark — streaming as an extension of the core engine, not pure streaming (mini-batches); Apache Flink — unifies streaming with batch, easier to deploy a Kappa architecture, supports event time and advanced windowing; others: Samza, Akka, Flume, Kafka Streams; commercial: Amazon Kinesis, Google Dataflow.
A message is the unit of data (like a row), optionally carrying a key: messages with the same key end up in the same place. Messages are transferred in batches bound for the same destination — batch size is tunable (latency vs throughput tradeoff) and batches are typically compressed. Messages are categorized into topics (the queues, like a DB table or folder); topics are broken into partitions that provide scalability and redundancy. The key determines the partition, and ordering is guaranteed only within a partition.
Consumers are organized into consumer groups: each partition is owned by (assigned to) only one consumer per group, which enables scalability and fault tolerance of reads (though ownership rebalancing requires downtime); more groups can read from the same topic. Each consumer maintains an offset — the last message read — which can be committed to the broker; offset commits enable failover between consumers in the same group. Replication of partitions enables fault tolerance only: reads and writes are carried out on the leader partition, replicas merely take over. Topics are committed to disk with a tunable retention policy, and consumers read only committed messages.
Exactly once: directly supported only when the consumer is a Kafka Streams application; with other consumers it is implementable through careful management of the offset. At least once: supported by default. At most once: obtained by tuning — disable retries on the producer (don’t produce more than once) and commit offsets in the consumer prior to processing a batch (don’t consume more than once).
Spark Streaming divides the stream into mini-batches; the high-level abstraction is the discretized stream (DStream), a sequence of RDDs — each RDD containing data from a fixed interval (mini-batches are fixed windows with length = period = batch interval, e.g., new StreamingContext(sc, Seconds(1))). Any DStream operation translates to operations on the underlying RDDs. Lifecycle: once started, no new computations can be added; once stopped, it cannot be restarted (a new one must be created); only one StreamingContext can be active per JVM; stop() also stops the SparkContext — use stop(false) to keep it; a SparkContext can be reused for multiple StreamingContexts created one after the other.
By default mini-batches are independent; updateStateByKey associates a state to every key and, for every new mini-batch, updates the state according to a user function (updateFunction(newValues, oldValue) — the new values from this batch plus the previous state; returning None removes the key). Maintaining state requires checkpointing: metadata checkpointing (configuration, DStream operations, incomplete batches) is required to recover from driver failure; data checkpointing is required when stateful operations are used — e.g., ssc.checkpoint("hdfs:/user/egallinucci/streaming/checkpoint").
Each mini-batch is already a fixed window (the last X seconds every X seconds). Other windowing — overlapping and sampling — is obtained by creating a new DStream with a length and a period (slideInterval): e.g., the last 3 seconds of data every 2 seconds. Both parameters must be multiples of the source DStream’s interval. The API: window, countByWindow, reduceByWindow, reduceByKeyAndWindow (with an optional invFunc for incremental recomputation across overlapping windows), countByValueAndWindow.
Output operations allow the transformed data to be consumed by external systems and, like RDD actions, trigger the actual execution of all the DStream transformations. Available: print() — prints the first ten elements of every batch on the driver node; saveAsTextFiles(prefix, suffix), saveAsObjectFiles(prefix, suffix), saveAsHadoopFiles(prefix, suffix); and foreachRDD(func) — the most generic, applying a function to each RDD generated from the stream, with func executed in the driver process.
Structured Streaming (Spark 2) works with DataFrames/Datasets in the streaming scenario through the unbounded table: every record from the stream is appended to the table, and queries run over it as it grows. Output modes: Append — the table is emptied after each trigger (only rows that can never change are emitted); Complete — the table is never emptied and the query is re-run against the whole table every time; Update — the table is never emptied but the query runs only on new records. Con: long-running aggregations may easily fill the state. Pro: it enables event-time windowing with watermark — the theory of section 9 as a native API.