Building Real-Time Service Topology at Netflix Scale

Peter Bubenik · Netflix Tech · · Source
Image for Building Service Topology at Scale: Architecture, Challenges, and Lessons Learned

Learning Outcomes

After studying this material, you should be able to:

  1. Explain why streaming architectures outperform batch processing for real-time observability
  2. Describe how backpressure prevents data loss in high-throughput pipelines
  3. Analyze why multi-stage distributed pipelines solve hot node and data locality problems
  4. Apply consistent hashing with dynamic discovery for load distribution
  5. Evaluate technology choices (SSE vs gRPC) based on measured performance
  6. Understand time-travel query architecture using snapshots and mutation tracking
  7. Recognize the iterative optimization methodology for distributed systems at scale

Step 1: The Core Problem — Why This System Exists

The Observability Gap

Imagine a production incident at 3am. A service is failing, but you don't know which upstream services depend on it or which downstream services it calls.

Traditional Approach:
┌─────────────────────────────────────────┐
│  Batch Processing (hourly/daily)        │
│                                         │
│  Raw Data → Aggregate → Store Snapshot  │
│                                         │
│  Result: Data is 1-24 hours OLD        │
│  Problem: Useless during incidents      │
└─────────────────────────────────────────┘

The Netflix Solution: Streaming-First Architecture

Streaming Approach:
┌─────────────────────────────────────────────┐
│  Continuous Processing                       │
│                                             │
│  Raw Data → Real-time Pipeline → Live Graph │
│                                             │
│  Result: Data is minutes old               │
│  Benefit: Useful during incidents           │
└─────────────────────────────────────────────┘

Key Insight: Freshness is not a nice-to-have. It is a fundamental requirement for incident response.


Step 2: Backpressure — The Mechanism That Prevents Collapse

The Problem Without Backpressure

Without Backpressure:
                                    ❌ OVERFLOW
Producer ──────────────────────────► Buffer ──► Consumer
(fast)                              (full)      (slow)

Result: Data loss, crashes, cascading failures

How Backpressure Works

Think of it like a highway with traffic signals:

With Backpressure:

Stage 1          Stage 2          Stage 3
(Kafka           (Resolution)     (Persistence)
Consumer)

Normal:
[Produce] ──►  [Process] ──►  [Write DB]
  fast           medium          fast

DB Slows Down:
[Pause] ◄──  [Slow Down] ◄──  [Overwhelmed]
  ✅            ✅               ⚠️

Signal travels BACKWARD through the pipeline
Data waits safely in Kafka
No data lost

The Trade-off

ScenarioWithout BackpressureWith Backpressure
Normal loadFastFast
Traffic spikeData loss / crashSlight delay
DB slowdownBuffer overflowGraceful slowdown
RecoveryManual interventionAutomatic

Key Insight: Slightly delayed real-time is vastly better than dropped data or system crashes.


Step 3: The Three-Stage Pipeline — Solving Data Locality

Why Network Flows Are Misleading

In cloud environments, traffic rarely flows directly between services:

What Actually Happens:
App A ──► Load Balancer ──► App B

What Network Logs Show:
Flow 1: App A → Load Balancer
Flow 2: Load Balancer → App B

What Engineers Need:
App A → App B  (the logical dependency)

Without resolving intermediaries, your topology map is cluttered with infrastructure noise instead of service relationships.

Why You Cannot Solve This in One Stage

The Data Locality Problem:

Instance 1 has: [App A → Load Balancer X]
Instance 2 has: [Load Balancer X → App B]

To resolve: App A → App B
You need BOTH flows on the SAME instance

But Kafka distributes flows across instances randomly!

This is why multiple stages are necessary.

The Three-Stage Solution

STAGE 1: Initial Aggregation
┌─────────────────────────────────────────────┐
│ Multi-Region Kafka (4 regions)              │
│         ↓                                   │
│ Filter invalid flow logs                    │
│         ↓                                   │
│ 5-minute time-window batching               │
│         ↓                                   │
│ Create aggregator objects                   │
│         ↓                                   │
│ Distribute via consistent hashing           │
│         ↓                                   │
│ Stream to Stage 2 via SSE ──────────────►  │
└─────────────────────────────────────────────┘
Purpose: Compress raw flows into aggregators
         Reduce data volume before redistribution

STAGE 2: Intermediary Resolution
┌─────────────────────────────────────────────┐
│ Receive aggregators from Stage 1            │
│         ↓                                   │
│ Group by intermediary (Load Balancer, etc.) │
│         ↓                                   │
│ Join: (A→LB) + (LB→B) = (A→B)             │
│         ↓                                   │
│ Redistribute resolved edges                 │
│         ↓                                   │
│ Stream to Stage 3 via SSE ──────────────►  │
└─────────────────────────────────────────────┘
Purpose: Resolve infrastructure hops into
         logical service-to-service edges

