🗳️ Raft vs Paxos:分布式共识算法的"选举之战"!

38 阅读9分钟

副标题:让分布式系统达成一致的奥秘 🎯


🎬 开场:为什么需要共识算法?

分布式系统的核心难题

场景:3个节点的分布式系统

问题:如何保证这3个节点的数据一致?

┌──────┐  ┌──────┐  ┌──────┐
│节点A │  │节点B │  │节点C │
│data=1│  │data=1│  │data=1│
└──────┘  └──────┘  └──────┘

客户端发起更新:data = 2

理想情况:
┌──────┐  ┌──────┐  ┌──────┐
│节点A │  │节点B │  │节点C │
│data=2│  │data=2│  │data=2│ ✅ 一致
└──────┘  └──────┘  └──────┘

但现实是:

网络分区:
┌──────┐     ×     ┌──────┐  ┌──────┐
│节点A │  (断网)   │节点B │  │节点C │
│data=2│           │data=1│  │data=1│ ❌ 不一致
└──────┘           └──────┘  └──────┘

节点崩溃:
┌──────┐  ┌──────┐  ┌──────┐
│节点A │  │节点B │  │节点C │
│data=2│  │data=2│  │ 💥  │ ❌ 数据丢失
└──────┘  └──────┘  └──────┘

并发写入:
┌──────┐  ┌──────┐  ┌──────┐
│节点A │  │节点B │  │节点C │
│data=2│  │data=3│  │data=4│ ❌ 冲突
└──────┘  └──────┘  └──────┘

共识算法的作用

共识(Consensus):
让分布式系统中的多个节点对某个值达成一致

保证:
1. 一致性:所有节点的值相同
2. 可用性:系统能够响应请求
3. 分区容错:网络分区时仍能工作

应用场景:
├── 分布式数据库:主从复制
├── 配置中心:配置同步
├── 分布式锁:锁的持有者
├── 分布式队列:消息顺序
└── Leader选举:选出主节点

📚 Paxos算法

历史背景

发明者:Leslie Lamport(图灵奖得主)
发布时间:1998年
论文标题:The Part-Time Parliament
          (兼职议会)

特点:
✅ 理论完备
✅ 安全性证明严谨
❌ 难以理解
❌ 难以实现

核心角色

Paxos的三种角色:

1. Proposer(提议者)
   - 提出提案
   - 发起投票

2. Acceptor(接受者)
   - 接受提案
   - 进行投票

3. Learner(学习者)
   - 学习被选定的值
   - 不参与投票

实际系统中,一个节点通常扮演多个角色

Basic Paxos流程

两阶段提交:

阶段1:Prepare(准备阶段)
┌─────────┐
│Proposer │ 发送 Prepare(n)
└────┬────┘
     │
     ↓
┌────────────────────────────┐
│  Acceptor A, B, C          │
│  收到提案编号 n            │
│  如果 n > 之前收到的最大编号│
│    则承诺:                │
│    - 不再接受编号 < n 的提案│
│    - 返回已接受的最大提案  │
└────────────────────────────┘

阶段2:Accept(接受阶段)
┌─────────┐
│Proposer │ 发送 Accept(n, value)
└────┬────┘
     │
     ↓
┌────────────────────────────┐
│  Acceptor A, B, C          │
│  如果 n >= 承诺的最小编号   │
│    则接受该提案            │
│    否则拒绝                │
└────────────────────────────┘

达成共识:
多数派(Quorum)接受 → 提案通过

示例流程

场景:3AcceptorA, B, C),需要2个同意

时刻1Proposer1 提案
P1A, B, C: Prepare(1)
A: 承诺 ✅ (maxN=1)
B: 承诺 ✅ (maxN=1)
C: 承诺 ✅ (maxN=1)

P1 收到多数派承诺 → 进入Accept阶段

时刻2Proposer1 提交
P1A, B, C: Accept(1, "value_X")
A: 接受 ✅ (acceptedN=1, acceptedV="value_X")
B: 接受 ✅ (acceptedN=1, acceptedV="value_X")
C: 接受 ✅ (acceptedN=1, acceptedV="value_X")

P1 收到多数派接受 → 提案通过 ✅
共识结果:value_X

