一、Raft 协议核心:三大子问题

1.1 角色与状态机

Raft 集群中的每个节点在任意时刻处于三种角色之一:

状态转换图:

                    ┌─────────────┐
                    │   Follower  │ ◀── 启动默认状态
                    │             │
                    │ 被动接收    │
                    │ Leader 指令 │
                    │ 通过心跳    │
                    │ 检测存活    │
                    └──────┬──────┘
                           │
                           │ 选举超时
                           │ (随机化 150-300ms)
                           │ 未收到心跳
                           ▼
                    ┌─────────────┐
                    │  Candidate  │
                    │             │
                    │ 递增 term   │
                    │ 发起选举     │
                    │ 并行发送     │
                    │ RequestVote │
                    └──────┬──────┘
                           │
                    ┌──────┴──────┐
                    │             │
           获得多数票 │             │ 未获得多数票
                    ▼             ▼
              ┌──────────┐  ┌─────────────┐
              │  Leader  │  │  Follower   │
              │          │  │ (回到跟随者) │
              │ 处理客户端│  └─────────────┘
              │ 请求     │
              │ 日志复制 │
              │ 发送心跳 │
              └──────────┘

1.2 Leader 选举

// Leader 选举核心逻辑(Go 实现)

type Node struct {
    currentTerm int64
    votedFor    int32       // -1 表示未投票
    state       State       // Follower / Candidate / Leader
    log         []LogEntry
    
    // 选举相关
    electionTimeout  time.Duration  // 随机化 150-300ms
    heartbeatInterval time.Duration  // 通常 50ms
    lastHeartbeat    time.Time
    
    // 集群配置
    peers  []string
    selfID int32
}

// RequestVote RPC 请求/响应
type RequestVoteRequest struct {
    Term         int64  // Candidate 的当前 term
    CandidateID  int32  // Candidate 的 ID
    LastLogIndex int64  // Candidate 最后一条日志的索引
    LastLogTerm  int64  // Candidate 最后一条日志的 term
}

type RequestVoteResponse struct {
    Term        int64  // 响应者的当前 term
    VoteGranted bool   // 是否投票
}

func (n *Node) startElection() {
    n.state = Candidate
    n.currentTerm++
    n.votedFor = n.selfID  // 为自己投票
    n.lastHeartbeat = time.Now()
    
    // 重置选举超时(随机化,防止活锁)
    n.electionTimeout = randomElectionTimeout()
    
    votes := 1  // 自己的一票
    needed := len(n.peers)/2 + 1  // 多数票
    
    // 并行向所有其他节点发送 RequestVote
    for _, peer := range n.peers {
        if peer == n.selfID {
            continue
        }
        go func(peerID int32) {
            req := &RequestVoteRequest{
                Term:         n.currentTerm,
                CandidateID:  n.selfID,
                LastLogIndex: n.lastLogIndex(),
                LastLogTerm:  n.lastLogTerm(),
            }
            resp, err := n.sendRequestVote(peerID, req)
            if err != nil {
                return
            }
            
            // 如果响应者的 term 更高,回退为 Follower
            if resp.Term > n.currentTerm {
                n.becomeFollower(resp.Term)
                return
            }
            
            if resp.VoteGranted {
                votes++
                if votes >= needed && n.state == Candidate {
                    n.becomeLeader()
                }
            }
        }(peer)
    }
}

func (n *Node) handleRequestVote(req *RequestVoteRequest) *RequestVoteResponse {
    resp := &RequestVoteResponse{Term: n.currentTerm}
    
    // 规则 1: 如果请求的 term < 当前 term,拒绝
    if req.Term < n.currentTerm {
        resp.VoteGranted = false
        return resp
    }
    
    // 如果请求的 term > 当前 term,更新 term 并回退
    if req.Term > n.currentTerm {
        n.currentTerm = req.Term
        n.votedFor = -1
        n.state = Follower
    }
    
    // 规则 2: 如果已经投过票(给别的候选人),拒绝
    if n.votedFor != -1 && n.votedFor != req.CandidateID {
        resp.VoteGranted = false
        return resp
    }
    
    // 规则 3: 候选人的日志必须至少和自己的日志一样新
    if !n.isLogUpToDate(req.LastLogIndex, req.LastLogTerm) {
        resp.VoteGranted = false
        return resp
    }
    
    // 投票
    n.votedFor = req.CandidateID
    n.lastHeartbeat = time.Now()  // 重置选举超时
    resp.VoteGranted = true
    return resp
}