STAGE 3: Final Aggregation & Persistence
┌─────────────────────────────────────────────┐
│ Receive resolved aggregators                │
│         ↓                                   │
│ Final aggregation across time windows       │
│         ↓                                   │
│ Enrich with metadata (health, ownership)    │
│         ↓                                   │
│ Convert to graph entities                   │
│         ↓                                   │
│ Persist to graph database (throttled)       │
└─────────────────────────────────────────────┘
Purpose: Enrich and store final topology data

Why Not Two Stages?

Two-Stage Attempt (Failed):
Stage 2 must:
  - Collect ALL flows for each intermediary (data concentration)
  - Resolve intermediaries (CPU intensive)
  - Enrich with external data (I/O intensive)
  - Persist to database (I/O intensive)

Result: Stage 2 becomes overwhelmed
        Hot nodes crash
        Cascading failures

Three-Stage Solution:
  Stage 2: ONLY resolution + redistribution
  Stage 3: ONLY enrichment + persistence
  
  Responsibilities separated
  Load spread across more instances
  No single bottleneck

Step 4: Hot Nodes — The Power-Law Distribution Problem

Understanding the Problem

Not all services are equal in traffic volume:

Traffic Distribution (Power Law):
                    
Auth Service:    ████████████████████████████ 10,000 calls/sec
Recommendation:  ████████████████████ 7,000 calls/sec  
Typical Service: ██ 100 calls/sec
Rare Service:    █ 10 calls/sec

If consistent hashing assigns Auth Service to Instance 1:
Instance 1: ████████████████████████████ OVERWHELMED
Instance 2: ██ fine
Instance 3: █ idle

The Cascading Failure Cycle

Hot Instance Failure Loop:

High Traffic
    ↓
Memory Fills Up
    ↓
Garbage Collection Runs Constantly
    ↓
CPU Spent on GC, Not Business Logic
    ↓
Processing Slows Down
    ↓
More Memory Accumulates
    ↓
Instance Crashes
    ↓
Load Redistributes to Other Instances
    ↓
They Become Hot Nodes Too
    ↓
Cascading Failure ❌

How Three Stages Solve Hot Nodes

Data Compression at Each Stage:

Raw Flow Logs:     1,000,000 records
                        ↓
After Stage 1:        10,000 aggregators  (100x compression)
                        ↓
After Stage 2:         5,000 resolved edges (2x compression)
                        ↓
Stage 3 writes:        5,000 graph entities

Each stage redistributes compressed data
No single instance handles raw volume
Load spreads naturally

Step 5: Technology Choices — SSE vs gRPC

Why gRPC Failed at This Scale

gRPC Problems for Streaming Aggregation:
┌────────────────────────────────────────┐
│ • Serialization overhead per message   │
│ • Connection pool management overhead  │
│ • Memory pressure from streaming resp  │
│ • More CPU on protocol than business   │
│   logic                                │
└────────────────────────────────────────┘

Why SSE Won

SSE Advantages for This Use Case:
┌────────────────────────────────────────┐
│ • Lightweight HTTP-based protocol      │
│ • Minimal serialization overhead       │
│ • Natural backpressure integration     │
│ • Simpler connection model             │
│ • Human-readable (easier debugging)    │
└────────────────────────────────────────┘

The Decision Framework

Choose gRPC when:
  ✅ Request-response RPC patterns
  ✅ Bidirectional streaming needed
  ✅ Strong typing across teams critical
  ✅ Low-latency individual calls

Choose SSE when:
  ✅ One-directional streaming
  ✅ Large volumes of pre-aggregated data
  ✅ Backpressure integration needed
  ✅ Debugging simplicity valued

Rule: Measure first. Choose based on YOUR data, not industry defaults.

Step 6: Dynamic Consistent Hashing

The Challenge

Auto Scaling Groups add and remove instances dynamically. How do you route data consistently when the cluster size keeps changing?

The Solution

Dynamic Consistent Hashing:

Step 1: Each instance queries service registry
        Registry returns: [Instance-A, Instance-B, Instance-C]

Step 2: Sort the list (ensures all instances agree)
        Sorted: [Instance-A, Instance-B, Instance-C]

Step 3: Hash the aggregator key
        hash("auth-service") → maps to Instance-B

Step 4: Route aggregator to Instance-B

When Instance-D is added:
        Sorted: [Instance-A, Instance-B, Instance-C, Instance-D]
        Most aggregators stay on same instance
        Only ~25% need to move (consistent hashing property)
        No manual coordination needed ✅

Why This Works

Traditional Approach:          Dynamic Consistent Hashing:
┌──────────────────────┐      ┌──────────────────────────┐
│ Static cluster size  │      │ Dynamic cluster size     │
│ Manual rebalancing   │      │ Automatic rebalancing    │
│ Coordination service │      │ Service registry reused  │
│ Downtime during      │      │ Zero downtime scaling    │
│ scaling              │      │                          │
└──────────────────────┘      └──────────────────────────┘

Step 7: Time-Travel Queries

The Problem