时刻3:并发场景
P2A, B, C: Prepare(2)
A: 承诺 ✅ (maxN=2, 返回已接受的(1, "value_X"))
B: 承诺 ✅ (maxN=2, 返回已接受的(1, "value_X"))
C: 延迟/崩溃

P2 发现已有提案被接受,必须使用 value_X
P2A, B: Accept(2, "value_X")
A: 接受 ✅
B: 接受 ✅

最终一致:都是 value_X

Multi-Paxos

问题:Basic Paxos每次只能决定一个值

Multi-Paxos:
1. 选举一个Leader
2. Leader独占提案权
3. 跳过Prepare阶段(Leader已获承诺)
4. 直接Accept

优势:
- 减少消息数量
- 提高性能
- 接近实用

流程:
┌────────┐
│ Leader │ 发送 Accept(n, value)
│ 选举   │
└───┬────┘
    │
    ↓
┌─────────────────┐
│ Acceptor A,B,C  │
│ 直接接受        │
└─────────────────┘

🎯 Raft算法

设计目标

发明者:Diego Ongaro, John Ousterhout
发布时间:2013年
论文标题:In Search of an Understandable 
          Consensus Algorithm
          (寻找可理解的共识算法)

设计目标:
✅ 易于理解(Understandability)
✅ 易于实现
✅ 等价于Multi-Paxos的安全性
✅ 更好的性能

成果:
- 被广泛应用(etcd, Consul, TiKV等)
- 工业界事实标准

核心角色

Raft的三种状态:

1. Leader(领导者)
   - 处理所有客户端请求
   - 复制日志到Follower
   - 唯一的写入点

2. Follower(跟随者)
   - 被动接收Leader的日志
   - 投票选举Leader

3. Candidate(候选人)
   - 发起选举
   - 尝试成为Leader

状态转换:
Follower → (超时) → Candidate → (获得多数票) → Leader
   ↑                                               │
   └───────────────(发现更高term)──────────────────┘

Raft的三个子问题

1️⃣ Leader选举(Leader Election)

选举触发:
- Follower在选举超时内未收到心跳
- 转变为Candidate并发起选举

选举流程:
┌──────────┐
│Candidate │
└────┬─────┘
     │
     ├─ 增加任期号(term)
     ├─ 投票给自己
     └─ 向所有节点请求投票
        │
        ↓
┌───────────────────────┐
│ 其他节点收到请求      │
│ 检查:                │
│ 1. term >= 自己的term │
│ 2. 日志至少和自己一样新│
│ 3. 本term还没投过票   │
│ → 投赞成票            │
└───────────────────────┘
        │
        ↓
┌──────────┐
│Candidate │ 收到多数票
└────┬─────┘
     │
     └─ 成为Leader ✅

时间线示例:
Term 1: ALeader
Term 2: A崩溃,B超时,发起选举,成为Leader
Term 3: BC网络分区,C发起选举,成为Leader

2️⃣ 日志复制(Log Replication)

复制流程:

Step 1: 客户端请求
Client  Leader: 写入 data=X

Step 2: Leader写入日志
Leader: 
  Log: [..., (term=3, index=5, data=X)]
  状态:Uncommitted(未提交)

Step 3: Leader复制到Follower
Leader  Follower A: AppendEntries(...)
Leader  Follower B: AppendEntries(...)
Leader  Follower C: AppendEntries(...)

Step 4: Follower写入日志
Follower A: Log: [..., (term=3, index=5, data=X)] 
Follower B: Log: [..., (term=3, index=5, data=X)] 
Follower C: 网络延迟...

Step 5: 多数派确认
Leader收到 A, B 的确认  多数派达成 

Step 6: Leader提交
Leader: 
  Log: [..., (term=3, index=5, data=X)]
  状态:Committed(已提交)
  commitIndex = 5

Step 7: 通知Follower提交
Leader  Follower A, B: commitIndex = 5
Follower A, B: 提交日志 

Step 8: 响应客户端
Leader  Client: 写入成功 

日志结构

Leader的日志:
Index:  1    2    3    4    5    6
Term:   1    1    2    2    3    3
Data:   a    b    c    d    e    f
        ─────────────────────
        已提交(commitIndex=4)

