Surviving the Split-Brain (Distributed Consensus)
1. Problem Statement & Scope
A student confidently explains that they have built a Highly Available (HA) database by deploying two master nodes. If Master A goes down, a load balancer automatically routes all traffic to Master B. They tell you they have achieved 100% uptime.
You pull the literal plug on the network switch connecting Data Center A to Data Center B. Both masters are still powered on, but they cannot talk to each other. Because they cannot see each other, both assume the other is dead. Both promote themselves to the primary writer. For three hours, Master A accepts writes from European clients, while Master B accepts writes from Asian clients.
When you plug the switch back in, the datasets are hopelessly divergent. The student has just discovered the Split-Brain Problem. To fix it, we must abandon simple active-passive failovers and introduce a rigorous Distributed Consensus algorithm like Raft.
Functional Requirements
- Leader Election: The cluster must autonomously elect a single leader to serialize all write operations.
- Log Replication: The leader must safely replicate state changes to follower nodes in the exact same order.
Non-Functional Requirements
- Partition Tolerance: The system must continue to operate correctly as long as a strict majority (quorum) of nodes remain connected.
- Strict Consistency: A read immediately following a successful write must return the updated value, regardless of which node serves the read.
2. High-Level Design (HLD)
To survive a network partition, a consensus cluster must always have an odd number of nodes (typically 3, 5, or 7). This guarantees that in the event of a network split, one side will always hold a mathematical majority (the quorum), while the other side is mathematically frozen.
3. The Viva: Pushing the Boundaries
Q:In the diagram above, Node 4 was the original leader before the network cable was cut. Client B sends a write to Node 4. Since Node 4 doesn’t know it has been cut off from the main group yet, does it execute the write and corrupt the database? Reveal ▾
No, the database is safe. This is the beauty of the quorum requirement.
When Node 4 receives the write from Client B, it appends the entry to its local log and sends an AppendEntries RPC to all other nodes. Because the network is partitioned, it only reaches Node 5. Node 4 only has 2 out of 5 votes. It will never achieve the majority (3 votes) required to officially commit the log entry. The write hangs or times out, and Client B receives an error. The isolated minority degrades gracefully into a halted state, prioritizing Consistency over Availability (CP in the CAP theorem).
Q:The network cable is repaired. Node 4 and Node 5 rejoin the cluster. Node 4 still thinks it is the Leader. Node 1 also thinks it is the Leader. How does the Raft algorithm mathematically resolve this without manual intervention? Reveal ▾
It resolves it using Term Numbers (Logical Clocks).
In Raft, time is divided into logical terms, represented by a monotonically increasing integer. When Node 4 was isolated, its term was, for example, Term 2. Meanwhile, Nodes 1, 2, and 3 noticed the leader was gone, incremented the term to Term 3, and elected Node 1.
When the network heals, Node 4 sends a heartbeat claiming to be the leader of Term 2. Node 1 receives this, looks at its own term (Term 3), and instantly rejects the message, replying with its higher term number. The moment Node 4 sees a term higher than its own, the Raft protocol dictates it must instantly step down, revert to a Follower state, and overwrite its uncommitted logs with Node 1’s authoritative log.
Q:A chaotic edge case: Node 3 has a faulty network card. It constantly drops packets. It stops receiving heartbeats from Node 1, assumes the leader is dead, increments its term to Term 4, and calls an election. It fails to get votes, times out, increments to Term 5, and repeats. When the network stabilizes, Node 3 rejoins with Term 100. What catastrophic event does this trigger? Reveal ▾
This is known as the Disruptive Server Problem.
Even though Node 1 is a perfectly healthy leader, when Node 3 finally reconnects and broadcasts its RequestVote RPC with Term 100, Node 1 sees a term vastly higher than its own. Node 1 is forced to step down immediately. The entire healthy cluster is thrown into chaos, forcing a brand new election just because one node had a loose cable.
To solve this, enterprise implementations of Raft introduce a Pre-Vote phase. Before a node is allowed to increment its term number and disrupt the cluster, it must ping the other nodes to ask, “If I were to call an election, would you even vote for me?” Since the other nodes are receiving healthy heartbeats from Node 1, they will say “No,” and Node 3 is prevented from incrementing its term and hijacking the cluster.
4. Low-Level Design (LLD): The Raft State Machine
At its core, a consensus node is a finite state machine. Below is a concise Python conceptualization of the transition logic that governs how a Follower becomes a Leader.
import threading
import time
from enum import Enum
from typing import List, Dict
class NodeState(Enum):
FOLLOWER = 1
CANDIDATE = 2
LEADER = 3
class RaftNode:
def __init__(self, node_id: int, peers: List[int]):
self.node_id = node_id
self.peers = peers
# Persistent state on all servers
self.current_term = 0
self.voted_for = None
self.log = []
# Volatile state
self.state = NodeState.FOLLOWER
self.commit_index = 0
self.last_heartbeat = time.time()
def on_election_timeout(self):
"""Triggered if no heartbeat is received within the randomized timeout window."""
if self.state == NodeState.LEADER:
return
print(f"Node {self.node_id}: Election timeout. Transitioning to Candidate.")
self.state = NodeState.CANDIDATE
self.current_term += 1
self.voted_for = self.node_id
# Start the election process
self.request_votes()
def request_votes(self):
votes_received = 1 # Votes for itself
quorum = (len(self.peers) + 1) // 2 + 1
for peer in self.peers:
# In LLD, this is an asynchronous RPC call
vote_granted = self.send_request_vote_rpc(peer, self.current_term)
if vote_granted:
votes_received += 1
if votes_received >= quorum and self.state == NodeState.CANDIDATE:
print(f"Node {self.node_id}: Quorum achieved. I am the Leader for Term {self.current_term}.")
self.state = NodeState.LEADER
self.start_sending_heartbeats()
def receive_append_entries_rpc(self, leader_term: int, leader_id: int, entries: List):
"""Handles incoming heartbeats and log replications from a Leader."""
if leader_term >= self.current_term:
# A valid leader exists. Step down if we were a candidate or a stale leader.
if self.state != NodeState.FOLLOWER:
print(f"Node {self.node_id}: Found higher term {leader_term}. Stepping down.")
self.state = NodeState.FOLLOWER
self.current_term = leader_term
self.last_heartbeat = time.time()
# (Log replication logic goes here)
return True
return False
def send_request_vote_rpc(self, peer_id: int, term: int) -> bool:
# Mock network call to a peer
pass
def start_sending_heartbeats(self):
# Spawns a background thread to continuously assert leadership
pass
5. Fault Tolerance & Edge Cases
- Log Inconsistency: When a leader crashes, it might leave followers with partial, uncommitted logs. When the new leader takes over, it does not attempt to merge these logs. Raft enforces the rule that the Leader’s log is absolute truth. The new leader will aggressively overwrite conflicting entries in the followers’ logs to force them into exact alignment.
- Byzantine Failures: What if Node 2 is hacked and intentionally alters its log data before responding to the leader? Raft assumes a Crash-Fault-Tolerant (CFT) model, meaning nodes might die or delay, but they do not lie. To protect against malicious actors or corrupted memory returning forged data, the architecture must abandon Raft and implement much slower Practical Byzantine Fault Tolerance (PBFT), which requires rigorous cryptographic signing of every single state transition.