Chapter 6: Distributed Systems Theory
Consensus, Paxos, Raft, Vector Clocks, Lamport Timestamps
Introduction
Distributed systems are fundamentally about coordination across independent machines that communicate via an unreliable network. This chapter explores the theoretical foundations that make distributed consensus possible—and the impossibility results that constrain what we can achieve.
Key questions this chapter answers:
- Why can't we rely on physical clocks?
- How do Lamport timestamps and vector clocks order events without synchronized time?
- What makes consensus theoretically hard (FLP, Two Generals)?
- How do Paxos, Raft, and ZAB achieve consensus in practice?
- What ordering guarantees can distributed systems provide?
Why Distributed Systems Are Hard
The Two Generals Problem
Two armies need to coordinate an attack. They can only communicate via messengers who may be captured. The problem: there is no protocol that guarantees both generals will attack simultaneously in the presence of message loss.
Reality: No matter how many acknowledgment rounds, the last sender can never know if their final message was received. This is an impossibility result.
The Byzantine Generals Problem
Extension where some generals may be traitors sending conflicting information. Requires 3f+1 nodes to tolerate f Byzantine faults. Much harder than crash faults.
| Failure Model | Description | Nodes Required | Examples |
|---|---|---|---|
| Crash Faults | Node stops responding | 2f+1 for f faults | Raft, Paxos, ZAB |
| Byzantine Faults | Node behaves maliciously | 3f+1 for f faults | PBFT, Blockchain consensus |
| Omission Faults | Node drops messages | Between crash & Byzantine | Network partitions |
Asynchronous vs Synchronous Models
- Message delay
- Processing time
- Clock drift"] C --> C1["Eventually synchronous
(Real systems)"] D --> D1["No timing assumptions
(FLP applies)"] style B fill:#2ecc71 style C fill:#f39c12 style D fill:#e74c3c
- Synchronous: Known upper bounds on message delay and processing time. Consensus is solvable.
- Asynchronous: No timing assumptions. FLP impossibility applies—deterministic consensus is impossible with even one crash fault.
- Partially Synchronous: Real-world model. System is synchronous most of the time but may violate bounds temporarily. Practical algorithms (Raft, Paxos) work here.
Partial Failures
The defining challenge: some components fail while others continue. Unlike a single machine where everything crashes together, distributed systems have nondeterministic partial failures.
Key insight: You cannot distinguish between:
- A crashed node
- A slow node
- A partitioned node
- A node whose response was lost
Time in Distributed Systems
Why Physical Clocks Fail
Physical clocks drift due to temperature, manufacturing variance, and relativistic effects. Even with NTP, you get:
- Clock skew: Clocks disagree on current time
- Clock drift: Clocks progress at different rates
- NTP accuracy: ~1-50ms on LAN, worse on WAN
even though X happened first!
Reality: NTP provides best-effort synchronization with millisecond-level error. Not sufficient for causality reasoning or distributed transactions.
GPS and Atomic Clocks
High-precision systems (Google Spanner, AWS Time Sync Service) use:
- GPS receivers: ~100ns accuracy
- Atomic clocks: Nanosecond precision
- TrueTime API (Spanner): Returns time interval [earliest, latest] with confidence bounds
Cost tradeoff: GPS/atomic clocks are expensive. Most systems use logical clocks instead.
Logical Clocks
If we can't trust physical time, how do we order events? Logical clocks capture causality without synchronized clocks.
Lamport Timestamps
Invented by Leslie Lamport (1978). Each process maintains a counter that captures the happens-before relation.
Happens-Before Relation (→)
- Local ordering: If a and b are events in the same process, and a occurs before b, then a → b
- Message ordering: If a is the send event and b is the receive event, then a → b
- Transitivity: If a → b and b → c, then a → c
Lamport Clock Rules
- Initial: Each process starts with clock = 0
- Local event: Increment clock before event
- Send message: Include clock value in message
- Receive message: clock = max(local_clock, message_clock) + 1
Limitations of Lamport Timestamps
If a → b, then L(a) < L(b). BUT the converse is NOT true: L(a) < L(b) does NOT imply a → b.
P1: L=5"] B["Event B
P2: L=7"] A -.->|"L(A) < L(B)
but A ∦ B"| B Note[Concurrent events!
Cannot determine order] style A fill:#3498db style B fill:#3498db style Note fill:#f39c12
Reality: They only capture potential causality. Cannot detect concurrent events.
Real-World Usage
- Distributed debugging: Reconstruct event ordering from logs
- Conflict-free ordering: Break ties in distributed databases (e.g., last-write-wins with Lamport timestamp)
- Snapshot isolation: Assign monotonic timestamps to transactions
Vector Clocks
Vector clocks extend Lamport timestamps to detect concurrency. Each process maintains a vector of clocks for all processes.
Vector Clock Rules
- Initial: V[i] = [0, 0, ..., 0] for all processes
- Local event at Pi: V[i][i]++
- Send message from Pi: Include V[i] in message
- Receive message at Pj: V[j][k] = max(V[j][k], V_msg[k]) for all k, then V[j][j]++
Comparing Vector Clocks
For vectors V1 and V2:
- V1 < V2 (happens-before) iff V1[i] ≤ V2[i] for all i, and V1 ≠ V2
- V1 = V2 (same event) iff V1[i] = V2[i] for all i
- V1 || V2 (concurrent) iff neither V1 < V2 nor V2 < V1
Space Overhead
Vector clocks require O(N) space per event, where N = number of processes. Problematic for large-scale systems (millions of clients).
| Clock Type | Space | Detects Causality | Detects Concurrency |
|---|---|---|---|
| Lamport | O(1) | Yes (one direction) | No |
| Vector | O(N) | Yes (both directions) | Yes |
| Dotted Version Vector | O(K), K=replicas | Yes | Yes |
Real-World Usage
- Dynamo/Riak: Vector clocks for detecting conflicting writes (siblings)
- Distributed debugging: ShiViz, Dinv visualize causality with vector clocks
- Version control: Git's commit graph is essentially a vector clock (SHA hashes)
Hybrid Logical Clocks (HLC)
Combines physical time and logical counters to get the best of both worlds. Used in CockroachDB and Google Spanner's external consistency.
HLC Structure
HLC = (pt, lc) where:
- pt: Physical time (wall clock)
- lc: Logical counter (disambiguates events at same pt)
HLC Rules
- Send/Local event:
- pt = max(pt, physical_time)
- If pt increased, lc = 0, else lc++
- Receive message:
- pt = max(pt, msg.pt, physical_time)
- If pt = msg.pt, lc = max(lc, msg.lc) + 1
- Else if pt = physical_time, lc++
- Else lc = 0
Physical time=100 P1->>P1: Event A Note over P1: (100, 1)
Same physical time, lc++ P1->>P2: Msg (100, 1) Note over P2: Physical time=99 Note over P2: (100, 2)
pt=max(99,100)=100
lc=max(0,1)+1=2 P2->>P2: Receive
Advantages
- Bounded drift from physical time: HLC closely tracks wall clock (unlike pure logical clocks)
- O(1) space: No vector overhead
- Efficient comparisons: Lexicographic order (pt, lc)
- Human-readable: Timestamps are approximate wall-clock times
Real-World Usage
- CockroachDB: HLC for MVCC timestamps and external consistency
- MongoDB (in research): Causal consistency with HLC
- Academic systems: OrbitDB, ScyllaDB experiments
The Consensus Problem
Consensus is the foundation of distributed coordination. Formal problem: N processes propose values; all must agree on one value.
Safety Properties
- Agreement: No two correct processes decide differently
- Validity: If all processes propose v, then v is the only possible decision (no trivial solutions)
- Termination: Every correct process eventually decides (liveness)
proposes A] --> C{Consensus
Protocol} P2[Process 2
proposes B] --> C P3[Process 3
proposes A] --> C P4[Process 4
proposes A] --> C C --> D1[Process 1
decides A] C --> D2[Process 2
decides A] C --> D3[Process 3
decides A] C --> D4[Process 4
decides A] style C fill:#e74c3c style D1 fill:#2ecc71 style D2 fill:#2ecc71 style D3 fill:#2ecc71 style D4 fill:#2ecc71
Why Consensus is Fundamental
Many distributed problems reduce to consensus:
- Leader election: Agree on which node is the leader
- State machine replication: Agree on order of operations
- Atomic commit: Agree on whether to commit or abort a transaction
- Group membership: Agree on which nodes are in the cluster
Consensus vs Atomic Broadcast
These are equivalent problems:
- Consensus: Agree on a single value
- Total Order Broadcast: Agree on a sequence of values (order of messages)
Atomic broadcast = repeated consensus (one consensus per message).
Paxos
Invented by Leslie Lamport (1989, published 1998). The first practical consensus algorithm. Notoriously difficult to understand and implement correctly.
Roles
- Proposers: Propose values
- Acceptors: Vote on proposals (quorum = majority)
- Learners: Learn the chosen value
In practice, nodes often play multiple roles.
Basic Paxos: Two Phases
Phase 1: Prepare/Promise
- Proposer selects proposal number
n(must be unique and increasing) - Sends PREPARE(n) to majority of acceptors
- Acceptors respond:
- If n > highest seen, promise not to accept proposals < n
- Return any previously accepted value (if any)
Phase 2: Accept/Accepted
- Proposer receives promises from majority
- Chooses value:
- If any acceptor returned a value, use the value with highest proposal number
- Otherwise, use proposer's own value
- Sends ACCEPT(n, value) to acceptors
- Acceptors accept if they haven't promised a higher number
- Learners learn the value once majority has accepted
Failure Scenarios
Dueling Proposers
Two proposers compete with increasing proposal numbers, preventing progress (livelock).
Solution: Randomized backoff or leader election (Multi-Paxos).
Multi-Paxos: Optimization for Log Replication
Running Basic Paxos for every log entry is expensive. Multi-Paxos optimizations:
- Stable leader: One proposer is elected leader; skips Prepare phase for subsequent proposals
- Batching: Leader proposes multiple entries in one round
- Pipelining: Don't wait for Accept responses before sending next proposal
to elect leader] C --> C1[Leader proposes entries
without Prepare phase] C --> C2[Accept phase only] style B fill:#e74c3c style C fill:#2ecc71
Why Paxos is Hard
- Opaque reasoning: Why does accepting the highest-numbered prior value work? Requires proof.
- Many variants: Basic Paxos, Multi-Paxos, Fast Paxos, Cheap Paxos, Byzantine Paxos...
- Implementation details: Lamport's paper left many practical issues unspecified (log compaction, leadership, membership changes)
- Terminology confusion: Proposals, ballots, rounds, instances...
Reality: Most systems use Raft or ZAB because they're easier to understand and implement. Google's Chubby used Paxos but the implementation was notoriously complex.
Real-World Usage
- Google Chubby: Early Paxos implementation (later evolved)
- Apache Cassandra: Uses Paxos for lightweight transactions (LWT)
- Azure Cosmos DB: Internally uses Paxos variants
Raft
Designed by Diego Ongaro and John Ousterhout (2014) to be more understandable than Paxos. "In search of an understandable consensus algorithm."
Design Philosophy
- Understandability: Decompose problem into leader election, log replication, safety
- Strong leader: Log entries only flow from leader to followers (simplifies reasoning)
- Term-based epochs: Time divided into terms with at most one leader per term
Leader A] --> T2[Term 2
No Leader
Election fails] T2 --> T3[Term 3
Leader B] T3 --> T4[Term 4
Leader B] style T1 fill:#2ecc71 style T2 fill:#f39c12 style T3 fill:#2ecc71 style T4 fill:#2ecc71
Raft Roles
- Leader: Handles all client requests, replicates log to followers
- Follower: Passive; responds to RPCs from leader and candidates
- Candidate: Follower that times out becomes candidate to request votes
Leader Election
Election Process
- Follower increments current term
- Transitions to Candidate
- Votes for itself
- Sends RequestVote RPCs to all peers
- Waits for:
- Win: Receives votes from majority → becomes Leader
- Lose: Receives AppendEntries from valid leader → becomes Follower
- Timeout: No winner (split vote) → new election
term=2, vote for self C->>P1: RequestVote(term=2) C->>P2: RequestVote(term=2) C->>P3: RequestVote(term=2) P1->>C: VoteGranted P2->>C: VoteGranted Note over C: Majority achieved (3/5)
Become Leader
Split Vote Prevention
Randomized election timeouts (e.g., 150-300ms) reduce likelihood of split votes.
Log Replication
AppendEntries RPC
Leader sends AppendEntries RPCs to followers:
- Heartbeat: Empty AppendEntries (no log entries) to maintain authority
- Log replication: Include new entries to replicate
Log Structure
Each log entry contains:
- Command: State machine command
- Term: Term when entry was created
- Index: Position in log
(term 1)"] L2["2: set y=2
(term 1)"] L3["3: set z=3
(term 2)"] L4["4: set x=4
(term 3)"] end subgraph "Follower 1 Log" F1["1: set x=1
(term 1)"] F2["2: set y=2
(term 1)"] F3["3: set z=3
(term 2)"] F4["4: set x=4
(term 3)"] end subgraph "Follower 2 Log (lagging)" F21["1: set x=1
(term 1)"] F22["2: set y=2
(term 1)"] end style L4 fill:#2ecc71 style F4 fill:#2ecc71
Commitment Rule
Leader commits entry when it's replicated on a majority of servers. Once committed, entry is durable and will be applied to all state machines.
index=10, term=3 L->>F1: AppendEntries(10, x=5) L->>F2: AppendEntries(10, x=5) L->>F3: AppendEntries(10, x=5) F1->>L: Success F2->>L: Success Note over L: Majority replicated (3/5)
Commit index=10 L->>C: OK Note over L: Next heartbeat informs followers L->>F1: AppendEntries(commitIndex=10) L->>F2: AppendEntries(commitIndex=10)
Safety Properties
Election Restriction
A candidate can only become leader if its log contains all committed entries. Voters reject candidates with less up-to-date logs.
Log comparison:
- Log A is more up-to-date than Log B if:
- Last entry has higher term, OR
- Same term but longer log
Leader Completeness
If a log entry is committed in term T, it will be present in the logs of all leaders for terms > T.
State Machine Safety
If a server has applied a log entry at index i, no other server will apply a different command at index i.
Log Inconsistencies and Repair
Followers' logs may diverge due to crashes. Leader forces followers' logs to match its own.
Inconsistency Example
Repair Process
- Leader sends AppendEntries with prevLogIndex and prevLogTerm
- Follower checks if entry at prevLogIndex matches prevLogTerm
- If mismatch, follower rejects
- Leader decrements nextIndex and retries
- When match found, follower deletes conflicting entries and appends new ones
Append entries 3-4
Membership Changes
Adding/removing servers safely is tricky. Raft uses joint consensus (C_old,new configuration).
Joint consensus] B --> C[C_new] style B fill:#f39c12
During C_old,new:
- Log entries replicated to all servers in both configurations
- Any server from either configuration can be leader
- Agreement requires majorities from BOTH C_old and C_new
Raft vs Paxos
| Aspect | Raft | Paxos (Multi-Paxos) |
|---|---|---|
| Understandability | Designed for clarity | Notoriously complex |
| Leadership | Strong leader (only leader proposes) | Weak leader (others can propose) |
| Terms/Epochs | Explicit term numbers | Implicit in proposal numbers |
| Log gaps | No gaps allowed | Gaps possible (out-of-order commits) |
| Membership changes | Joint consensus (well-specified) | Underspecified in original paper |
| Performance | Similar (slight overhead from strong leader) | Similar (flexibility may help in some scenarios) |
Real-World Usage
- etcd: Kubernetes' coordination service
- Consul: Service discovery and configuration
- TiKV: Distributed key-value store (TiDB's storage layer)
- CockroachDB: Range replication uses Raft
- Dgraph: Graph database uses Raft for replication
Raft Log Compaction & Snapshotting
Problem: In production, the log grows unbounded. After 1 million entries, a new follower joining the cluster would take forever to replay the entire log from index 1. Memory usage grows indefinitely.
Solution: Periodic Snapshots
Each server periodically takes a snapshot of its state machine at log index N, then discards all log entries ≤ N. This prevents unbounded log growth.
x=100, y=200, z=300
lastIndex=1M, lastTerm=5] S --> L6[1M+1: new entry] L6 --> L7[1M+2: new entry] end style S fill:#2ecc71
InstallSnapshot RPC
When a follower is too far behind (the log entries it needs have been discarded), the leader sends the entire snapshot using the InstallSnapshot RPC.
Log starts at 1M+1 L->>F: AppendEntries(prevIndex=1M, ...) F->>L: ❌ Reject (I only have up to 500K) Note over L: Follower too far behind
Send snapshot L->>F: InstallSnapshot(lastIndex=1M, lastTerm=5, data) Note over F: Discard log 1-500K
Apply snapshot state F->>L: ✓ OK L->>F: AppendEntries(prevIndex=1M, entry 1M+1) F->>L: ✓ Success
When to Snapshot
Snapshotting policy is configurable per deployment:
- Entry-based: Every 10,000 entries (typical for small state machines)
- Size-based: When log exceeds 100MB (common in production)
- Time-based: Every hour (for predictable background load)
Safety Guarantees
Each snapshot includes:
- Last included index: Highest log entry included in snapshot
- Last included term: Term of that entry
- State machine data: Complete state (e.g., key-value pairs)
When a follower receives InstallSnapshot:
- If snapshot is older than its current state, ignore
- Discard its log up to
last_included_index - Replace state machine with snapshot data
- Update metadata (currentTerm, lastApplied)
Performance Impact
// Without snapshotting (1 year uptime)
Log entries: 100M entries (~10GB memory)
New follower join: 45 minutes (replay 100M entries)
Memory per server: 10GB (log) + state machine
// With snapshotting (every 10K entries)
Log entries: ~10K entries (~1MB memory)
New follower join: 30 seconds (InstallSnapshot + replay 10K)
Memory per server: 1MB (log) + state machine
Real-World Example: etcd
etcd uses Raft and automatically snapshots the key-value store:
- Default: Snapshot every 10,000 applied entries
- Snapshot stored in
dbfile (BoltDB backend) - Old log entries compacted, reducing memory usage
- New members join by receiving snapshot + recent log
Viewstamped Replication (VR)
Invented by Barbara Liskov and Brian Oki (1988), predating Paxos publication. Similar to Raft but less well-known.
Key Concepts
- View: Analogous to Raft's term. Each view has a primary (leader).
- View change: When primary fails, replicas perform view change to elect new primary
- Normal operation: Primary orders requests, sends to backups
VR vs Raft
- Similarities: Strong primary, log replication, view changes
- Differences:
- VR allows non-primary to initiate view change (Raft uses randomized timeouts)
- VR commit protocol is slightly different (explicit COMMIT message)
- VR has more complex recovery protocol
ZAB (ZooKeeper Atomic Broadcast)
The consensus protocol used by Apache ZooKeeper. Optimized for ZooKeeper's workload (reads >> writes).
Phases
- Leader Election: Fast Leader Election (FLE) algorithm
- Discovery: New leader discovers most recent state from followers
- Synchronization: Leader syncs followers to its state
- Broadcast: Normal operation, atomic broadcast of writes
Transaction IDs (zxid)
ZAB uses 64-bit zxid = (epoch, counter):
- Epoch: Leader election round (high 32 bits)
- Counter: Sequential transaction counter (low 32 bits)
Broadcast Protocol
Leader proposes transactions using two-phase commit:
zxid=0x0300000001 L->>F1: PROPOSE(zxid, data) L->>F2: PROPOSE(zxid, data) F1->>L: ACK(zxid) F2->>L: ACK(zxid) Note over L: Quorum ACKed L->>F1: COMMIT(zxid) L->>F2: COMMIT(zxid) L->>C: OK
Ordering Guarantees
- Reliable delivery: If message m is delivered by one server, it will be delivered by all servers
- Total order: All servers deliver messages in the same order
- Causal order: If m1 → m2 (causally), m1 delivered before m2
- Prefix property: If m is the i-th message delivered, all messages before i are also delivered
ZAB vs Raft
| Aspect | ZAB | Raft |
|---|---|---|
| Primary use case | Coordination service (ZooKeeper) | General-purpose replicated log |
| Epochs | Explicit in zxid (high 32 bits) | Term numbers |
| Discovery phase | Separate phase after election | Implicit in leader election restrictions |
| Commit protocol | Explicit COMMIT messages | Implicit via commitIndex |
| Reads | Can read from followers (with sync) | Read from leader or with ReadIndex |
Leader Election Patterns
Leader election is a common distributed systems problem with many solutions depending on assumptions.
Bully Algorithm
Simple algorithm assuming processes have unique IDs and can communicate with all others.
Algorithm
- Process P notices leader failure
- P sends ELECTION message to all processes with higher IDs
- If no response, P becomes leader and announces
- If response received, P waits for new leader announcement
- Higher-ID processes repeat the process
Become leader P4->>P1: COORDINATOR P4->>P2: COORDINATOR P4->>P3: COORDINATOR
Pros: Simple, highest ID always wins
Cons: O(N²) messages, not Byzantine-tolerant, assumes reliable failure detection
Ring Algorithm
Processes arranged in logical ring. Election message circulates, collecting IDs.
Algorithm
- Process P detects leader failure, sends ELECTION message with its ID
- Each process adds its ID to the list and forwards
- When message returns to initiator, it selects the highest ID and announces
Pros: O(N) messages
Cons: Single point of failure (ring break), slow if ring is large
Raft Leader Election (Randomized Timeouts)
Already covered in detail. Key innovation: randomized election timeouts prevent split votes.
ZooKeeper-Based Leader Election
Use ZooKeeper's ephemeral sequential znodes for leader election.
Algorithm
- Each process creates ephemeral sequential znode (e.g., /election/member_0000000001)
- Process with lowest sequence number is leader
- Others watch the znode immediately before theirs
- If watched znode disappears, check if now the leader
Leader] Z --> M2[member_0000000002
watches M1] Z --> M3[member_0000000003
watches M2] style M1 fill:#2ecc71
Pros: Simple, uses ZooKeeper's guarantees, avoids herd effect
Cons: Requires ZooKeeper (external dependency)
Consensus-Based Leader Election
Use consensus protocol (Paxos, Raft) to agree on leader. This is how Raft's leader election works internally.
Comparison
| Algorithm | Messages | Assumptions | Use Case |
|---|---|---|---|
| Bully | O(N²) | Reliable failure detection, synchronous | Small clusters, academic |
| Ring | O(N) | Ring topology | Academic |
| Raft | O(N) | Majority available | Production (etcd, Consul) |
| ZooKeeper | O(1) | ZooKeeper available | Application-level LE |
| Paxos | O(N) | Majority available | Production (Chubby) |
State Machine Replication (SMR)
Fundamental technique for building fault-tolerant services. Idea: replicate a deterministic state machine across multiple servers.
Concept
If multiple state machines:
- Start in the same initial state
- Apply the same sequence of deterministic commands
- Then they will reach the same final state
Requirements
- Determinism: Same input → same output (no randomness, no reading wall clock)
- Total order: All replicas apply commands in the same order
- Consensus: Use consensus protocol to agree on command order
Architecture
Handling Non-Determinism
Real-world operations are often non-deterministic. Solutions:
- Leader dictates: Leader computes non-deterministic values (timestamps, random numbers) and includes in log entry
- Deterministic transformations: Convert non-deterministic operations to deterministic (e.g., "get current time" → "time is 12:00:00.123")
- External state: Store non-deterministic data (e.g., file contents) in log entry
Examples
- etcd: Key-value store replicated via Raft SMR
- ZooKeeper: Coordination service using ZAB SMR
- Chubby: Google's lock service using Paxos SMR
- CockroachDB: Each range is a Raft SMR
Failure Detectors
Mechanisms for detecting when a node has failed. Critical for consensus protocols in partially synchronous systems.
Perfect vs Eventually Perfect
| Property | Perfect Failure Detector | Eventually Perfect FD |
|---|---|---|
| Strong Completeness | Every crashed process is eventually detected | Every crashed process is eventually detected |
| Strong Accuracy | No correct process is ever suspected | Eventually, no correct process is suspected |
| Achievable? | Only in synchronous systems | Yes, in partially synchronous systems |
Heartbeat-Based Detection
Most common approach: send periodic heartbeats, suspect node if timeout expires.
Suspect T failed
Challenges
- False positives: Timeout too short → suspect slow nodes
- False negatives: Timeout too long → slow to detect real failures
- Network variability: Delays vary unpredictably
Phi Accrual Failure Detector
Adaptive failure detector that outputs a suspicion level (φ) instead of binary alive/dead. Used in Cassandra, Akka.
How It Works
- Record heartbeat arrival times
- Compute mean (μ) and std deviation (σ) of inter-arrival times
- When new heartbeat expected, compute φ based on how late it is:
- φ = -log₁₀(P(now - last_heartbeat | μ, σ))
- Higher φ → higher suspicion
Advantages
- Adaptive: Adjusts to network conditions
- Tunable: Set threshold based on application needs
- Gradual: Suspicion increases gradually (no sudden flip)
Timeout Tuning
Tradeoff between false positives and detection speed:
Best practices:
- Set timeout > 99th percentile of network RTT
- Use adaptive detectors (Phi Accrual) for variable networks
- Monitor false positive rate
- Increase timeout during known network issues
Impossibility Results
Theoretical limits on what distributed systems can achieve. These results constrain practical system design.
FLP Impossibility
Fischer, Lynch, Paterson (1985): No deterministic consensus protocol can tolerate even one crash failure in an asynchronous system.
Assumptions
- Asynchronous: No bounds on message delays or processing time
- Deterministic: No randomization
- Crash faults: At most one process can crash
Proof Sketch
FLP shows there exists an execution where the protocol never terminates (consensus is not achieved), because:
- An adversary can delay messages such that the system is always in a "bivalent" state (could go either way)
- No process can distinguish between a crashed process and a very slow one
- Therefore, no deterministic protocol can guarantee termination
Bivalent] --> B[Deliver message to P1] A --> C[Deliver message to P2] B --> D[Still Bivalent] C --> E[Still Bivalent] D --> F[Adversary keeps system bivalent forever] E --> F style A fill:#f39c12 style D fill:#f39c12 style E fill:#f39c12 style F fill:#e74c3c
Circumventing FLP
Practical systems work despite FLP because they relax assumptions:
- Randomization: Ben-Or's randomized consensus, Byzantine agreement
- Partial synchrony: Assume synchrony holds eventually (Paxos, Raft, ZAB)
- Failure detectors: Eventually perfect FD allows consensus
- Weaker guarantees: Eventual consistency, CRDTs (no consensus needed)
Reality: FLP applies to purely asynchronous systems. Real networks are partially synchronous (synchronous most of the time), allowing Paxos/Raft/ZAB to work.
CAP Theorem (Impossibility Angle)
Revisiting CAP from a theoretical perspective:
Theorem: In the presence of a network partition (P), you must choose between Consistency (C) and Availability (A).
Proof Sketch
- Assume system is CA (consistent and available during partition)
- Network partitions into two groups: G1 and G2
- Client writes v1 to G1 (must succeed for availability)
- Client reads from G2 (must return latest value for consistency)
- But G2 never received v1 (partition!) → Contradiction
What CAP Really Means
- Not binary: Spectrum of consistency and availability during partitions
- Partitions are rare: When no partition, can be CA
- Tunable: Choose consistency level per operation (Cassandra: QUORUM, ONE, ALL)
Other Impossibility Results
- Two Generals Problem: No protocol for agreement over unreliable channels
- Consensus lower bound: Ω(N²) messages required for Byzantine agreement in worst case
- Atomic commit impossibility: No non-blocking atomic commit protocol (2PC blocks on coordinator failure)
Consistency & Replication
Replication improves availability and performance but introduces consistency challenges. Multiple replication strategies exist.
Primary-Backup Replication
One primary handles all writes; backups replicate. Simple but primary is a bottleneck.
Variants:
- Synchronous: Wait for all backups to ack (strong consistency, low availability)
- Asynchronous: Don't wait for backups (eventual consistency, high availability)
- Semi-synchronous: Wait for k backups (MySQL semi-sync, Kafka min.insync.replicas)
Chain Replication
Nodes arranged in a chain. Writes go through head → tail, reads from tail. Used in CRAQ, Azure Storage.
Advantages
- Strong consistency: Reads from tail see all committed writes
- High throughput: Head and tail can process different requests
- Simple recovery: If head fails, next node becomes head
Disadvantages
- Latency: Writes must traverse entire chain
- Single read point: Tail is bottleneck for reads
Quorum-Based Replication
Generalization of majority. Configure read quorum (R) and write quorum (W) such that R + W > N.
Quorum Condition
If R + W > N, then read and write quorums overlap → read sees latest write.
read sees latest write] style N3 fill:#2ecc71
Used in: Dynamo, Cassandra, Riak (tunable R/W)
Gossip Protocols (Epidemic Protocols)
Nodes periodically exchange state with random peers. Information spreads like an epidemic.
Basic Gossip
- Each node maintains local state
- Periodically, select random peer and exchange state
- Merge state (resolve conflicts if needed)
Properties
- Eventual consistency: All nodes eventually receive updates
- Scalability: O(log N) rounds to reach all nodes
- Fault tolerance: Works even if some messages are lost
- Simple: No central coordinator
Used in: Cassandra (anti-entropy), Riak (cluster membership), Consul (node discovery)
| Replication Type | Consistency | Write Latency | Read Latency | Complexity |
|---|---|---|---|---|
| Primary-Backup (sync) | Strong | High | Low | Low |
| Primary-Backup (async) | Eventual | Low | Low | Low |
| Chain | Strong | High (chain traversal) | Low (tail only) | Medium |
| Quorum | Tunable | Medium (W nodes) | Medium (R nodes) | Medium |
| Gossip | Eventual | Low (async) | Low | Medium |
Ordering Guarantees
Distributed systems provide different ordering guarantees depending on their design. Stronger guarantees are more expensive.
Ordering Hierarchy
Cheapest] --> A E2[Strongest
Most Expensive] --> E style A fill:#e74c3c style E fill:#2ecc71
FIFO Ordering
Messages from the same sender are delivered in the order they were sent. No guarantees across different senders.
can be interleaved arbitrarily
Example: TCP guarantees FIFO ordering within a connection.
Causal Ordering
Respects causality (happens-before relation). If event A causally precedes event B (A → B), then A is delivered before B.
because A → B (causal) P1->>P3: C (What's for lunch?) Note over P3: C can be delivered anytime
(C || B, concurrent)
Implementation: Vector clocks, causal broadcast algorithms
Example: MongoDB causal consistency, session guarantees
Total Order Broadcast
All processes deliver messages in the same order. Equivalent to consensus.
Used in: Raft (log replication), ZAB (atomic broadcast), Kafka (within partition)
Total Order vs Causal Order
- Causal: Respects causality but allows concurrent messages to be delivered in different orders
- Total: All messages have a global order, even concurrent ones
Kafka's Ordering Guarantees
Kafka provides total order within a partition, but no ordering across partitions.
offset=0] --> P0_M2[Message 2
offset=1] P0_M2 --> P0_M3[Message 3
offset=2] end subgraph "Partition 1" P1_M1[Message A
offset=0] --> P1_M2[Message B
offset=1] end Note[Messages in different partitions
can be consumed in any order] style P0_M3 fill:#2ecc71 style P1_M2 fill:#2ecc71
Key Takeaways
- Messages with same key go to same partition (total order)
- Consumer sees messages in offset order within partition
- No ordering guarantees across partitions
Linearizability
Strongest consistency model. Operations appear to execute instantaneously at some point between their invocation and response (real-time ordering).
Formal definition: There exists a total order of operations such that:
- Respects real-time ordering (if op A completes before op B starts, A comes before B)
- Each read returns the value written by the most recent write in the total order
Write completes before read starts C2->>DB: Read(x) DB->>C2: x=1 (must return 1, not 0)
Reality: Serializability is about transactions (equivalent to some serial execution). Linearizability is about single operations (respects real-time ordering). Can have serializability without linearizability (snapshot isolation) and vice versa.
Ordering Guarantees Comparison
| Guarantee | Definition | Cost | Examples |
|---|---|---|---|
| FIFO | Per-sender order preserved | Low | TCP |
| Causal | Respects happens-before | Medium | MongoDB causal, CRDTs |
| Total Order | All nodes see same order | High (consensus) | Raft, ZAB, Kafka (per partition) |
| Linearizable | Real-time total order | Highest | Spanner, etcd, ZooKeeper |
Real-World Examples
etcd: Linearizable Reads
etcd provides linearizable reads by default (every read goes through Raft). For better performance, can use serializable reads (read from any replica, may be stale).
Cassandra: Eventual Consistency
By default, no ordering guarantees. Can configure quorum reads/writes for stronger consistency.
Spanner: External Consistency
Stronger than linearizability—uses TrueTime to provide external consistency (if txn T1 commits before txn T2 starts in real time, T1's timestamp < T2's timestamp).
MongoDB: Causal Consistency
Client sessions provide causal consistency—reads see all writes that causally precede them.
Advanced Consensus & Membership Protocols
EPaxos (Egalitarian Paxos)
EPaxos is a leaderless consensus protocol that eliminates the single leader bottleneck of Raft and Multi-Paxos. In EPaxos, ANY replica can propose a command without routing through a designated leader, making it optimal for geo-distributed systems where the "closest" replica should handle the request.
The key innovation is the fast path vs slow path decision: If a command does not conflict with other concurrent commands (e.g., writes to different keys), it commits in just 1 RTT (one round trip). If conflicts are detected, EPaxos falls back to a slow path requiring an extra coordination round—but this is still faster than routing all requests through a remote leader in a different datacenter.
Trade-offs: EPaxos is significantly more complex than Raft (harder to implement and debug). It shines in geo-distributed deployments where leader bottlenecks dominate, but offers little advantage in single-datacenter setups where Raft's simplicity wins. CockroachDB's design was influenced by EPaxos, though it uses Raft in practice.
| Protocol | Leader? | Best-Case Latency | Complexity | Geo-Distributed? |
|---|---|---|---|---|
| Raft | Single leader | 2 RTTs (client → leader → followers → client) | Low | ❌ Leader bottleneck |
| Multi-Paxos | Stable leader | 2 RTTs (similar to Raft) | Medium | ❌ Leader bottleneck |
| EPaxos | Leaderless | 1 RTT (no conflicts) | High | ✅ Optimal for geo-replication |
SWIM Protocol (Scalable Membership)
Traditional membership protocols use all-to-all heartbeats: every node pings every other node, resulting in O(N²) network overhead. SWIM (Scalable Weakly-consistent Infection-style Process Group Membership) solves this with a clever combination of random probing + indirect probing + gossip dissemination.
How SWIM works: Each node randomly selects K peers per protocol period and sends a ping. If a node doesn't respond, the prober asks K' other nodes to send indirect pings (maybe the direct path is broken, but the node is alive). If all indirect pings fail, the node is marked "suspect" and this suspicion is disseminated via piggybacked gossip on subsequent pings. If no refutation arrives within a timeout, the node is declared failed.
Properties: SWIM achieves constant load per node (O(1) messages sent per period), fast failure detection (~2 protocol periods), and O(log N) convergence time for membership changes to propagate. It is robust to packet loss (indirect probing provides redundancy) and scales to thousands of nodes.
| Protocol | Network Overhead | Failure Detection Time | Scalability | Used By |
|---|---|---|---|---|
| All-to-All Heartbeats | O(N²) | 1 heartbeat interval | ❌ Poor (<100 nodes) | Simple systems |
| Gossip-Only | O(N log N) | Slow (many rounds) | ✅ Good (1000s nodes) | Cassandra |
| SWIM | O(N) | ~2 protocol periods | ✅ Excellent (1000s nodes) | Consul (Serf), Uber Ringpop |
Network Partition Testing (Jepsen)
Jepsen is a testing framework created by Kyle Kingsbury (aphyr) that exposes distributed systems bugs by injecting network partitions and verifying consistency guarantees. The methodology: start a cluster, run concurrent client operations, inject network partitions (via iptables), and then check if the database violated its claimed guarantees (e.g., linearizability, serializability).
Famous Jepsen findings: Redis Sentinel lost acknowledged writes during partitions. MongoDB violated read-your-writes consistency. Elasticsearch lost acknowledged documents. VoltDB exhibited split-brain. The key insight: most databases that CLAIM strong consistency actually violate it under partitions. Many systems pass tests under normal conditions but fail catastrophically when the network misbehaves.
How to test your own system: Use tools like Toxiproxy (inject latency/partitions at TCP level), Chaos Mesh (Kubernetes-native chaos engineering), or Linux tc (traffic control). Write tests that inject faults during writes, then verify that reads respect your consistency model. For linearizability testing, use a checker that builds a partial order of operations and verifies it's consistent with a total order that respects real-time ordering.
| Tool | Purpose | Target | Use Case |
|---|---|---|---|
| Jepsen | Consistency verification | Databases, coordination services | Test if system violates claimed guarantees |
| Toxiproxy | Network fault injection | Any TCP service | Inject latency, packet loss, partitions |
| Chaos Mesh | Kubernetes chaos engineering | K8s pods/services | Network/pod/DNS faults in K8s |
| Linux tc | Traffic shaping | Linux network interfaces | Delay, drop, reorder packets |
Merkle DAGs
A Merkle DAG (Directed Acyclic Graph) is a data structure where each node is content-addressed—its identifier is the cryptographic hash of its content plus references to child nodes. This makes the entire structure tamper-evident: any change to any node changes all ancestor hashes up to the root.
Git: Commits form a Merkle DAG where each commit is the hash of its tree (file snapshots) plus parent commit(s). This enables O(1) integrity verification: compare root hashes; if equal, entire histories are identical. IPFS: Files are split into content-addressed blocks linked via Merkle DAG, enabling deduplication (same block = same hash) and distributed retrieval (fetch blocks from any peer). Blockchain: Block headers form a hash chain (special case of Merkle DAG) where each block references the previous block's hash.
System design use case: Efficient sync between replicas. Instead of transferring entire datasets, compare root hashes. If they differ, traverse the DAG to find the divergence point and sync only the differing subtrees. This is how Git efficiently determines what objects to send during push/pull, and how IPFS replicates content across peers.
| System | Merkle DAG Structure | Key Property | Benefit |
|---|---|---|---|
| Git | Commits → trees → blobs | Content-addressed commits | Integrity + efficient push/pull |
| IPFS | Files → blocks (chunked) | Content-addressed blocks | Deduplication + P2P retrieval |
| Blockchain | Block headers (hash chain) | Tamper-evident history | Trustless verification |
| Distributed DB Sync | Data shards → Merkle DAG | Compare root hashes | Efficient sync (only diff subtrees) |
Conclusion
Distributed systems theory provides the foundation for building correct, fault-tolerant systems. Key takeaways:
- Time is tricky: Physical clocks are unreliable; use logical clocks (Lamport, Vector, HLC) for causality
- Consensus is fundamental: Many problems (leader election, SMR, atomic commit) reduce to consensus
- Paxos vs Raft: Both solve consensus; Raft is designed for understandability
- Impossibility results constrain us: FLP says deterministic consensus is impossible in async systems, but partial synchrony allows practical protocols
- Ordering is a spectrum: Stronger ordering guarantees (linearizability) are more expensive than weaker ones (causal, eventual)
- Failure detection is imperfect: Adaptive detectors (Phi Accrual) handle network variability better than fixed timeouts
- Understand why protocols work (safety/liveness proofs), not just mechanics
- Know real-world systems (etcd uses Raft, Cassandra uses gossip + quorums, ZooKeeper uses ZAB)
- Be able to reason about tradeoffs (consistency vs availability, latency vs correctness)
- Explain impossibility results and how practical systems circumvent them
Further Reading
- Raft Consensus Algorithm — Interactive visualization
- "Designing Data-Intensive Applications" by Martin Kleppmann — Chapters 8-9 (Distributed Systems)
- "Distributed Systems" by Maarten van Steen & Andrew Tanenbaum
- Leslie Lamport's papers: "Time, Clocks, and the Ordering of Events" (1978), "The Part-Time Parliament" (Paxos, 1998)
- FLP Paper: "Impossibility of Distributed Consensus with One Faulty Process" (1985)