// 检查候选人的日志是否至少和自己一样新
func (n *Node) isLogUpToDate(candidateLastIndex int64, candidateLastTerm int64) bool {
    myLastTerm := n.lastLogTerm()
    myLastIndex := n.lastLogIndex()
    
    if candidateLastTerm != myLastTerm {
        return candidateLastTerm > myLastTerm
    }
    return candidateLastIndex >= myLastIndex
}

func randomElectionTimeout() time.Duration {
    // 随机化 150-300ms,防止多个节点同时发起选举(活锁)
    min := 150 * time.Millisecond
    max := 300 * time.Millisecond
    return min + time.Duration(rand.Int63n(int64(max-min)))
}

1.3 日志复制

// 日志复制核心逻辑

type LogEntry struct {
    Term    int64       // 创建该条目时的 Leader term
    Index   int64       // 日志索引
    Command interface{} // 状态机命令
}

type AppendEntriesRequest struct {
    Term         int64        // Leader 的当前 term
    LeaderID     int32        // Leader 的 ID
    PrevLogIndex int64        // 紧接新条目之前的日志索引
    PrevLogTerm  int64        // PrevLogIndex 条目的 term
    Entries      []LogEntry   // 待复制的日志条目(心跳时为空)
    LeaderCommit int64        // Leader 的 commitIndex
}

type AppendEntriesResponse struct {
    Term    int64  // 响应者的当前 term
    Success bool   // 如果 Follower 包含匹配 PrevLogIndex 和 PrevLogTerm 的条目则为 true
    // 优化字段(etcd 的 Fast Backup)
    ConflictIndex int64  // 冲突的第一条日志索引
    ConflictTerm  int64  // 冲突的 term
}

func (n *Node) handleAppendEntries(req *AppendEntriesRequest) *AppendEntriesResponse {
    resp := &AppendEntriesResponse{Term: n.currentTerm}
    
    // 规则 1: 如果请求的 term < 当前 term,拒绝
    if req.Term < n.currentTerm {
        resp.Success = false
        return resp
    }
    
    // 如果请求的 term > 当前 term,更新
    if req.Term > n.currentTerm {
        n.currentTerm = req.Term
        n.votedFor = -1
    }
    
    n.state = Follower
    n.lastHeartbeat = time.Now()  // 重置选举超时
    
    // 规则 2: 检查日志匹配
    // Follower 必须包含与 PrevLogIndex 和 PrevLogTerm 匹配的条目
    if req.PrevLogIndex > 0 {
        prevEntry, exists := n.getLogEntry(req.PrevLogIndex)
        if !exists {
            resp.Success = false
            resp.ConflictIndex = n.lastLogIndex()
            return resp
        }
        if prevEntry.Term != req.PrevLogTerm {
            // 日志冲突,找到冲突 term 的第一条日志
            resp.Success = false
            resp.ConflictTerm = prevEntry.Term
            resp.ConflictIndex = n.firstIndexForTerm(prevEntry.Term)
            return resp
        }
    }
    
    // 规则 3: 删除冲突的条目并追加新条目
    for i, entry := range req.Entries {
        existingEntry, exists := n.getLogEntry(req.PrevLogIndex + 1 + int64(i))
        if exists {
            if existingEntry.Term != entry.Term {
                // 冲突:删除此条目及之后所有条目
                n.truncateLog(req.PrevLogIndex + int64(i))
                n.appendLogEntry(entry)
            }
            // 否则跳过(已存在)
        } else {
            n.appendLogEntry(entry)
        }
    }
    
    // 规则 4: 更新 commitIndex
    if req.LeaderCommit > n.commitIndex {
        newCommit := min(req.LeaderCommit, n.lastLogIndex())
        if newCommit > n.commitIndex {
            n.commitIndex = newCommit
            n.applyCommittedEntries()  // 应用到状态机
        }
    }
    
    resp.Success = true
    return resp
}

