system-design · intermediate
Consensus Algorithms — Agreeing Despite Failures
The Central Question
Consider a critical distributed metadata cluster running on the TxLab platform (txlab.com) processing 100,000,000 queries per day:
- The cluster consists of 5 database nodes (
Node A,Node B,Node C,Node D,Node E) maintaining distributed lock ownership and service discovery state. - Suddenly,
Node Asuffers a hard hardware crash and goes offline. - Simultaneously, a network partition isolates
Node Efrom the rest of the cluster.
How do the remaining nodes (
Node B, Node C, Node D) elect a new cluster leader automatically, guarantee that no two nodes act as leader simultaneously (Split-Brain Disasters), and ensure that committed state modifications are never lost or corrupted?
This fundamental challenge is solved by Distributed Consensus Algorithms.
A Consensus Algorithm is a protocol that allows a set of independent network nodes to agree on a single data value, execution order, or state transition log—even when some nodes fail, drop packets, or exhibit network latency.
This lesson answers one central question: How does the Raft consensus algorithm decompose distributed agreement into Leader Election, Log Replication, and Safety invariants, and how do systems leverage Quorum Majority ($Q = \lfloor \frac{N}{2} \rfloor + 1$) and Term Leases to maintain linearizable state safety in production?
Replicated State Machines (RSM)
Consensus algorithms are typically deployed to build Replicated State Machines (RSM):
flowchart TD
Client[Application Client] -->|1. Submit Command| Leader[Leader Node]
subgraph Replicated State Machine Cluster
Leader -->|2. Write Command to Log| LeaderLog[Leader Consensus Log]
LeaderLog -->|3. AppendEntries RPC| FollowerLog1[Follower 1 Log]
LeaderLog -->|3. AppendEntries RPC| FollowerLog2[Follower 2 Log]
LeaderLog -->|4. Apply to State Machine| SM1[Leader State Machine]
FollowerLog1 -->|4. Apply to State Machine| SM2[Follower 1 State Machine]
FollowerLog2 -->|4. Apply to State Machine| SM3[Follower 2 State Machine]
end
Figure 1: Replicated State Machine architecture executing identical ordered logs across nodes.
The Core RSM Invariant
If two independent state machines start in the identical initial state and execute the identical sequence of deterministic commands from an ordered log, they will compute identical output states.
The Raft Consensus Protocol: 3 Core Sub-Problems
While Paxos was historically the dominant consensus protocol, its complexity made implementation notoriously difficult. In 2014, Diego Ongaro and John Ousterhout introduced Raft, which decomposes consensus into three clear sub-problems:
flowchart TD
Raft[Raft Consensus Decomposition] --> Election[1. Leader Election]
Raft --> Replication[2. Log Replication]
Raft --> Safety[3. Safety Invariants]
Election --> ElDesc["Select 1 Leader node per Term using Randomized Heartbeat Timeouts."]
Replication --> RepDesc["Leader accepts writes from clients, appends to log, and replicates to Followers."]
Safety --> SafeDesc["Enforce Election Safety, Leader-Only Writes, and Log Matching Properties."]
Figure 2: Taxonomy of Raft consensus sub-problems.
1. Node Roles in Raft
At any given moment, every node in a Raft cluster exists in one of three states:- Leader: Handles all client requests, manages log replication, and sends periodic heartbeats (
AppendEntries). - Follower: Passive state; responds to RPCs from Leaders and Candidates. If a Follower receives no heartbeats within its election timeout, it converts to a Candidate.
- Candidate: Active state during elections; requests votes (
RequestVote) from peer nodes to become Leader for a new Term.
Quorum Mathematics ($Q = \lfloor \frac{N}{2} \rfloor + 1$)
Raft requires a Quorum Majority of healthy nodes to elect a leader or commit a log entry:
$$Q = \left\lfloor \frac{N}{2} \right\rfloor + 1$$
For a cluster of $N = 5$ nodes, the quorum majority is:
$$Q = \left\lfloor \frac{5}{2} \right\rfloor + 1 = 2 + 1 = 3 \text{ nodes}$$
Cluster Fault Tolerance Table
| Cluster Size ($N$) | Quorum Majority ($Q$) | Maximum Tolerated Node Failures ($F$) |
|---|---|---|
| 3 Nodes | 2 Nodes | 1 Node |
| 5 Nodes | 3 Nodes | 2 Nodes |
| 7 Nodes | 4 Nodes | 3 Nodes |
[!IMPORTANT] Why Production Clusters Use Odd Node Counts: A 4-node cluster requires $Q = 3$ nodes, tolerating only $F = 1$ failure ($4 - 3 = 1$). A 5-node cluster also requires $Q = 3$ nodes, but tolerates $F = 2$ failures. Adding a 4th node increases network overhead without improving fault tolerance! Always deploy consensus clusters with odd node counts (3, 5, or 7).
Raft Invariants and Log Safety
Raft guarantees correctness through five strict safety properties:
- Election Safety: At most one leader can be elected per term.
- Leader Append-Only: A leader never overwrites or truncates its own log entries; it only appends new entries.
- Log Matching Property: If two logs contain an entry with the same index and term, then the logs are identical in all entries up through the given index.
- Leader Completeness: If a log entry is committed in a given term, that entry will be present in the logs of the leaders for all higher-numbered terms.
- State Machine Safety: If a server has applied a log entry at a given index to its state machine, no other server will ever apply a different log entry for that index.
Pre-Vote Phase (Preventing Disrupted Leaders)
If a partitioned node (`Node E`) is disconnected from the cluster, its election timer continuously expires, incrementing its `currentTerm` to term 50. When `Node E` reconnects to the cluster, its high term forces the healthy Leader to step down, triggering unnecessary re-elections. To prevent this, Raft engines implement a **Pre-Vote Phase**:- A Candidate node sends a speculative
PreVoteRPC to check if a majority quorum would grant a vote before incrementing its term. - If the majority quorum reports that a healthy leader is active, the Candidate drops its PreVote without incrementing the term, preserving cluster stability.
Joint Consensus Configuration Membership Changes
When adding or removing nodes from a live Raft cluster (e.g. expanding from 3 to 5 nodes), naive configuration changes risk split-brain conditions where old and new quorums overlap incorrectly. Raft uses **Joint Consensus**:- The cluster transitions through an intermediate joint configuration $C_{\text{old,new}}$ requiring separate majority quorums from BOTH $C_{\text{old}}$ and $C_{\text{new}}$ before committing to the final $C_{\text{new}}$ state.
Leader Leases for Zero-Latency Reads
To serve read queries without taking log replication consensus roundtrips on every `SELECT`, Raft leaders maintain a **Leader Lease**. As long as the leader receives heartbeat acknowledgments from a majority quorum within the lease window, it serves linearizable reads directly from local memory without invoking log replication.Complete Worked Example: Production Go Raft Leader Election & Log Engine
Let's inspect a complete Go implementation of a Raft Leader Election & Log Engine for the TxLab platform (txlab.com).
package main
import (
"context"
"fmt"
"math/rand"
"sync"
"time"
)
type NodeRole int
const (
RoleFollower NodeRole = iota
RoleCandidate
RoleLeader
)
type RaftNode struct {
mu sync.Mutex
id int
currentTerm int64
votedFor int
role NodeRole
peers []*RaftNode
heartbeat chan bool
log []string
}
func NewRaftNode(id int) *RaftNode {
return &RaftNode{
id: id,
currentTerm: 0,
votedFor: -1,
role: RoleFollower,
heartbeat: make(chan bool, 10),
log: make([]string, 0),
}
}
func (n *RaftNode) Run(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
n.mu.Lock()
role := n.role
n.mu.Unlock()
switch role {
case RoleFollower:
n.runFollower(ctx)
case RoleCandidate:
n.runCandidate(ctx)
case RoleLeader:
n.runLeader(ctx)
}
}
}
}
func (n RaftNode) runFollower(ctx context.Context) {
// Randomized Election Timeout (150ms - 300ms)
timeout := time.Duration(150+rand.Intn(150)) time.Millisecond
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-n.heartbeat:
// Reset timeout on heartbeat from Leader
case <-timer.C:
n.mu.Lock()
n.role = RoleCandidate
fmt.Printf("[ELECTION TIMEOUT] Node %d converted to CANDIDATE for Term %d\n", n.id, n.currentTerm+1)
n.mu.Unlock()
case <-ctx.Done():
return
}
}
func (n *RaftNode) runCandidate(ctx context.Context) {
n.mu.Lock()
n.currentTerm++
n.votedFor = n.id
term := n.currentTerm
n.mu.Unlock()
votes := 1
var wg sync.WaitGroup
var voteMu sync.Mutex
for _, peer := range n.peers {
wg.Add(1)
go func(p *RaftNode) {
defer wg.Done()
if p.RequestVote(n.id, term) {
voteMu.Lock()
votes++
voteMu.Unlock()
}
}(peer)
}
wg.Wait()
n.mu.Lock()
if votes >= (len(n.peers)+1)/2+1 {
n.role = RoleLeader
fmt.Printf("[ELECTION WON] Node %d elected LEADER for Term %d with %d votes!\n", n.id, term, votes)
} else {
n.role = RoleFollower
}
n.mu.Unlock()
}
func (n RaftNode) runLeader(ctx context.Context) {
fmt.Printf("[LEADER ACTIVE] Node %d sending heartbeats for Term %d...\n", n.id, n.currentTerm)
for _, peer := range n.peers {
peer.heartbeat <- true
}
time.Sleep(50 time.Millisecond)
}
func (n *RaftNode) RequestVote(candidateID int, term int64) bool {
n.mu.Lock()
defer n.mu.Unlock()
if term > n.currentTerm && (n.votedFor == -1 || n.votedFor == candidateID) {
n.currentTerm = term
n.votedFor = candidateID
return true
}
return false
}
Failure Modes and Engineering Mitigations
| Failure Scenario | Root Cause | System Symptom | Detection Signal | Mitigation Strategy |
|---|---|---|---|---|
| 1. Split Vote Election Loop | 2 Candidate nodes request votes simultaneously; neither achieves a majority quorum. | Election loops continuously without electing a leader; cluster writes block. | Repeated term increment alerts without leader election. | Use Randomized Election Timeouts (150ms-300ms) to stagger elections. |
| 2. Split-Brain Dual Leader | Network partition isolates old leader; remaining nodes elect a new leader. | Old leader accepts writes in minority partition that are later overwritten (Data Loss). | Term mismatch logs between isolated cluster nodes. | Enforce Quorum Majority Checks ($Q = \lfloor \frac{N}{2} \rfloor + 1$) on every write. |
| 3. Flapping Leader Distraction | Intermittent network packet loss causes followers to miss heartbeats and trigger unnecessary elections. | Frequent leader re-elections degrade cluster throughput by 80%. | High leader election rate metrics on monitoring dashboards. | Increase Heartbeat Timeout Margins and use Pre-Vote phases. |
| 4. Unbounded Log Growth | Raft consensus log appends millions of commands without compaction. | Server memory runs out; node startup recovery takes hours scanning disk log. | High Raft log byte size metrics on disk. | Implement Log Snapshotting (Compaction) to prune committed entries. |
What You Should Remember
- Raft decomposes consensus into 3 sub-problems: Leader Election, Log Replication, and Safety Invariants.
- Quorum Majority requires $Q = \lfloor \frac{N}{2} \rfloor + 1$: A 5-node cluster requires 3 node acknowledgments to elect leaders or commit logs.
- Deploy odd node counts: 3, 5, or 7 node clusters provide optimal fault tolerance without adding unnecessary network overhead.
- Randomize election timeouts: Stagger candidate election timers (150ms-300ms) to prevent split-vote election loops.
- Use Snapshot Compaction for log pruning: Compact committed Raft log entries into state machine snapshots to bound memory and startup recovery time.
Glossary of Terms
| Term | Definition |
|---|---|
| Consensus Algorithm | A protocol enabling distributed nodes to agree on data values or state transitions despite failures. |
| Replicated State Machine (RSM) | An architecture executing identical ordered command logs across nodes to maintain identical states. |
| Raft | A consensus algorithm designed for understandability, decomposing agreement into election, replication, and safety. |
| Quorum Majority | The minimum number of node votes ($\lfloor \frac{N}{2} \rfloor + 1$) required to elect a leader or commit entries. |
| Term | A monotonically increasing integer in Raft acting as a logical clock to distinguish leader epochs. |
| Log Snapshotting | The process of compacting committed log entries into a state checkpoint to free disk and RAM space. |
Practice Scenario and Self-Assessment
Architecture Scenario
You are designing the distributed lock manager for a cloud database platform (`locks.txlab.com`):- A 5-node Raft cluster (
Node 1toNode 5) manages distributed lease locks. Node 1(Leader) experiences a $500\text{ms}$ stop-the-world JVM Garbage Collection pause.
- Detail the step-by-step Raft election process triggered by
Node 2,Node 3,Node 4, andNode 5duringNode 1's GC pause. - Explain what happens when
Node 1completes its GC pause and attempts to issue writes under its old Term.
Interactive Self-Assessment
A 4-node cluster requires Q=3 nodes (tolerating F=1 failure), whereas a 5-node cluster also requires Q=3 nodes (tolerating F=2 failures). Adding an even node adds network overhead without increasing fault tolerance.
Even node counts automatically format persistent NVMe SSD disk drives on database servers.
Even node counts revoke client HTTPS TLS encryption certificates on edge load balancers.
Even node counts cut physical CPU hardware clock speeds in half across all consensus nodes.
It prevents Split Vote election loops by ensuring one follower times out first and collects majority votes before peers time out.
Randomized timeouts convert relational database primary key indexes into un-indexed CSV files.
Randomized timeouts replace public DNS nameservers with local hosts file entries.
Randomized timeouts reboot operating system hypervisors across all cluster nodes.
What to Learn Next
- Active-Active Conflict Resolution — CRDTs and Vector Clocks: Master multi-region write conflict resolution.
- Strong vs Eventual Consistency: Explore linearizability vs eventual consistency models.
- CAP Theorem & PACELC — Consistency vs Availability: Revisit network partition trade-offs.
Track: Distributed Systems
By Shubham Jain