Engineers need to ask: "What did the service topology look like during last Tuesday's incident?"

Three-Mechanism Solution

Mechanism 1: Time-Windowed Snapshots
┌─────────────────────────────────────────┐
│ Every 5 minutes, store aggregator state │
│                                         │
│ Key: (entity_id, timestamp)             │
│ Value: compressed aggregator state      │
│                                         │
│ Timeline:                               │
│ 12:00 ──[snapshot]──────────────────── │
│ 12:05 ──────────────[snapshot]───────── │
│ 12:10 ──────────────────────[snapshot]  │
└─────────────────────────────────────────┘

Mechanism 2: Property-Level Mutation Tracking
┌─────────────────────────────────────────┐
│ Only store CHANGED properties           │
│                                         │
│ 12:03 - health_status: "degraded"      │
│ 12:07 - error_rate: 0.15               │
│ 12:09 - health_status: "down"          │
│                                         │
│ Much more efficient than full copies    │
└─────────────────────────────────────────┘

Mechanism 3: Query-Time Reconstruction
┌─────────────────────────────────────────┐
│ To query topology at 12:08:             │
│                                         │
│ 1. Load snapshot from 12:05            │
│ 2. Apply mutations from 12:05-12:08    │
│ 3. Return reconstructed state          │
│                                         │
│ Fast: indexed lookups, no log replay   │
└─────────────────────────────────────────┘

Storage Efficiency Comparison

Full Snapshots:
Every 5 min × 288 snapshots/day × full graph size
= Exponential storage growth ❌

Event Sourcing (log replay):
All events stored, replay from beginning
= Slow reconstruction ❌

Combined Approach (Netflix):
Sparse snapshots + sparse mutations
= Efficient storage + fast queries ✅

Step 8: The Optimization Methodology

The Cascading Bottleneck Pattern

This is the most important meta-lesson:

Fix Kafka Lag
    ↓
Discover Hot Nodes
    ↓
Fix Hot Nodes
    ↓
Discover GC Pressure
    ↓
Fix GC Pressure
    ↓
Discover Serialization Issues
    ↓
Fix Serialization
    ↓
Discover Write Distribution Problems
    ↓
Fix Write Distribution
    ↓
Next bottleneck...

This is not failure. This is how distributed systems are built.

The Methodology

┌─────────────────────────────────────────────┐
│           OPTIMIZATION LOOP                 │
│                                             │
│  1. MEASURE                                 │
│     Profile CPU, memory, latency, GC        │
│     Use: async-profiler, heap dumps,        │
│           Kafka consumer metrics            │
│                                             │
│  2. HYPOTHESIZE                             │
│     Identify root cause from data           │
│     Don't guess — let metrics guide you     │
│                                             │
│  3. FIX                                     │
│     Target the specific bottleneck          │
│     Make one change at a time               │
│                                             │
│  4. VALIDATE                                │
│     Confirm improvement with metrics        │
│     Ensure no regression elsewhere          │
│                                             │
│  5. REPEAT                                  │
│     Move to next bottleneck                 │
└─────────────────────────────────────────────┘

When to Break "Best Practices"

Best Practice: Use immutable data structures
Reality at scale: Creates GC pressure at millions of 
                  allocations/second

Best Practice: Use gRPC for service communication  
Reality at scale: Heavyweight for streaming aggregation

Best Practice: Single-stage aggregation is simpler
Reality at scale: Creates hot nodes with skewed data

Rule: Best practices are starting points.
      Measurement justifies deviation.
      Document why you deviated.

Summary: Key Concepts Map

SERVICE TOPOLOGY AT SCALE
│
├── WHY STREAMING?
│   └── Batch = hours old → useless for incidents
│       Streaming = minutes old → actionable
│
├── HOW TO HANDLE LOAD?
│   └── Backpressure → slow down gracefully, never drop data
│
├── HOW TO RESOLVE NETWORK HOPS?
│   └── Three-stage pipeline
│       Stage 1: Compress (aggregation)
│       Stage 2: Resolve (intermediary removal)
│       Stage 3: Enrich + Persist
│
├── HOW TO DISTRIBUTE LOAD?
│   └── Dynamic consistent hashing via service registry
│       Multi-stage redistribution for skewed data
│
├── HOW TO QUERY HISTORY?
│   └── Snapshots + Mutations + Query-time reconstruction
│
└── HOW TO OPTIMIZE?
    └── Measure → Hypothesize → Fix → Validate → Repeat
        Break conventions when data justifies it

Self-Assessment Questions

  1. Why is a 1-hour-old topology map insufficient for incident response?
  2. Explain backpressure using a real-world analogy
  3. Why does resolving network intermediaries require at least two stages?
  4. What causes hot nodes and how does the three-stage pipeline prevent them?
  5. Under what conditions would you choose SSE over gRPC?
  6. How does time-travel querying avoid exponential storage costs?
  7. Why is discovering a new bottleneck after fixing one considered normal, not failure?

More to study