func (n *Node) replicateLog() {
    // Leader 并行向所有 Follower 发送 AppendEntries
    for _, peer := range n.peers {
        if peer == n.selfID {
            continue
        }
        go func(peerID int32) {
            nextIndex := n.nextIndex[peerID]
            prevLogIndex := nextIndex - 1
            prevLogTerm := int64(0)
            if prevLogIndex > 0 {
                entry, _ := n.getLogEntry(prevLogIndex)
                prevLogTerm = entry.Term
            }
            
            entries := n.logSlice(nextIndex, n.lastLogIndex())
            
            req := &AppendEntriesRequest{
                Term:         n.currentTerm,
                LeaderID:     n.selfID,
                PrevLogIndex: prevLogIndex,
                PrevLogTerm:  prevLogTerm,
                Entries:      entries,
                LeaderCommit: n.commitIndex,
            }
            
            resp, err := n.sendAppendEntries(peerID, req)
            if err != nil {
                return
            }
            
            if resp.Term > n.currentTerm {
                n.becomeFollower(resp.Term)
                return
            }
            
            if resp.Success {
                // 更新 nextIndex 和 matchIndex
                n.nextIndex[peerID] = req.PrevLogIndex + int64(len(entries)) + 1
                n.matchIndex[peerID] = n.nextIndex[peerID] - 1
                n.updateCommitIndex()
            } else {
                // Fast Backup: 使用冲突信息快速回退
                if resp.ConflictIndex > 0 {
                    n.nextIndex[peerID] = resp.ConflictIndex
                } else {
                    n.nextIndex[peerID] = max(1, n.nextIndex[peerID]-1)
                }
            }
        }(peer)
    }
}

1.4 五个安全性保证

Raft 的正确性依赖五个核心性质,已通过 TLA+ 形式化验证:

性质 说明 保证机制
Election Safety 每个 term 最多一个 Leader 多数派投票:一个节点一个 term 只能投一票
Leader Append-Only Leader 日志只追加不修改 Leader 永远不覆盖或删除自己的日志
Log Matching 相同 index+term 的条目内容一致且前序一致 AppendEntries 的 PrevLogIndex/PrevLogTerm 校验
Leader Completeness 已 commit 的条目在后续所有 term 的 Leader 中都存在 选举时日志新旧比较 + 多数派交集
State Machine Safety 同一 index 已 apply 的命令相同 Leader Completeness + Log Matching

二、Raft vs Paxos 对比

维度 Paxos Raft
理解难度 极高(论文晦涩,实现复杂) 中等(分解为三个子问题)
Leader 角色 可选(Multi-Paxos 引入) 强 Leader(所有请求经 Leader)
日志结构 不保证有序(需额外机制) 连续有序(简化日志一致性)
成员变更 复杂(需 α 方法或联合共识) 两种方法:联合共识 + 单节点变更
日志压缩 需自行设计 内置快照机制
工程实现 难(Google Chubby、Spanner 等) 相对简单(etcd、Consul、TiKV 等)
线性一致性读 需额外设计 ReadIndex / Lease Read
形式化验证 TLA+(Lamport 本人) TLA+ / PVS(UW 团队)

三、生产级实现深度对比:etcd vs Consul

3.1 架构对比

etcd 架构:
┌────────────────────────────────────────────────────┐
│                    etcd Server                     │
├────────────────────────────────────────────────────┤
│                                                    │
│  ┌──────────┐  ┌──────────┐  ┌───────────────┐   │
│  │  Raft    │  │  KV      │  │  gRPC API     │   │
│  │  Layer   │──│  Store   │  │  (Watch/Lease │   │
│  │          │  │  (BBolt) │  │   /Elect)     │   │
│  └────┬─────┘  └──────────┘  └───────────────┘   │
│       │                                            │
│  ┌────┴─────┐  ┌──────────────┐                  │
│  │  WAL     │  │  Snapshot    │                  │
│  │  (预写   │  │  (定期快照   │                  │
│  │   日志)  │  │   压缩日志)  │                  │
│  └──────────┘  └──────────────┘                  │
│                                                    │
│  典型用途: Kubernetes 配置存储、分布式锁            │
│                                                    │
└────────────────────────────────────────────────────┘

Consul 架构:
┌────────────────────────────────────────────────────┐
│                   Consul Server                    │
├────────────────────────────────────────────────────┤
│                                                    │
│  ┌──────────┐  ┌──────────┐  ┌───────────────┐   │
│  │  Raft    │  │  KV      │  │  HTTP/gRPC    │   │
│  │  Layer   │──│  Store   │  │  API          │   │
│  │          │  │  (BBolt) │  │               │   │
│  └────┬─────┘  └────┬─────┘  └───────────────┘   │
│       │              │                             │
│  ┌────┴─────┐  ┌────┴─────────────┐              │
│  │  WAL     │  │  Service         │              │
│  │  + Snap  │  │  Discovery       │              │
│  │          │  │  + Health Check  │              │
│  └──────────┘  │  + WAN Gossip    │              │
│                │  + Connect (mTLS)│              │
│                └──────────────────┘              │
│                                                    │
│  典型用途: 服务网格、服务注册发现、健康检查         │
│                                                    │
└────────────────────────────────────────────────────┘

