11

Stream processing

Batch processing assumes the input is finite and known. Real data arrives forever, so stream processing handles each event soon after it happens. This chapter covers how events get from producers to consumers (message brokers, and the log-based ones that look like a database's replication log). It shows how a database's own writes can be turned into a stream that keeps caches and indexes in sync. Then come the hard parts of processing a stream: deciding what time an event belongs to, joining streams, and surviving crashes without losing or double-counting anything.

When producers outpace consumers

stream-eventmessage-brokerbackpressure
A broker sits between producers and consumers. If messages arrive faster than they can be processed, it has three options: drop them, buffer them in a growing queue, or push back on the producer.

Producer → broker → consumer

producer9/squeue (memory holds 60, then spills to disk)consumer6/s
05100secondsqueued

Messages arrive randomly at the producer rate; the consumer takes an exponential time per message. Try: with the producer faster than the consumer, compare the three policies. Buffering hides the problem until latency explodes; backpressure caps the producer at the consumer's pace.

in queue
0
dropped
0
avg latency
0.0 s
enqueue → consumed
producer blocked
0%
effective rate 0.0/s

Two kinds of broker: delete on ack, or keep a log

stream-load-balancingack-redeliverylog-based-brokerconsumer-offset
AMQP/JMS-style brokers hand each message to a consumer and delete it once it is acknowledged. Log-based brokers append messages to a partitioned log on disk; consumers keep track of their own offset, so reading is non-destructive and replay is free.

One queue shared by two consumers

broker queueconsumer (dashed = delivered, unacked)processed, in order of processingABAcked messages are deleted from the broker: there is nothing to replay.

Each consumer prefetches up to 3 messages and acks one after 6 ticks of work. Try: in load-balancing mode, crash consumer A. Its unacked messages are redelivered to B, which has already processed later ones, so they appear out of order (red).

queued at broker
0
acked & deleted
0
gone forever
redelivered
0
processed out of order
0
Rule of thumb from the book: if messages are expensive to process, can be handled in any order, and you want per-message parallelism, a JMS/AMQP broker suits. If throughput is high, each message is cheap, and ordering matters, use a log.

Keeping systems in sync: dual writes vs change data capture

dual-writeschange-data-capturelog-compaction
The same data often lives in a database, a cache, and a search index. If the application writes to each one itself, two concurrent writers can leave them disagreeing forever. If instead every derived system follows the database's change log, they all apply the same writes in the same order.

Two clients set the same key concurrently: X = A and X = B

client 1client 2databasesearch indexAABBX = BX = APermanently inconsistent: database says B, index says A.

Lines are network messages; their length is a random delay. Try: in dual-write mode press "Find a bad interleaving": the two writes reach the database and the index in opposite orders (Figure 11-4). Then switch to CDC: the index applies the database's change log in commit order, so it can lag but never disagrees.

database
X = B
search index
X = A
commit order
A → B
order of writes in the DB log
inconsistent races
21%
of 300 random races, this mode

Change log for a users table (key = primary key)

#0user:4Ada#1user:4Ed#2user:3Lu#3user:2Bo#4user:4Flo#5user:5tombstone#6user:4Hal#7user:5Di#8user:4Flo#9user:2Lu#10user:5Hal#11user:5Jo#12user:2Gus#13user:1tombstone
a new consumer replaying from offset 0 gets:user:2=Gususer:3=Luuser:4=Flouser:5=Jo

Faded entries are overwritten by a later write to the same key (or deleted by a tombstone). Try: append a few rounds, then compact. The log shrinks to one entry per live key, and replaying it still rebuilds exactly the current table, so a new derived system (say, a fresh search index) can bootstrap from the log alone.

writes ever made
14
log length
14
10 superseded
live keys in table
4
replay = table?
yes

Event sourcing: state is a fold over the log

event-sourcingcommand-vs-event
Instead of updating a cart row in place, record each thing the user did as an immutable event. The current cart, and any other view you later think of, is just the result of replaying those events.
commands:

Event log (append-only, immutable)

  1. 0ItemAdded(apples)
  2. 1ItemAdded(book)
  3. 2ItemAdded(apples)
  4. 3ItemRemoved(book)
  5. 4ItemAdded(socks)
  6. 5CheckedOut(3 items)
  7. 6ItemAdded(book)

Derived state = replay(events)

apples
0
book
1
socks
0

Try: remove something that is not in the cart (rejected command, no event). Drag the replay slider back to see the cart at any past moment; take a snapshot and watch how many events a replay still needs.

events in log
7
items in cart
1
current state
events replayed
7
from the beginning
snapshots
0
a cache; the log stays the truth

Event time vs processing time

event-timestraggler
A stream processor that counts requests per minute must decide which minute an event belongs to. Using the processor's own clock is simple but wrong whenever events are delayed; using the event's timestamp means you can never be quite sure a minute is complete.

When it happened (x) vs when the processor saw it (y)

0:001:002:003:004:005:006:000:001:002:003:004:005:006:007:008:00event timeproc.now 8:00

Green: on time. Amber: straggler that still made its window. Red: arrived after its window was closed (late). The dashed diagonal is zero delay; the red band is the outage, after which the backlog is processed in a burst.

Events per minute: windowed by processing time vs by event time

0:00 ✓1:00 ✓−22:00 ✓−33:00 ✓−24:00 ✓−35:00 ✓−16:007:00
by processing timeby event time (late dropped)dashed = true count · ✓ = window closed by the watermark

Try: play with the outage on. The processing-time chart shows a dip and then a fake spike at 4:00, though the real rate never changed. Event-time windows stay flat, but stragglers past the watermark are lost unless you raise the allowed lateness (and wait longer for every result) or publish corrections.

events seen
155
of 155
processing-time error
42
Σ |count − true count| per minute
late events
11
dropped
event-time error
11
over closed windows
result delay
20s
after window end, before a count is final
avg event delay
10.6s

Four kinds of window on one timeline

stream-windows
Aggregating an infinite stream means choosing a window. The same events get counted very differently depending on which kind you choose.

tumbling windows over one user's events

0s10s20s30s40s50s60s354346

Fixed length, back to back: every event belongs to exactly one window.

Numbers are event counts per window. Try: click an event to see every window it lands in (one for tumbling, several for hopping); in session mode, shrink the inactivity gap until one session splits in two.

events
25
windows
6
6 non-empty
windows per event
1.0
selected event
–
click a dot
New events can arrive at any time on either side of a join, so a streaming join must keep state. There are three shapes: two event streams within a window, an event stream against a table, and two table changelogs that keep a materialized view current.

Search events ⋈ click events on session id, within a window

search streamclick stream112233445no click6no click778no click9910no click

The processor indexes recent searches by session (blue bar = how long it keeps waiting). Green: click found in the window. Red: the click came too late and the search was already emitted as "not clicked". Try: shrink the window and watch measured click-through rate drop; grow it and the state the processor must hold grows too.

true click-through
60%
measured click-through
20%
clicks missed
4
arrived after the window closed
state held
10s of searches
index of recent events, both sides

Fault tolerance: making at-least-once look exactly-once

microbatchingstream-idempotence
A batch job can simply be rerun after a failure. A stream job never finishes, so it records its progress in checkpoints and restarts from the last one. What happens to output that was already sent before the crash depends on how progress and output are committed.

Input log → stream processor → external database

input records (amounts)344536931443173319984151427164checkpoint @0processing #0 (step 1)rows in the output database, per input offset

Write the output, then checkpoint the offset every few records (a microbatch). A crash replays everything since the last checkpoint: at-least-once.

Each record takes two steps (write output, record progress) in the order the mode dictates. Red stacks are duplicate side effects; dashed red boxes are records skipped forever. Try: crash mid-batch in each mode; then raise random crashes and compare totals.

expected total
0
Σ of records 0–0
total in output DB
0
duplicates
0
lost records
0
crashes
0
Exactly-once really means effectively once: records may be processed several times, but the visible effect is as if each was processed once. You get there with atomic commit of output and offsets, or with idempotent writes that carry the input offset.
  • takeawayA log-based broker treats messages like a database treats its replication log: durable, ordered per partition, readable by many consumers at their own pace, and replayable. Consumer offsets make acks cheap and reprocessing easy.
  • takeawayDon't write to several systems from the application. Make one system the leader and derive the rest from its change stream (CDC or event sourcing). Every view then sees the same writes in the same order, and new views can be built by replaying.
  • takeawayWindow by event time, not processing time, and decide up front what to do with stragglers. Make side effects idempotent or commit them atomically with offsets, so crash-and-replay gives exactly-once results.

Key concepts · 20

book chapter 11
  1. Event (in a stream)

    A small, self-contained, immutable record that something happened at some point in time.

    Events are generated once by a producer and processed by many consumers. Thinking of data as a sequence of events, rather than as mutable state, is the foundation for everything else in the chapter.

    ↑ see it in the visualization
  2. Messaging system / message broker

    A server that producers send events to and consumers receive them from, decoupling the two.

    Brokers tolerate clients coming and going and absorb bursts. Two key design questions: what happens when producers are faster than consumers, and what happens if a node crashes.

    RabbitMQActiveMQKafkaGoogle Cloud Pub/Sub
    ↑ see it in the visualization
  3. Backpressure (flow control)

    Blocking the producer when the consumer cannot keep up, instead of dropping messages or buffering without bound.

    The three options for an overloaded consumer are drop, buffer, or block. Buffering only postpones the problem and raises latency; dropping loses data; backpressure slows the whole pipeline to the speed of its slowest part.

    TCP flow controlUnix pipes
    ↑ see it in the visualization
  4. Load balancing vs fan-out

    Load balancing delivers each message to one of several consumers; fan-out delivers every message to all of them.

    Load balancing parallelizes expensive processing; fan-out lets independent consumers each see the whole stream. Consumer groups in log-based brokers combine both.

    ↑ see it in the visualization
  5. Acknowledgments and redelivery

    A consumer acknowledges each message after processing; unacknowledged messages are redelivered if the consumer dies.

    Redelivery prevents loss, but combined with load balancing it reorders messages: a redelivered message can reach another consumer after later ones were already processed.

    AMQPJMS
    ↑ see it in the visualization
  6. Log-based message broker

    A broker that stores messages in an append-only, partitioned log on disk; consumers read sequentially and messages are not deleted on consumption.

    It gives durable, replayable, totally ordered (per partition) streams with high throughput. Parallelism is at the granularity of partitions, and a slow message holds up its whole partition.

    Apache KafkaAmazon KinesisApache PulsarTwitter DistributedLog
    ↑ see it in the visualization
  7. Consumer offsets and lag

    Each consumer group records how far it has read in each partition as a single offset number.

    Offsets make acknowledgment cheap and replay trivial: rewind the offset to reprocess. A consumer that lags further behind than the retention window loses the messages that were deleted.

    ↑ see it in the visualization
  8. Dual writes

    Application code writing the same change to several systems (database, cache, search index) itself.

    Two concurrent clients can have their writes arrive in different orders at each system, leaving them permanently inconsistent, and one write can fail while the other succeeds.

    ↑ see it in the visualization
  9. Change data capture (CDC)

    Observing every write to a database and exposing it as a stream that other systems can apply in the same order.

    It makes the database the leader and derived systems followers of its replication log, so they converge on the same state. Initial state comes from a snapshot or a compacted log.

    DebeziumMaxwellLinkedIn DatabusPostgres logical decodingMongoDB oplog
    ↑ see it in the visualization
  10. Log compaction

    Periodically discarding older log entries for a key, keeping only the most recent value (and dropping keys with a tombstone).

    The log then stays proportional to the size of the database rather than to its write history, yet a new consumer can still rebuild the full current state by reading it from offset 0.

    Kafka compacted topics
    ↑ see it in the visualization
  11. Event sourcing

    Storing every change to application state as an immutable event in an append-only log, and deriving current state by replaying it.

    Events capture intent at the application level, history is never lost, and new read-optimized views can be derived from the same events. Snapshots keep replay fast.

    Event StoreAxon
    ↑ see it in the visualization
  12. Commands vs events

    A command is a request that may still be rejected; once validated it becomes an event, an immutable fact.

    Validation must happen synchronously before the event is written. Consumers of the event cannot reject it, only compensate with further events.

    ↑ see it in the visualization
  13. Event time vs processing time

    Event time is when the event occurred (by the device's clock); processing time is when the stream processor sees it.

    Queueing, network faults, and restarts delay events, sometimes by minutes. Windowing by processing time produces artifacts such as spikes after a backlog, so rates should be computed by event time.

    ↑ see it in the visualization
  14. Stragglers and watermarks

    A straggler is an event that arrives after its window was believed complete; a watermark is the processor's declaration that no earlier events are expected.

    You can never be sure all events for a window have arrived. Either ignore stragglers (and monitor how many you drop) or publish a correction later.

    ↑ see it in the visualization
  15. Window types

    Tumbling (fixed, non-overlapping), hopping (fixed, overlapping), sliding (all events within an interval of each other), session (bounded by inactivity gaps).

    Aggregations over an unbounded stream need a bounded scope. The window type determines which events are counted together and whether one event can count in several windows.

    ↑ see it in the visualization
  16. Stream–stream join (window join)

    Joining events from two streams that share a key and occur within some window of each other, e.g. searches and clicks.

    The processor must keep state, an index of recent events from both sides, and the window decides when it stops waiting and emits a non-match.

    ↑ see it in the visualization
  17. Stream–table join (enrichment)

    Augmenting each event with data from a table, using a local copy of the table kept current by a changelog stream.

    Avoids a remote database query per event. The join is time-dependent: an event joined before or after a profile update gets different data.

    ↑ see it in the visualization
  18. Table–table join (materialized view)

    Two changelog streams joined to maintain a materialized view of the join, such as a per-user timeline cache of tweets from followed accounts.

    Instead of computing the join at read time, every tweet, delete, follow and unfollow incrementally updates the cached result.

    Twitter home timelines
    ↑ see it in the visualization
  19. Microbatching and checkpointing

    Breaking a stream into small blocks processed like mini batch jobs, or periodically checkpointing operator state and input offsets.

    On failure the processor restarts from the last checkpoint and discards partial output, but any side effects already sent to external systems would be repeated.

    Spark StreamingApache Flink
    ↑ see it in the visualization
  20. Idempotence and exactly-once

    An idempotent operation has the same effect whether it is applied once or several times; with it, at-least-once processing yields effectively exactly-once results.

    Storing the input offset alongside the output, or committing output and offset atomically, makes reprocessing after a crash harmless.

    Kafka transactionsGoogle Cloud DataflowVoltDB
    ↑ see it in the visualization