Follower的日志(正常):
Index:  1    2    3    4    5
Term:   1    1    2    2    3
Data:   a    b    c    d    e
        ─────────────────
        已提交(commitIndex=4)

Follower的日志(落后):
Index:  1    2    3
Term:   1    1    2
Data:   a    b    c
        ─────────
        已提交(commitIndex=3)

Follower的日志(冲突):
Index:  1    2    3    4    5
Term:   1    1    2    2    2  ← term冲突
Data:   a    b    c    d    x
        ─────────────────
Leader会覆盖冲突的日志

3️⃣ 安全性(Safety)

安全性保证:

1. 选举安全性(Election Safety)
   一个term最多一个Leader

2. Leader完整性(Leader Completeness)
   如果一个日志条目在某个term被提交,
   那么这个条目必然出现在更高term的Leader的日志中

3. 状态机安全性(State Machine Safety)
   如果某个节点已应用了某个日志条目到状态机,
   则其他节点不会在相同index应用不同的日志

关键机制:
- 投票时检查日志新旧程度
- 只有拥有最新已提交日志的节点才能当选Leader

🆚 Raft vs Paxos 对比

可理解性对比

Paxos:
❌ 难以理解
   - 角色复杂(Proposer, Acceptor, Learner)
   - 两阶段协议晦涩
   - 缺乏清晰的实现指导

❌ 难以实现
   - 论文偏理论
   - 实现细节需要自己补充
   - 容易出错

Raft:
✅ 易于理解
   - 角色清晰(Leader, Follower, Candidate)
   - 流程直观(选举 → 日志复制)
   - 问题分解(Leader选举、日志复制、安全性)

✅ 易于实现
   - 论文详细
   - 提供实现指导
   - 有参考实现

性能对比

消息复杂度:

Paxos(Basic):
- Prepare阶段:2n
- Accept阶段:2n
- 总计:4n

Multi-Paxos:
- Leader选举后:2n
- 性能接近Raft

Raft:
- 正常情况:2n
- Leader选举:O(n²)(但很少发生)

结论:性能相当 ≈

应用场景对比

维度PaxosRaft
理论完备性⭐⭐⭐⭐⭐⭐⭐⭐⭐
易理解性⭐⭐⭐⭐⭐⭐⭐
易实现性⭐⭐⭐⭐⭐⭐⭐
工业应用⭐⭐⭐⭐⭐⭐⭐⭐
性能⭐⭐⭐⭐⭐⭐⭐⭐

🛠️ 实际应用

Raft应用

1. etcd(Kubernetes配置中心)
   - Leader处理所有写请求
   - Follower转发给Leader
   - 保证配置一致性

2. Consul(服务发现)
   - 服务注册表一致性
   - Leader选举
   - KV存储

3. TiKV(分布式KV)
   - 数据分片
   - 每个分片一个Raft组
   - 保证数据一致性

4. Redis Sentinel(主从切换)
   - Leader选举
   - 故障检测
   - 自动切换

Paxos应用

1. Google Chubby(分布式锁)
   - Paxos保证锁的一致性
   - Multi-Paxos优化

2. Google Spanner(分布式数据库)
   - 数据复制一致性
   - 使用改进的Paxos

3. Zookeeper(类Paxos)
   - ZAB协议(类似Paxos)
   - 配置管理
   - 分布式协调

💻 简化实现

Raft Leader选举(Go语言)

// Raft节点
type RaftNode struct {
    id          int
    currentTerm int
    votedFor    int
    state       NodeState  // Follower, Candidate, Leader
    
    // 选举超时
    electionTimeout time.Duration
    lastHeartbeat   time.Time
    
    // 其他节点
    peers []*RaftNode
    
    // 日志
    log []LogEntry
    
    // 锁
    mu sync.Mutex
}

// 节点状态
type NodeState int
const (
    Follower NodeState = iota
    Candidate
    Leader
)

// 日志条目
type LogEntry struct {
    Term  int
    Index int
    Data  interface{}
}

