Workspace IndexAlgorithms › Day 54

Raft in Depth — Membership Changes, Log Compaction, and Read Guarantees TODO

Algorithms · Day 54 / 100 · D. Distributed Systems & Consensus (Day 52-68)

Concept

Raft's basic skeleton is leader election plus log replication, but the hard part in real operation is the three things outside that skeleton. Membership changes have to prevent the old and new configurations from forming two different majorities at once, which would produce two leaders — this is handled either via joint consensus, which requires a majority in both configurations simultaneously, or by restricting changes to adding or removing one node at a time. Log compaction stops the log from growing forever by storing the state machine as a snapshot and discarding the log before it; a lagging follower needs a separate RPC that sends the whole snapshot instead of the log. The read-guarantee problem comes from the leader possibly returning stale state without realizing it's no longer the leader. The standard fix for linearizable reads is ReadIndex: record the commit index, confirm leadership via a majority of heartbeat responses, then read. Lease reads replace that confirmation with a clock assumption, which is faster but loses safety if the clocks drift.

Implement textbook Raft and go to production, and split-brain during node replacement, an ever-growing log, and stale reads right after a leader change show up one after another — these three account for most real incidents.

Code & Formula

# Raft 심화 — 전체 프로토콜 대신 리더 선출과 term 증가만 축약해 시뮬레이션한다.
# 팔로워가 리더의 하트비트를 못 받으면 election timeout 후 term을 올리고 후보가 된다.

import random

random.seed(4)

class Node:
    def __init__(self, node_id):
        self.node_id = node_id
        self.term = 0
        self.state = "follower"  # follower | candidate | leader
        self.voted_for = None

class Cluster:
    def __init__(self, n):
        self.nodes = [Node(i) for i in range(n)]
        self.leader = None

    def start_election(self, candidate):
        candidate.term += 1
        candidate.state = "candidate"
        candidate.voted_for = candidate.node_id
        votes = 1  # 자기 자신에게 투표
        for node in self.nodes:
            if node is candidate:
                continue
            # 후보의 term이 더 높고, 이번 term에 아직 투표 안 했으면 승인
            if candidate.term > node.term or node.voted_for is None:
                node.term = candidate.term
                node.voted_for = candidate.node_id
                votes += 1
        majority = len(self.nodes) // 2 + 1
        if votes >= majority:
            candidate.state = "leader"
            self.leader = candidate
            for node in self.nodes:
                if node is not candidate:
                    node.state = "follower"
            return True, votes
        candidate.state = "follower"
        return False, votes

cluster = Cluster(5)
print(f"초기 상태: 5개 노드, 모두 term=0, follower")

# 리더 부재로 election timeout 발생 -> node 2가 후보로 나섬
won, votes = cluster.start_election(cluster.nodes[2])
majority = len(cluster.nodes) // 2 + 1
print(f"node 2 선거 시작 (term=1): {votes}/{len(cluster.nodes)}표 획득 "
      f"(과반 {majority}) -> {'당선' if won else '낙선'}")
print(f"  leader = node {cluster.leader.node_id}, term = {cluster.leader.term}")

# 리더 파티션 -> 남은 노드 중 node 0이 새 term으로 재선거
cluster.leader = None
won2, votes2 = cluster.start_election(cluster.nodes[0])
print(f"\nnode 0 재선거 (term={cluster.nodes[0].term}): {votes2}/{len(cluster.nodes)}표 -> "
      f"{'당선' if won2 else '낙선'}")
print(f"  leader = node {cluster.leader.node_id}, term = {cluster.leader.term}")
print("-> term은 단조 증가하며, 같은 term에 한 노드만 투표를 받을 수 있어 리더가 유일하게 결정된다.")

Exercise

Trace the messages exchanged during node add/remove in an existing Raft implementation (etcd or hashicorp/raft), and reproduce experimentally how lease read and ReadIndex results differ during a leader partition.

Practical Connection

Blockchain consensus is a Byzantine model, unlike Raft, but the same structural fact holds — "if a leader answers reads without confirming a majority, the answer is stale" — and it applies directly to designing leadership for an indexer that caches the latest block or a multi-instance matching engine.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

Raft 심화 TODO

Algorithms · Day 54 / 100 · D. 분산시스템·합의 (Day 52–68)

멤버십 변경·로그 압축·읽기 보장

개념