3.2 关键工程优化对比

优化项 etcd Consul 说明
读一致性 ReadIndex + Lease Leader Read Forward etcd 支持 Lease Read 避免每次 RPC
批量写入 Batch Commit Batch Commit 两者均支持批量提交提升吞吐
日志压缩 快照 + WAL 截断 快照 + WAL 截断 机制类似
成员变更 v2 联合共识 / v3 单节点 单节点变更 etcd v3 简化为单节点变更
WAL 优化 fdatasync 批量化 fdatasync 批量化 减少 fsync 开销
网络 gRPC (HTTP/2) Serf (Gossip) + gRPC Consul 多了 WAN Gossip 层
跨数据中心 需上层方案(如 Mirror Maker) 原生 WAN Gossip 支持 Consul 跨 DC 更友好

四、关键工程优化技术

4.1 快速回退(Fast Backup)

原始 Raft 论文中,当 Leader 和 Follower 日志不一致时,Leader 每次 nextIndex 减 1 逐条重试,效率低下。etcd 引入了 Fast Backup 优化:

// Fast Backup: 使用冲突信息快速定位不一致点

func (n *Node) handleAppendEntriesFastBackup(
    req *AppendEntriesRequest,
) *AppendEntriesResponse {
    
    // 如果 PrevLogIndex 超出 Follower 日志长度
    if req.PrevLogIndex > n.lastLogIndex() {
        return &AppendEntriesResponse{
            Success:       false,
            ConflictIndex: n.lastLogIndex() + 1,  // 告诉 Leader 从哪里开始
            ConflictTerm:  -1,                     // 没有 conflict term
        }
    }
    
    entry, exists := n.getLogEntry(req.PrevLogIndex)
    if !exists {
        return &AppendEntriesResponse{
            Success:       false,
            ConflictIndex: n.lastLogIndex() + 1,
            ConflictTerm:  -1,
        }
    }
    
    if entry.Term != req.PrevLogTerm {
        // 找到冲突 term 的第一条日志
        conflictTerm := entry.Term
        conflictIndex := req.PrevLogIndex
        
        // 向前查找直到找到不同 term 的条目
        for conflictIndex > 1 {
            prev, _ := n.getLogEntry(conflictIndex - 1)
            if prev.Term != conflictTerm {
                break
            }
            conflictIndex--
        }
        
        return &AppendEntriesResponse{
            Success:       false,
            ConflictIndex: conflictIndex,
            ConflictTerm:  conflictTerm,
        }
    }
    
    return &AppendEntriesResponse{Success: true}
}

// Leader 端处理 Fast Backup 响应
func (n *Node) handleFastBackupResponse(peerID int32, resp *AppendEntriesResponse) {
    if resp.ConflictTerm > 0 {
        // 在 Leader 日志中查找 ConflictTerm 的最后一条条目
        lastIndexForTerm := n.findLastIndexForTerm(resp.ConflictTerm)
        if lastIndexForTerm > 0 {
            n.nextIndex[peerID] = lastIndexForTerm + 1
        } else {
            n.nextIndex[peerID] = resp.ConflictIndex
        }
    } else {
        n.nextIndex[peerID] = resp.ConflictIndex
    }
}

4.2 日志压缩与快照

日志压缩与快照机制:

压缩前:
┌─────────────────────────────────────────────────────┐
│ Log: [1][2][3][4][5][6][7][8][9][10][11][12][13]   │
│       │  │  │  │  │                                 │
│       │  │  │  │  └── 已应用到状态机的最新条目       │
│       │  │  │  └── 重复操作(可压缩)                │
│       │  │  └── 已被后续操作覆盖(可压缩)           │
│       │  └── 初始化操作(可压缩到快照)              │
│       └── 集群配置变更(可压缩)                     │
└─────────────────────────────────────────────────────┘

