Chapter 72: Real-Time Analytics
Real-time analytics systems process and analyze data as it arrives, enabling immediate insights and actions. Unlike batch systems that process data in scheduled intervals, real-time systems provide sub-second to second-level latency. This chapter covers the architectural patterns, technologies, and trade-offs for building analytics systems that handle millions of events per second.
Lambda Architecture
Lambda Architecture combines batch processing and stream processing to provide both accurate historical analysis and low-latency real-time results. Proposed by Nathan Marz (creator of Apache Storm) in 2011.
Lambda Architecture Components
graph TB
Source[Data Sources
Events, Logs, Sensors]
subgraph "Batch Layer"
BatchStore[Immutable Master Dataset
HDFS, S3]
BatchProcess[Batch Processing
Spark, MapReduce]
BatchView[Batch Views
Pre-computed results]
end
subgraph "Speed Layer"
Stream[Stream Processing
Flink, Storm, Spark Streaming]
SpeedView[Real-Time Views
Incremental updates]
end
subgraph "Serving Layer"
Merge[Merge Logic
Batch + Real-time]
Query[Query Interface
REST API, GraphQL]
end
Source --> BatchStore
Source --> Stream
BatchStore --> BatchProcess
BatchProcess --> BatchView
Stream --> SpeedView
BatchView --> Merge
SpeedView --> Merge
Merge --> Query
User[User Queries] --> Query
Layer Details
1. Batch Layer (Accuracy)
- Purpose: Compute accurate views from complete historical data
- Storage: Immutable, append-only master dataset (HDFS, S3)
- Processing: Apache Spark, Hadoop MapReduce
- Frequency: Hourly or daily batch jobs
- Output: Pre-computed batch views
- Latency: Hours (fresh data delayed)
2. Speed Layer (Latency)
- Purpose: Provide real-time views for recent data
- Processing: Apache Flink, Storm, Spark Streaming
- State: In-memory or fast storage (Redis, Cassandra)
- Approximation: May sacrifice accuracy for speed
- Output: Incremental real-time views
- Latency: Seconds to subseconds
3. Serving Layer (Queries)
- Purpose: Merge batch and real-time views for queries
- Storage: Druid, Cassandra, HBase
- Query logic: batch_view(old data) + speed_view(recent data)
- Interface: REST API, SQL interface
Example: Website Analytics
Requirement: Show pageview counts by URL for any time range.
Batch Layer
- Runs daily: aggregate all pageviews from past 90 days
- Output: {"/home": 1M views, "/product": 500K views, ...}
- Accurate but data is up to 24 hours old
Speed Layer
- Processes pageview events in real-time
- Maintains counts for last 24 hours
- Output: {"/home": +5K views today, "/product": +2K views today}
Serving Layer
- Query: "Show pageviews for /home in last 7 days"
- Response: batch_view("/home", 6 days ago) + speed_view("/home", today)
- Result: Accurate historical + up-to-the-second recent data
Lambda Architecture Pros & Cons
| Pros | Cons |
|---|---|
| Accurate results (batch layer recomputes from source) | Complex: maintain two codebases (batch + stream) |
| Real-time results (speed layer compensates) | Duplicate logic: same computation in both layers |
| Fault-tolerant: speed layer can be rebuilt from batch | Higher operational cost: two systems to monitor |
| Scalable: batch and speed layers scale independently | Data consistency issues during merge |
Kappa Architecture
Kappa Architecture simplifies Lambda by using only stream processing. Proposed by Jay Kreps (creator of Apache Kafka) as a response to Lambda's complexity. Everything is a stream, including historical data.
Kappa Architecture Diagram
graph TB
Source[Data Sources
Events, Logs, Sensors]
subgraph "Stream Storage"
Kafka[Kafka
Durable Event Log
Retention: days to months]
end
subgraph "Stream Processing"
Consumer1[Stream Processor 1
Current version]
Consumer2[Stream Processor 2
Reprocessing old version]
end
subgraph "Serving Layer"
ServingDB[Serving Database
Cassandra, Druid, ClickHouse]
Query[Query Interface
REST API]
end
Source --> Kafka
Kafka --> Consumer1
Kafka --> Consumer2
Consumer1 --> ServingDB
Consumer2 --> |During reprocessing| ServingDB
ServingDB --> Query
User[User Queries] --> Query
Key Principles
1. Everything is a Stream
- Historical data: replay from durable event log (Kafka)
- Real-time data: process as it arrives
- No distinction between batch and streaming
2. Reprocessing for Changes
- Need to change logic? Start new stream processor
- Replay all historical events from beginning
- Write to new serving table
- Cutover when reprocessing catches up
3. Single Codebase
- One stream processing job handles all data
- No separate batch/speed layer logic
- Easier to maintain and test
Reprocessing Workflow
sequenceDiagram
participant K as Kafka (30 days retention)
participant Old as Old Processor (v1 logic)
participant New as New Processor (v2 logic)
participant DB1 as Serving DB v1
participant DB2 as Serving DB v2
Note over K: Historical events stored
Old->>K: Processing real-time
Old->>DB1: Write results
Note over New: Deploy new version
New->>K: Replay from beginning
(offset 0)
New->>DB2: Write to new table
Note over New: Reprocessing catches up
Note over DB2: Validate results
Note over DB1,DB2: Cutover traffic to v2
Old->>Old: Shutdown v1
Requirements for Kappa
- Durable event log: Kafka with long retention (days to months)
- Fast reprocessing: Must replay history in reasonable time
- Stateless or fast state restore: Stream processor can rebuild state
- Idempotent writes: Reprocessing same event produces same result
- Storage for multiple versions: Serving layer holds old + new during cutover
Example: User Activity Counts
Scenario: Count user actions per day. Need to change counting logic.
Without Kappa (Painful)
- Update batch job code → wait 24 hours for next run
- Historical data reprocessed, but slow
- Real-time and batch out of sync during transition
With Kappa (Smooth)
- Deploy new stream processor (v2 logic)
- Replay Kafka from day 1 at high speed (parallel consumers)
- Write to new table: user_activity_v2
- When caught up, redirect queries to v2
- Shutdown old processor
Lambda vs Kappa Comparison
| Aspect | Lambda Architecture | Kappa Architecture |
|---|---|---|
| Complexity | High (two layers, duplicate logic) | Low (single stream processing layer) |
| Code Maintenance | Difficult (sync batch + stream code) | Easy (one codebase) |
| Reprocessing | Run batch job on all historical data | Replay event log from Kafka |
| Latency | Batch: hours, Speed: seconds | Seconds (all data treated as stream) |
| Accuracy | Batch: exact, Speed: approximate | Exact (if exactly-once processing) |
| Storage | HDFS/S3 (immutable dataset) | Kafka (durable log with retention) |
| Data Volume | Petabytes (batch handles large scale) | Limited by Kafka retention |
| Best For | Very large historical data, guaranteed accuracy | Moderate data, need simplicity, fast reprocessing |
When to Use Which
Use Lambda When
- Historical data too large to replay (petabytes)
- Batch processing tools already in place (Spark jobs)
- Different algorithms needed for batch vs real-time
- Regulatory requirements for exact accuracy
Use Kappa When
- Historical data fits in Kafka retention (days to months)
- Frequent logic changes (need fast reprocessing)
- Team prefers simpler architecture
- Stream processing can handle full data volume
Hybrid Approach
- Use Kappa for recent data (last 30 days in Kafka)
- Use batch for deep historical analysis (years in S3)
- Combine results at query time
Materialized Views
Materialized views are pre-computed query results stored as tables. Instead of running expensive aggregations on-demand, the system pre-aggregates data and stores it for fast lookups.
Materialized View Concept
graph TB
Raw["Raw Events Table
Billions of rows
user_id, action, timestamp"]
Query1["Query: Daily Active Users
SELECT date, COUNT(DISTINCT user_id)
FROM events
GROUP BY date"]
Query2["Query: Actions per User
SELECT user_id, COUNT(*)
FROM events
GROUP BY user_id"]
MV1["Materialized View: daily_active_users
Pre-aggregated, fast"]
MV2["Materialized View: user_action_counts
Pre-aggregated, fast"]
Raw --> |Slow: scan billions| Query1
Raw --> |Slow: scan billions| Query2
Raw --> |Incremental update| MV1
Raw --> |Incremental update| MV2
MV1 --> |Fast: read aggregates| User[User Dashboard]
MV2 --> |Fast: read aggregates| User
Refresh Strategies
| Strategy | How It Works | Pros | Cons |
|---|---|---|---|
| Full Refresh | Recompute entire view from scratch | Simple, guaranteed correct | Expensive, high latency |
| Incremental Refresh | Apply only new changes since last refresh | Fast, low cost | Complex logic, requires change tracking |
| On-Demand | Refresh when view is queried (if stale) | No wasted work | Query latency unpredictable |
| Scheduled | Refresh every N minutes/hours | Predictable freshness | May refresh unnecessarily |
| Streaming | Continuously update as data arrives | Always fresh, low latency | Complex, requires stream processor |
Incremental Materialized Views
Goal: Update materialized view without recomputing everything.
Example: COUNT
- Old value: 1000 pageviews for /home
- New events: 5 pageviews for /home
- Incremental update: 1000 + 5 = 1005
Example: SUM
- Old value: $10,000 total revenue
- New events: $500 in new orders
- Incremental update: $10,000 + $500 = $10,500
Challenge: AVG
- Cannot incrementally update average directly
- Solution: Store SUM and COUNT, compute AVG = SUM / COUNT
- Incrementally update SUM and COUNT separately
Materialized Views in Practice
PostgreSQL Materialized Views
CREATE MATERIALIZED VIEW daily_sales AS
SELECT date, product_id, SUM(amount) AS total_sales
FROM orders
GROUP BY date, product_id;
-- Refresh manually
REFRESH MATERIALIZED VIEW daily_sales;
-- Refresh concurrently (non-blocking)
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_sales;
Apache Flink / Kafka Streams (Real-Time)
// Continuously update materialized view from stream
stream
.groupBy(event -> event.productId)
.aggregate(new SumAggregator())
.to("materialized_view_topic");
Druid (Rollup)
- Automatically rolls up data at ingestion time
- Stores pre-aggregated metrics (SUM, COUNT, MIN, MAX)
- Queries run on rolled-up data, not raw events
Stream Processing for Analytics
Stream processing frameworks enable real-time analytics by computing aggregations, joins, and transformations on unbounded data streams.
Windowing in Stream Processing
Since streams are infinite, we use windows to group events into finite chunks for aggregation.
graph TB
subgraph "Tumbling Window (Non-overlapping)"
T1[Window 1
00:00 - 00:05]
T2[Window 2
00:05 - 00:10]
T3[Window 3
00:10 - 00:15]
end
subgraph "Sliding Window (Overlapping)"
S1[Window 1
00:00 - 00:05]
S2[Window 2
00:02 - 00:07]
S3[Window 3
00:04 - 00:09]
end
subgraph "Session Window (Activity-based)"
Ses1[Session 1
User active
then 30min gap]
Ses2[Session 2
User active again]
end
Window Types
| Window Type | Description | Use Case |
|---|---|---|
| Tumbling | Fixed-size, non-overlapping (e.g., every 5 min) | Hourly sales totals, daily active users |
| Sliding | Fixed-size, overlapping (e.g., 5 min window, slide every 1 min) | Moving averages, rolling metrics |
| Session | Variable-size, gap-based (e.g., close after 30 min inactivity) | User sessions, clickstreams |
| Global | Entire stream (all time) | Total counts, all-time metrics |
Event Time vs Processing Time
Event Time
- Definition: When the event actually occurred (timestamp in event)
- Pros: Accurate, handles late arrivals, replayable
- Cons: Complex (watermarks, late data handling)
- Example: User clicked button at 10:05:23 AM
Processing Time
- Definition: When the event was processed by the system
- Pros: Simple, no late data
- Cons: Not reproducible, skewed by delays
- Example: Event processed at 10:07:15 AM (2 min delay)
Handling Late Data
sequenceDiagram
participant S as Stream
participant W as Watermark
participant Win as Window [10:00-10:05]
participant Out as Output
Note over S: Events arrive in order
S->>Win: Event @ 10:01 ✓
S->>Win: Event @ 10:03 ✓
S->>Win: Event @ 10:04 ✓
Note over W: Watermark = 10:05
(No more events before 10:05)
W->>Win: Close window
Win->>Out: Emit result: count=3
Note over S: Late event arrives!
S->>Win: Event @ 10:02 (late)
Win->>Out: Update result: count=4
(if allowed lateness)
Note over Win: Allowed lateness expires
Win->>Win: Discard late events
Apache Flink Example
// Real-time analytics: Count pageviews per URL in 5-minute windows
DataStream<PageView> stream = env.addSource(new KafkaSource(...));
stream
.keyBy(view -> view.url)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.allowedLateness(Time.minutes(1)) // Handle late data
.aggregate(new CountAggregator())
.addSink(new CassandraSink(...)); // Write to serving DB
Key Features
- Event time: Use timestamp from event, not processing time
- Watermarks: Track progress of event time
- Allowed lateness: Accept events up to 1 min late, update results
- Exactly-once: Flink guarantees no duplicates or lost data
OLAP Cubes
OLAP (Online Analytical Processing) cubes pre-compute aggregations across multiple dimensions, enabling fast drill-down and slice-and-dice queries.
OLAP Cube Structure
graph TB
Cube["Dimensions: - Product - Region - Time Measures: - Revenue - Units Sold"]
L0["Level 0: All combinations Product × Region × Time"]
L1["Level 1: Two dimensions Product × Region Product × Time Region × Time"]
L2["Level 2: One dimension Product Region Time"]
L3["Level 3: Grand total All data"]
Cube --> L0
L0 --> L1
L1 --> L2
L2 --> L3
Cube Dimensions vs Measures
| Concept | Definition | Examples |
|---|---|---|
| Dimensions | Attributes to group by (categorical) | Product, Region, Time, Customer Segment |
| Measures | Numeric values to aggregate | Revenue, Units Sold, Profit, Page Views |
| Hierarchies | Drill-down levels within a dimension | Time: Year → Quarter → Month → Day |
OLAP Operations
1. Slice
Select a single value from one dimension.
- Example: "Show sales for Region = 'North America'"
- Result: Reduces 3D cube to 2D (Product × Time)
2. Dice
Select a sub-cube by filtering multiple dimensions.
- Example: "Show sales for Product IN ('A', 'B') AND Time IN (Q3, Q4)"
- Result: Smaller 3D cube
3. Drill-Down
Navigate from summary to detail.
- Example: Year → Quarter → Month → Day
- Use case: User clicks "Q4" to see monthly breakdown
4. Roll-Up
Aggregate from detail to summary.
- Example: Day → Month → Quarter → Year
- Use case: Summarize daily sales into monthly totals
5. Pivot (Rotate)
Swap dimensions to change perspective.
- Example: Change from "Products by Region" to "Regions by Product"
- Use case: Excel pivot tables
Storage: MOLAP vs ROLAP vs HOLAP
| Type | Full Name | Storage | Pros | Cons |
|---|---|---|---|---|
| MOLAP | Multidimensional OLAP | Pre-computed cube in optimized format | Fastest queries (sub-second) | Slow to build, storage explosion |
| ROLAP | Relational OLAP | Query relational DB on-the-fly | No pre-computation, flexible | Slower queries (seconds) |
| HOLAP | Hybrid OLAP | Hot data in MOLAP, cold in ROLAP | Balanced speed and cost | Complex to manage |
Cube Explosion Problem
Problem: Pre-computing all combinations explodes exponentially.
Example
- 10 dimensions, each with 100 values
- Possible combinations: 100^10 = 10^20 cells
- Even with sparse data, storage becomes massive
Solutions
- Partial cubes: Pre-compute only common queries
- Aggregation hierarchies: Store only summary levels
- Sparse storage: Don't store empty cells
- Sampling: Approximate results for large cubes
Real-Time Dashboards
Real-time dashboards display up-to-the-second data, requiring efficient data push mechanisms and optimized queries.
Dashboard Architecture
graph TB
subgraph "Data Sources"
Stream[Kafka Streams]
DB[Analytical DB
Druid, ClickHouse]
end
subgraph "Backend"
API[API Server
Node.js, Go]
Cache[Redis Cache
Hot metrics]
WS[WebSocket Server
Real-time push]
end
subgraph "Frontend"
Browser[Browser
React, Vue]
Chart[Chart Library
D3.js, Grafana]
end
Stream --> |Aggregations| Cache
DB --> API
Cache --> API
API --> WS
WS --> |Push updates| Browser
Browser --> Chart
Browser --> |Poll fallback| API
Update Mechanisms
| Mechanism | How It Works | Pros | Cons |
|---|---|---|---|
| WebSocket Push | Server pushes updates to clients | Instant updates, efficient | Complex, connection management |
| Server-Sent Events (SSE) | Server sends events over HTTP | Simple, HTTP-based | One-way only (server → client) |
| Short Polling | Client requests updates every N seconds | Simple, works everywhere | High latency (up to N seconds) |
| Long Polling | Client waits for update, server responds when available | Lower latency than short polling | Inefficient, connection overhead |
Optimization Techniques
1. Caching Hot Metrics
- Store frequently queried metrics in Redis
- Update cache from stream processor
- Serve dashboard queries from cache (sub-ms latency)
- Example: "Current active users" updated every second
2. Pre-Aggregation
- Stream processor computes aggregates (hourly, daily)
- Dashboard queries pre-aggregated data, not raw events
- Reduces query time from seconds to milliseconds
3. Downsampling
- Reduce resolution for old data
- Example: Last hour: 1-second granularity → Last day: 1-minute → Last month: 1-hour
- Saves storage and speeds up queries
4. Query Result Caching
- Cache dashboard query results for 5-30 seconds
- Many users viewing same dashboard hit cache
- Trade-off: slight staleness for massive speedup
Grafana-Style Dashboard
Architecture
- Data source: Prometheus, InfluxDB, Druid
- Queries: PromQL, SQL, custom DSL
- Refresh interval: 5s to 1min (configurable)
- Alerting: Trigger alerts on threshold breach
Best Practices
- Limit panels per dashboard (5-10) to avoid query overload
- Use variables for filtering (time range, region, service)
- Pre-aggregate data for historical views (rollups)
- Optimize queries: avoid SELECT * , use indexed columns
Handling Dashboard Load
graph LR
Users[1000 Users
Viewing Dashboard]
LB[Load Balancer]
subgraph "API Tier (Stateless)"
API1[API 1]
API2[API 2]
API3[API 3]
end
subgraph "Caching Layer"
Redis[Redis
Query result cache
TTL: 10s]
end
subgraph "Database"
DB[ClickHouse
Analytical DB]
end
Users --> LB
LB --> API1
LB --> API2
LB --> API3
API1 --> Redis
API2 --> Redis
API3 --> Redis
Redis --> |Cache miss| DB
DB --> Redis
Real-World Example: Uber's Real-Time Dashboard
Requirements
- Show active rides, driver availability, demand heatmaps
- Update every 1-5 seconds
- Support 10,000+ simultaneous viewers (ops team, execs)
Solution
- Stream processing: Flink aggregates ride events by city, time window
- Serving layer: Pre-aggregated data in Pinot (real-time OLAP)
- Caching: Redis caches common queries (city-level metrics)
- Push updates: WebSocket pushes new data to connected clients
- Result: Sub-second latency for 10K+ concurrent users
Key Takeaways
- Lambda Architecture combines batch (accurate) and speed (low-latency) layers, at the cost of duplicate logic and complexity.
- Kappa Architecture simplifies Lambda by treating everything as a stream, enabled by Kafka's durable log and fast reprocessing.
- Materialized views pre-compute aggregations for fast queries. Incremental refresh is key to efficiency.
- Stream processing enables real-time analytics with windowing (tumbling, sliding, session) and event time semantics.
- OLAP cubes pre-compute multi-dimensional aggregations, enabling fast drill-down but risking storage explosion.
- Real-time dashboards require caching, pre-aggregation, and efficient push mechanisms (WebSocket, SSE) to scale.