Raft의 기본 골격은 리더 선출과 로그 복제지만, 실운영에서 어려운 부분은 그 바깥의 세 가지다. 멤버십 변경은 옛 구성과 새 구성이 동시에 서로 다른 과반을 만들어 두 리더가 생기는 상황을 막아야 하므로, 두 구성의 과반을 동시에 요구하는 joint consensus를 거치거나 한 번에 한 노드만 더하고 빼는 방식으로 제한한다. 로그 압축은 로그가 무한히 자라는 것을 막기 위해 상태 머신을 스냅샷으로 저장하고 그 이전 로그를 버리는 것이며, 뒤처진 팔로워에게는 로그 대신 스냅샷을 통째로 전송하는 별도 RPC가 필요하다. 읽기 보장은 리더가 자신이 아직 리더인지 모른 채 옛 상태를 반환할 수 있다는 문제에서 온다. 선형화 가능한 읽기는 커밋 인덱스를 기록한 뒤 하트비트 과반 응답으로 리더십을 확인하고 읽는 ReadIndex 방식이 표준이고, lease read는 이 확인을 시계 가정으로 대체해 더 빠르지만 시계가 어긋나면 안전성을 잃는다.

논문 수준의 Raft만 구현하고 운영에 들어가면 노드 교체 중 스플릿 브레인, 무한히 커지는 로그, 리더 교체 직후의 stale read가 차례로 터진다. 이 세 가지가 실제 장애의 대부분이다.

코드 · 수식

# Raft 심화 — 전체 프로토콜 대신 리더 선출과 term 증가만 축약해 시뮬레이션한다.
# 팔로워가 리더의 하트비트를 못 받으면 election timeout 후 term을 올리고 후보가 된다.

import random

random.seed(4)

class Node:
    def __init__(self, node_id):
        self.node_id = node_id
        self.term = 0
        self.state = "follower"  # follower | candidate | leader
        self.voted_for = None

class Cluster:
    def __init__(self, n):
        self.nodes = [Node(i) for i in range(n)]
        self.leader = None

    def start_election(self, candidate):
        candidate.term += 1
        candidate.state = "candidate"
        candidate.voted_for = candidate.node_id
        votes = 1  # 자기 자신에게 투표
        for node in self.nodes:
            if node is candidate:
                continue
            # 후보의 term이 더 높고, 이번 term에 아직 투표 안 했으면 승인
            if candidate.term > node.term or node.voted_for is None:
                node.term = candidate.term
                node.voted_for = candidate.node_id
                votes += 1
        majority = len(self.nodes) // 2 + 1
        if votes >= majority:
            candidate.state = "leader"
            self.leader = candidate
            for node in self.nodes:
                if node is not candidate:
                    node.state = "follower"
            return True, votes
        candidate.state = "follower"
        return False, votes

cluster = Cluster(5)
print(f"초기 상태: 5개 노드, 모두 term=0, follower")

# 리더 부재로 election timeout 발생 -> node 2가 후보로 나섬
won, votes = cluster.start_election(cluster.nodes[2])
majority = len(cluster.nodes) // 2 + 1
print(f"node 2 선거 시작 (term=1): {votes}/{len(cluster.nodes)}표 획득 "
      f"(과반 {majority}) -> {'당선' if won else '낙선'}")
print(f"  leader = node {cluster.leader.node_id}, term = {cluster.leader.term}")

# 리더 파티션 -> 남은 노드 중 node 0이 새 term으로 재선거
cluster.leader = None
won2, votes2 = cluster.start_election(cluster.nodes[0])
print(f"\nnode 0 재선거 (term={cluster.nodes[0].term}): {votes2}/{len(cluster.nodes)}표 -> "
      f"{'당선' if won2 else '낙선'}")
print(f"  leader = node {cluster.leader.node_id}, term = {cluster.leader.term}")
print("-> term은 단조 증가하며, 같은 term에 한 노드만 투표를 받을 수 있어 리더가 유일하게 결정된다.")

연습

기존 Raft 구현체(etcd나 hashicorp/raft)에서 노드 추가·제거 시 주고받는 메시지를 로그로 추적하고, 리더 파티션 상황에서 lease read와 ReadIndex의 결과 차이를 실험으로 재현하라.

실무 · Verex 연결

블록체인 합의는 비잔틴 모델이라 Raft와 다르지만 "과반 확인 없이 리더가 읽어 응답하면 stale하다"는 구조는 동일해, 최신 블록 조회를 캐시하는 인덱서나 다중 인스턴스 매칭 엔진의 리더십 설계에 그대로 적용된다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 53. 논리 시계·벡터 시계·하이브리드 논리 시계(HLC)55. Multi-Paxos·Flexible Paxos →