压缩后:
┌──────────────┬──────────────────────────────────────┐
│   Snapshot   │  Log: [11][12][13]                  │
│              │                                     │
│ compactIndex │  (仅保留快照之后的条目)              │
│ = 10         │                                     │
│ compactTerm  │                                     │
│ = 5          │                                     │
│              │                                     │
│ 状态机快照   │                                     │
│ (包含 1-10   │                                     │
│  的所有状态  │                                     │
│  变更结果)   │                                     │
└──────────────┴──────────────────────────────────────┘
// 快照管理实现

type Snapshot struct {
    LastIndex int64       // 快照包含的最后一条日志索引
    LastTerm  int64       // 该条目的 term
    Data      []byte      // 状态机快照数据
    Metadata  SnapshotMeta // 集群配置等元数据
}

type SnapshotStore interface {
    Create(index int64, term int64, data []byte) error
    List() ([]*Snapshot, error)
    Open(index int64) (*Snapshot, error)
}

// 触发快照的条件
type SnapshotConfig struct {
    Threshold  int64  // 日志条目数超过此值触发快照
    Interval   time.Duration  // 定期触发间隔
    Compact    int64  // 快照后保留的日志条目数
}

func (n *Node) maybeSnapshot() {
    if n.lastApplied - n.snapshotIndex < n.snapshotConfig.Threshold {
        return  // 未达阈值,跳过
    }
    
    // 1. 创建状态机快照
    snapshotData := n.stateMachine.Snapshot()
    
    // 2. 保存快照
    snapshot := &Snapshot{
        LastIndex: n.lastApplied,
        LastTerm:  n.getLogEntry(n.lastApplied).Term,
        Data:      snapshotData,
    }
    n.snapshotStore.Create(snapshot.LastIndex, snapshot.LastTerm, snapshotData)
    
    // 3. 截断已快照的日志
    compactIndex := snapshot.LastIndex - n.snapshotConfig.Compact
    n.log = n.log[compactIndex:]
    n.snapshotIndex = snapshot.LastIndex
    
    // 4. 通知 Follower 发送快照(如果需要)
    for _, peer := range n.peers {
        if n.matchIndex[peer] < snapshot.LastIndex {
            go n.sendSnapshot(peer, snapshot)
        }
    }
}

// InstallSnapshot RPC: Leader 向落后的 Follower 发送快照
func (n *Node) sendSnapshot(peerID int32, snapshot *Snapshot) {
    req := &InstallSnapshotRequest{
        Term:              n.currentTerm,
        LeaderID:          n.selfID,
        LastIndex:         snapshot.LastIndex,
        LastTerm:          snapshot.LastTerm,
        Data:              snapshot.Data,
    }
    
    resp, err := n.sendInstallSnapshot(peerID, req)
    if err != nil {
        return
    }
    
    if resp.Term > n.currentTerm {
        n.becomeFollower(resp.Term)
        return
    }
    
    // 更新 nextIndex 和 matchIndex
    n.nextIndex[peerID] = snapshot.LastIndex + 1
    n.matchIndex[peerID] = snapshot.LastIndex
}

4.3 线性一致性读

Raft 的线性一致性读有三种实现方式,各有取舍:

线性一致性读的三种实现:

方式 1: ReadIndex (最准确)
┌──────────────────────────────────────────────────┐
│ Client → Leader: Read(key)                       │
│                                                  │
│ Leader: 1. 记录当前 commitIndex 作为 readIndex   │
│         2. 向多数节点发送心跳确认自己仍是 Leader  │
│         3. 等待状态机执行到 readIndex            │
│         4. 从状态机读取并返回                    │
│                                                  │
│ 延迟: 1 RTT (心跳确认) + 等待 apply              │
│ 一致性: 强线性一致                                │
└──────────────────────────────────────────────────┘

方式 2: Lease Read (最优性能)
┌──────────────────────────────────────────────────┐
│ Client → Leader: Read(key)                       │
│                                                  │
│ Leader: 1. 基于租约确认自己仍是 Leader           │
│         (租约期内无需心跳确认)                    │
│         2. 等待状态机执行到 lastApplied          │
│         3. 从状态机读取并返回                    │
│                                                  │
│ 延迟: 0 RTT (租约期内) + 等待 apply              │
│ 一致性: 依赖时钟准确性(有争议)                  │
└──────────────────────────────────────────────────┘

方式 3: Read Through Raft Log (最简单)
┌──────────────────────────────────────────────────┐
│ Client → Leader: Read(key)                       │
│                                                  │
│ Leader: 1. 将读操作作为日志条目提交              │
│         2. 等待日志 commit + apply               │
│         3. 从状态机读取并返回                    │
│                                                  │
│ 延迟: 1 RTT (日志复制) + 等待 commit + apply     │
│ 一致性: 强线性一致(但性能最差)                  │
└──────────────────────────────────────────────────┘
// ReadIndex 实现(etcd 风格)