// 选举超时,转为Candidate
func (n *RaftNode) startElection() {
    n.mu.Lock()
    defer n.mu.Unlock()
    
    // 增加任期
    n.currentTerm++
    n.state = Candidate
    n.votedFor = n.id
    
    fmt.Printf("节点%d发起选举,term=%d\n", n.id, n.currentTerm)
    
    // 投票给自己
    votes := 1
    
    // 请求投票
    for _, peer := range n.peers {
        go func(peer *RaftNode) {
            if peer.requestVote(n.currentTerm, n.id) {
                n.mu.Lock()
                votes++
                
                // 获得多数票
                if votes > len(n.peers)/2 && n.state == Candidate {
                    n.becomeLeader()
                }
                n.mu.Unlock()
            }
        }(peer)
    }
}

// 处理投票请求
func (n *RaftNode) requestVote(term int, candidateId int) bool {
    n.mu.Lock()
    defer n.mu.Unlock()
    
    // term太旧,拒绝
    if term < n.currentTerm {
        return false
    }
    
    // 发现更高的term,更新自己
    if term > n.currentTerm {
        n.currentTerm = term
        n.state = Follower
        n.votedFor = -1
    }
    
    // 如果还没投票,投给candidate
    if n.votedFor == -1 || n.votedFor == candidateId {
        n.votedFor = candidateId
        fmt.Printf("节点%d投票给节点%d\n", n.id, candidateId)
        return true
    }
    
    return false
}

// 成为Leader
func (n *RaftNode) becomeLeader() {
    n.state = Leader
    fmt.Printf("节点%d成为Leader,term=%d\n", n.id, n.currentTerm)
    
    // 开始发送心跳
    go n.sendHeartbeats()
}

// 发送心跳
func (n *RaftNode) sendHeartbeats() {
    ticker := time.NewTicker(100 * time.Millisecond)
    defer ticker.Stop()
    
    for n.state == Leader {
        <-ticker.C
        
        for _, peer := range n.peers {
            go peer.appendEntries(n.currentTerm, n.id, n.log)
        }
    }
}

// 接收心跳/日志
func (n *RaftNode) appendEntries(term int, leaderId int, entries []LogEntry) bool {
    n.mu.Lock()
    defer n.mu.Unlock()
    
    // 更新心跳时间
    n.lastHeartbeat = time.Now()
    
    // term太旧,拒绝
    if term < n.currentTerm {
        return false
    }
    
    // 发现更高的term,转为Follower
    if term > n.currentTerm {
        n.currentTerm = term
        n.state = Follower
        n.votedFor = -1
    }
    
    fmt.Printf("节点%d收到Leader%d的心跳,term=%d\n", n.id, leaderId, term)
    
    return true
}

🎉 总结

核心区别

1. 设计目标
   Paxos:理论完备性
   Raft:可理解性

2. 角色设计
   Paxos:Proposer, Acceptor, Learner(复杂)
   Raft:Leader, Follower, Candidate(清晰)

3. Leader
   Paxos:可以有多个Proposer竞争
   Raft:强Leader模型,同一时刻只有一个Leader

4. 日志
   Paxos:日志可以有空洞
   Raft:日志连续,不能有空洞

5. 实现
   Paxos:需要自己补充很多细节
   Raft:论文提供详细实现指导

选择建议

选Raft的场景:
✅ 新项目
✅ 团队理解分布式共识不深
✅ 需要快速实现
✅ 有现成的Raft库可用(etcd, Consul)
✅ 推荐 ⭐⭐⭐⭐⭐

选Paxos的场景:
✅ 已有Paxos实现
✅ 理论研究
✅ 特殊场景(如需要多个Proposer)

记忆口诀

共识算法解难题,
分布式系统达一致。

Paxos理论很完备,
两阶段协议晦涩难懂。
Proposer提出提案,
Acceptor进行投票,
Learner学习结果,
Multi-Paxos性能好,
但实现细节要补充。

Raft设计更清晰,
强Leader模型简单。
Leader处理写请求,
Follower跟随复制,
Candidate发起选举,
日志复制保一致。

三个子问题分解好:
Leader选举要投票,
日志复制保顺序,
安全性有保证!

Paxos偏理论,
Raft偏工程。
新项目用Raft,
易懂又易实现!

愿你的分布式系统达成共识,数据永远一致! 🗳️✨