func (n *Node) readIndex(key string) (string, error) {
    if n.state != Leader {
        return "", ErrNotLeader
    }
    
    // 1. 记录当前 commitIndex
    readIndex := n.commitIndex
    
    // 2. 向多数节点发送心跳确认自己仍是 Leader
    confirmations := 1  // 自己
    needed := len(n.peers)/2 + 1
    
    for _, peer := range n.peers {
        if peer == n.selfID {
            continue
        }
        go func(peerID int32) {
            resp, err := n.sendHeartbeat(peerID)
            if err == nil && resp.Term == n.currentTerm {
                confirmations++
            }
        }(peer)
    }
    
    // 等待多数确认(带超时)
    if !n.waitForConfirmations(needed, 100*time.Millisecond) {
        return "", ErrLeaderConfirmation
    }
    
    // 3. 等待状态机执行到 readIndex
    for n.lastApplied < readIndex {
        time.Sleep(100 * time.Microsecond)
        // 可配 select + context 超时
    }
    
    // 4. 从状态机读取
    return n.stateMachine.Get(key), nil
}

// Lease Read 实现(性能最优)

func (n *Node) leaseRead(key string) (string, error) {
    if n.state != Leader {
        return "", ErrNotLeader
    }
    
    // 检查租约是否有效
    if time.Since(n.lastLeaseRenewal) > n.leaseDuration {
        // 租约过期,降级为 ReadIndex
        return n.readIndex(key)
    }
    
    // 租约有效,直接读取(无需心跳确认)
    // 等待状态机执行到 lastApplied
    for n.lastApplied < n.commitIndex {
        time.Sleep(100 * time.Microsecond)
    }
    
    return n.stateMachine.Get(key), nil
}

4.4 WAL 预写日志优化

// WAL (Write-Ahead Log) 实现

type WAL struct {
    dir       string
    file      *os.File
    encoder   *WALEncoder
    pipeline  chan *walEntry  // 管道化写入
    batch     []*walEntry     // 批量缓冲
    batchLock sync.Mutex
    flushTimer *time.Timer
}

type walEntry struct {
    Type     EntryType  // Entry / State / CRC
    Term     int64
    Index    int64
    Data     []byte
}

// 批量 + 管道化写入优化
func (w *WAL) Save(entries []LogEntry) error {
    w.batchLock.Lock()
    defer w.batchLock.Unlock()
    
    for _, entry := range entries {
        data, _ := encode(entry)
        w.batch = append(w.batch, &walEntry{
            Type:  EntryType,
            Term:  entry.Term,
            Index: entry.Index,
            Data:  data,
        })
    }
    
    // 批量达到阈值或超时后刷盘
    if len(w.batch) >= w.batchSize || w.flushTimer == nil {
        return w.flush()
    }
    
    return nil
}

func (w *WAL) flush() error {
    if len(w.batch) == 0 {
        return nil
    }
    
    // 1. 编码批量写入文件
    for _, entry := range w.batch {
        if err := w.encoder.Encode(entry); err != nil {
            return err
        }
    }
    
    // 2. fdatasync 刷盘(不是 fsync,省略元数据同步)
    if err := w.file.Sync(); err != nil {
        return err
    }
    
    // 3. 清空批量缓冲
    w.batch = w.batch[:0]
    
    return nil
}

// 预分配文件空间(减少写入时的文件扩展开销)
func (w *WAL) preallocate(size int64) error {
    return w.file.Truncate(size)
}

// Segment 日志切分(防止单文件过大)
func (w *WAL) maybeRotate() error {
    stat, _ := w.file.Stat()
    if stat.Size() < w.segmentSize {
        return nil
    }
    
    // 关闭当前段
    w.file.Sync()
    w.file.Close()
    
    // 创建新段
    segName := fmt.Sprintf("%016d-%016d.wal", w.lastIndex, w.currentTerm)
    w.file, _ = os.Create(filepath.Join(w.dir, segName))
    w.encoder = NewWALEncoder(w.file)
    w.preallocate(w.segmentSize)
    
    return nil
}

五、选举超时调优

5.1 参数选择指南

参数关系约束:
  heartbeatInterval < electionTimeout < MTBF (平均故障间隔)

推荐配置:
┌──────────────────┬───────────────┬───────────────┬──────────────┐
│ 场景             │ heartbeat     │ election      │ 备注         │
│                  │ Interval      │ Timeout       │              │
├──────────────────┼───────────────┼───────────────┼──────────────┤
│ 局域网 (低延迟)  │ 25-50ms       │ 100-300ms     │ etcd 默认    │
│ 跨可用区        │ 50-100ms      │ 300-500ms     │ 适当增大     │
│ 跨地域 (WAN)    │ 100-200ms     │ 500-1500ms    │ Consul WAN   │
│ 高负载环境      │ 50ms          │ 200-400ms     │ 增大超时     │
│ 测试环境        │ 10ms          │ 50-100ms      │ 快速故障检测 │
└──────────────────┴───────────────┴───────────────┴──────────────┘

关键公式:
  electionTimeout > broadcastTime × (bestCaseStable / worstCaseUnstable)
  
  即: 选举超时必须远大于网络往返时间
  通常: electionTimeout >= 10 × heartbeatInterval

5.2 不当配置的后果

// 选举超时过短的后果:频繁切主

// ❌ 错误配置: electionTimeout = 50ms, heartbeatInterval = 40ms
// 后果: 网络抖动 10ms 就触发选举,导致频繁切主
// 症状: Leader 频繁变更,日志复制中断,客户端请求超时

// ❌ 错误配置: 所有节点使用相同的 electionTimeout
// 后果: 多个节点同时超时并发起选举,导致选票分裂,无法选出 Leader
// 症状: 集群长时间无 Leader,拒绝服务

// ✅ 正确配置: 随机化 electionTimeout
func randomElectionTimeout(base time.Duration) time.Duration {
    // base 的 1.0-1.5 倍随机
    jitter := time.Duration(rand.Int63n(int64(base / 2)))
    return base + jitter
}

六、完整 Raft 实现示例

// 最小化 Raft 实现(教学用途,约 200 行核心逻辑)

package raft

import (
    "context"
    "sync"
    "time"
)

type State int

const (
    Follower State = iota
    Candidate
    Leader
)

type Raft struct {
    mu sync.Mutex
    
    // 持久化状态
    currentTerm int64
    votedFor    int32
    log         []LogEntry
    
    // 易失性状态
    commitIndex int64
    lastApplied int64
    
    // Leader 易失性状态
    nextIndex  map[int32]int64
    matchIndex map[int32]int64
    
    // 配置
    id             int32
    peers          []int32
    state          State
    electionTimer  *time.Timer
    heartbeatTimer *time.Timer
    
    // 通道
    applyCh   chan ApplyMsg
    commitCh  chan struct{}
}

type LogEntry struct {
    Term    int64
    Index   int64
    Command interface{}
}

type ApplyMsg struct {
    Index   int64
    Command interface{}
}

func NewRaft(id int32, peers []int32) *Raft {
    r := &Raft{
        id:           id,
        peers:        peers,
        state:        Follower,
        currentTerm:  0,
        votedFor:     -1,
        log:          []LogEntry{{Term: 0, Index: 0}},
        commitIndex:  0,
        lastApplied:  0,
        nextIndex:    make(map[int32]int64),
        matchIndex:   make(map[int32]int64),
        applyCh:      make(chan ApplyMsg, 100),
        commitCh:     make(chan struct{}, 1),
    }
    
    r.resetElectionTimer()
    go r.electionLoop()
    go r.applyLoop()
    
    return r
}

func (r *Raft) resetElectionTimer() {
    if r.electionTimer != nil {
        r.electionTimer.Stop()
    }
    timeout := time.Duration(150+time.Now().UnixNano()%150) * time.Millisecond
    r.electionTimer = time.AfterFunc(timeout, func() {
        r.mu.Lock()
        if r.state != Leader {
            r.startElection()
        }
        r.mu.Unlock()
    })
}

func (r *Raft) startElection() {
    r.state = Candidate
    r.currentTerm++
    r.votedFor = r.id
    
    votes := 1
    needed := len(r.peers)/2 + 1
    
    lastLogIndex := r.log[len(r.log)-1].Index
    lastLogTerm := r.log[len(r.log)-1].Term
    
    for _, peer := range r.peers {
        if peer == r.id {
            continue
        }
        go func(peer int32) {
            req := &RequestVoteRequest{
                Term:         r.currentTerm,
                CandidateID:  r.id,
                LastLogIndex: lastLogIndex,
                LastLogTerm:  lastLogTerm,
            }
            resp, err := r.sendRequestVote(peer, req)
            if err != nil {
                return
            }
            
            r.mu.Lock()
            defer r.mu.Unlock()
            
            if resp.Term > r.currentTerm {
                r.becomeFollower(resp.Term)
                return
            }
            
            if r.state != Candidate || resp.Term != r.currentTerm {
                return
            }
            
            if resp.VoteGranted {
                votes++
                if votes >= needed {
                    r.becomeLeader()
                }
            }
        }(peer)
    }
    
    r.resetElectionTimer()
}

func (r *Raft) becomeLeader() {
    r.state = Leader
    lastIdx := r.log[len(r.log)-1].Index
    for _, peer := range r.peers {
        r.nextIndex[peer] = lastIdx + 1
        r.matchIndex[peer] = 0
    }
    r.startHeartbeat()
}

func (r *Raft) becomeFollower(term int64) {
    r.state = Follower
    r.currentTerm = term
    r.votedFor = -1
    r.resetElectionTimer()
}

func (r *Raft) startHeartbeat() {
    r.heartbeatTimer = time.AfterFunc(50*time.Millisecond, func() {
        r.mu.Lock()
        if r.state != Leader {
            r.mu.Unlock()
            return
        }
        r.broadcastAppendEntries()
        r.heartbeatTimer.Reset(50 * time.Millisecond)
        r.mu.Unlock()
    })
}

func (r *Raft) Submit(command interface{}) (int64, int64, bool) {
    r.mu.Lock()
    defer r.mu.Unlock()
    
    if r.state != Leader {
        return 0, 0, false
    }
    
    index := r.log[len(r.log)-1].Index + 1
    entry := LogEntry{
        Term:    r.currentTerm,
        Index:   index,
        Command: command,
    }
    r.log = append(r.log, entry)
    
    r.broadcastAppendEntries()
    return index, r.currentTerm, true
}

func (r *Raft) applyLoop() {
    for range r.commitCh {
        r.mu.Lock()
        for r.lastApplied < r.commitIndex {
            r.lastApplied++
            entry := r.log[r.lastApplied]
            r.applyCh <- ApplyMsg{
                Index:   entry.Index,
                Command: entry.Command,
            }
        }
        r.mu.Unlock()
    }
}

func (r *Raft) updateCommitIndex() {
    for n := r.log[len(r.log)-1].Index; n > r.commitIndex; n-- {
        if r.log[n].Term != r.currentTerm {
            continue  // 只 commit 当前 term 的条目
        }
        count := 1
        for _, peer := range r.peers {
            if peer == r.id {
                continue
            }
            if r.matchIndex[peer] >= n {
                count++
            }
        }
        if count > len(r.peers)/2 {
            r.commitIndex = n
            select {
            case r.commitCh <- struct{}{}:
            default:
            }
            break
        }
    }
}

七、生产环境最佳实践清单

类别 实践 优先级
部署 奇数节点(3 或 5 或 7),容忍 (N-1)/2 节点故障 P0
部署 跨可用区部署,避免单 AZ 故障导致集群不可用 P0
网络 选举超时 > 10 × 心跳间隔;随机化选举超时 P0
存储 WAL 和快照使用独立磁盘(避免 I/O 竞争) P0
存储 SSD 存储,WAL 使用 fdatasync 而非 fsync P1
监控 监控 Leader 变更次数(频繁切主 = 网络问题) P0
监控 监控 commit 延迟(commitIndex - lastApplied 差值) P0
监控 监控日志增长速率(快照频率不足的信号) P1
调优 批量写入 + 管道化提交提升吞吐 P1
调优 使用 Lease Read 避免每次读的心跳确认 P1
安全 节点间通信加密(mTLS) P0
安全 客户端请求认证 + RBAC P0
运维 成员变更一次只增减一个节点 P0
运维 快照文件定期备份到对象存储 P1
运维 定期验证快照可恢复(灾难恢复演练) P1

免责声明

本文仅供技术学习和研究使用。文中涉及的代码示例为教学性质的最小化实现,未经过完整的安全审计和生产环境验证。在生产环境中使用 Raft 共识算法时,建议使用经过社区验证的成熟实现(如 etcd-raft、hashicorp/raft 等),并遵循官方部署指南。读者应根据自身业务需求进行充分的测试和评估。