盘一盘Raft算法到底是什么

摘要:从一次"为什么Zookeeper选举这么慢"的疑问出发,深度剖析Raft分布式一致性算法的核心原理。通过Leader选举、日志复制、安全性保证的完整流程图解,以及与Paxos算法的对比,揭秘为什么Raft比Paxos更易懂、如何保证分布式系统的强一致性、以及etcd和Consul的实现原理。手写200行代码模拟Raft选举过程,配合时序图展示脑裂处理和日志复制流程。


💥 翻车现场

周二下午,哈吉米在研究分布式系统。

哈吉米(看文档):"Zookeeper用的是Paxos算法……咦,好像又不完全是?还有个ZAB协议?"

南北绿豆(路过):"Zookeeper用的是ZAB,类似Paxos但有改进。"
哈吉米:"Paxos是啥?我看了半天论文,完全看不懂……"
阿西噶阿西(凑过来):"Paxos确实难懂,但有个更简单的算法:Raft!"
哈吉米:"Raft?"
南北绿豆:"Raft的设计目标就是:比Paxos更容易理解!来,我给你讲讲。"


🤔 为什么需要Raft?分布式一致性问题

问题场景

阿西噶阿西在白板上画了一个场景。

分布式系统:
3台服务器存储配置数据

服务器A:config = "version-1"
服务器B:config = "version-1"
服务器C:config = "version-1"

客户端更新配置:config = "version-2"

问题:
1. 同时更新3台服务器?
   → 如果只更新了1台就宕机,数据不一致 ❌

2. 先更新A,再更新B,再更新C?
   → 更新过程中,客户端读到的数据不一致 ❌

3. 如何保证3台服务器的数据一致?
   → 需要一致性算法

一致性算法的目标

目标:
1. 数据一致:所有节点的数据最终一致
2. 高可用:部分节点宕机,系统仍可用
3. 分区容错:网络分区时,系统仍能正常工作

经典算法:
- Paxos(1990年,难懂)
- Raft(2014年,易懂)
- ZAB(Zookeeper专用)

南北绿豆:"Raft的论文标题就是:《In Search of an Understandable Consensus Algorithm》(寻找一种易于理解的一致性算法)。"


🎯 Raft的核心思想

核心角色

Raft集群中的3种角色

1. Leader(领导者)
   - 处理所有客户端请求
   - 负责日志复制
   - 同一时刻只有1个Leader

2. Follower(跟随者)
   - 被动接收Leader的日志
   - 响应Leader的心跳
   - 大部分时间都是Follower

3. Candidate(候选人)
   - 选举时的临时角色
   - Follower超时未收到心跳 → 变成Candidate
   - Candidate发起选举

状态转换图

stateDiagram-v2
    [*] --> Follower: 启动
    
    Follower --> Candidate: 超时未收到心跳
    Candidate --> Leader: 获得多数票
    Candidate --> Follower: 发现更高term的Leader
    Candidate --> Candidate: 选举超时,重新选举
    
    Leader --> Follower: 发现更高term的节点
    
    Note right of Follower: 接收日志<br/>响应心跳
    Note right of Candidate: 发起选举<br/>拉票
    Note right of Leader: 处理请求<br/>复制日志<br/>发送心跳

🎯 Raft的3大核心机制

机制1:Leader选举

选举触发条件

Follower在一定时间内(150-300ms随机)没收到Leader心跳
→ 认为Leader挂了
→ 变成Candidate
→ 发起选举

选举流程

初始状态:
节点AFollower)
节点BFollower)
节点CFollowerT1: 节点A超时,变成CandidateT2: 节点Aterm+1(任期号),给自己投票
    term = 1, vote = AT3: 节点ABC发起投票请求(RequestVote)
    ↓
T4: 节点B收到请求,判断:
    - Aterm >= Bterm? 是
    - B还没给别人投票? 是
    → 投票给AT5: 节点C收到请求,也投票给AT6: 节点A收到2票(BC)+ 自己1票 = 3票
    总共3个节点,3票 > 3/2(过半数)
    ↓
T7: 节点A成为Leader ✅
    ↓
T8: 节点ABC发送心跳(AppendEntries)
    ↓
T9: BC收到心跳,变成Follower,承认ALeader

选举时序图

sequenceDiagram
    participant NodeA as 节点A(Candidate)
    participant NodeB as 节点B(Follower)
    participant NodeC as 节点C(Follower)

    Note over NodeA: 超时,变成Candidate
    NodeA->>NodeA: 1. term+1, 给自己投票
    
    par 并行发送投票请求
        NodeA->>NodeB: 2. RequestVote(term=1)
        NodeA->>NodeC: 3. RequestVote(term=1)
    end
    
    NodeB->>NodeB: 4. 判断:term够新,投票给A
    NodeB->>NodeA: 5. 返回:同意
    
    NodeC->>NodeC: 6. 判断:term够新,投票给A
    NodeC->>NodeA: 7. 返回:同意
    
    NodeA->>NodeA: 8. 收到3票(过半数)
    Note over NodeA: 成为Leader ✅
    
    par 发送心跳
        NodeA->>NodeB: 9. AppendEntries(心跳)
        NodeA->>NodeC: 10. AppendEntries(心跳)
    end
    
    Note over NodeB,NodeC: 承认A是Leader,变成Follower

选举的关键规则

规则1:任期号(term)

每次选举,term+1

示例:
- 第1次选举:term=1,A当选Leader
- A宕机,第2次选举:term=2,B当选Leader
- B宕机,第3次选举:term=3,C当选Leader

规则:
- 收到更高term的消息 → 立即变成Follower
- 收到更低term的消息 → 拒绝

规则2:投票规则

每个节点在同一term内只能投1票

场景:
节点AB同时发起选举(term=2)
节点C先收到A的请求 → 投票给A ✅
节点C再收到B的请求 → 拒绝(已经投给A了)❌

结果:A获得多数票,B失败

规则3:随机超时

每个Follower的超时时间是随机的(150-300ms)

目的:避免同时发起选举(split vote)

示例:
节点A超时时间:180ms
节点B超时时间:250ms
节点C超时时间:200msA先超时,发起选举
→ B和C投票给AA当选,B和C不再超时(收到A的心跳)

🎯 机制2:日志复制

日志复制流程

场景:客户端写入数据

1. 客户端向Leader发送写请求:set x=5
   ↓
2. Leader写入本地日志(未提交)
   log: [set x=5, term=1, index=1]
   ↓
3. Leader向所有Follower复制日志(AppendEntries)
   ↓
4. Follower写入本地日志,返回确认
   ↓
5. Leader收到多数确认(2/3)
   → 提交日志(应用到状态机)
   → x=5生效 ✅
   ↓
6. Leader返回客户端:"写入成功"
   ↓
7. Leader通知Follower提交日志

日志复制时序图

sequenceDiagram
    participant Client as 客户端
    participant Leader as Leader(A)
    participant FollowerB as Follower(B)
    participant FollowerC as Follower(C)

    Client->>Leader: 1. 写请求:set x=5
    Leader->>Leader: 2. 写本地日志(未提交)
    Note over Leader: log[1]: set x=5, term=1
    
    par 并行复制日志
        Leader->>FollowerB: 3. AppendEntries(log[1])
        Leader->>FollowerC: 4. AppendEntries(log[1])
    end
    
    FollowerB->>FollowerB: 5. 写本地日志
    FollowerB->>Leader: 6. 返回成功
    
    FollowerC->>FollowerC: 7. 写本地日志
    FollowerC->>Leader: 8. 返回成功
    
    Leader->>Leader: 9. 收到2个确认(过半数)
    Note over Leader: 提交日志,x=5生效 ✅
    
    Leader->>Client: 10. 返回成功
    
    par 通知Follower提交
        Leader->>FollowerB: 11. 下次心跳:commit log[1]
        Leader->>FollowerC: 12. 下次心跳:commit log[1]
    end
    
    Note over FollowerB,FollowerC: Follower提交日志,x=5生效

日志结构

每个节点的日志:

index | term | command
------|------|--------
  1   |  1   | set x=5
  2   |  1   | set y=10
  3   |  2   | set x=8
  4   |  2   | delete y

字段说明:
- index:日志索引(递增)
- term:任期号(Leader的term)
- command:具体命令

日志一致性保证

规则1:Leader的日志是权威的

Leader的日志:
[set x=5, set y=10, set x=8]

Follower的日志:
[set x=5, set y=10]  ← 少了一条

Leader发现Follower日志落后:
→ 复制缺失的日志给Follower
→ Follower日志变成:[set x=5, set y=10, set x=8]
→ 一致 ✅

规则2:Follower的冲突日志会被覆盖

Leader的日志(term=2):
[set x=5, set y=10, set x=8]

Follower的日志(term=1):
[set x=5, set y=10, delete x]  ← 第3条冲突(term不同)

Leader发现冲突:
→ 删除Follower的第3条日志
→ 复制Leader的日志
→ Follower日志变成:[set x=5, set y=10, set x=8]
→ 一致 ✅

南北绿豆:"这就是Leader的日志是权威的,Follower必须和Leader保持一致。"


🎯 机制3:安全性保证

问题:如何防止已提交的日志丢失?

场景

T1: Leader(A)提交了log[3]: set x=8
    A的日志:[set x=5, set y=10, set x=8](已提交)
    B的日志:[set x=5, set y=10, set x=8](已提交)
    C的日志:[set x=5, set y=10](未收到log[3])

T2: A宕机

T3: 选举新Leader
    问题:如果C当选Leader,log[3]会丢失吗?

Raft的选举限制

规则:只有拥有最新、最完整日志的节点才能当选Leader

投票判断:
Candidate请求投票时,携带自己的日志信息:
- lastLogIndex:最后一条日志的索引
- lastLogTerm:最后一条日志的term

Follower判断是否投票:
if (候选人的lastLogTerm > 我的lastLogTerm) {
    投票给候选人(他的日志更新)
} else if (候选人的lastLogTerm == 我的lastLogTerm) {
    if (候选人的lastLogIndex >= 我的lastLogIndex) {
        投票给候选人(他的日志更完整)
    } else {
        拒绝(我的日志更完整)
    }
} else {
    拒绝(我的日志更新)
}

回到场景

选举时:
节点B:lastLogIndex=3, lastLogTerm=1
节点C:lastLogIndex=2, lastLogTerm=1

B请求C投票:
C判断:B的lastLogIndex(3) > C的lastLogIndex(2)
→ C投票给B ✅

C请求B投票:
B判断:C的lastLogIndex(2) < B的lastLogIndex(3)
→ B拒绝 ❌

结果:B当选Leader,log[3]不会丢失

阿西噶阿西:"这就是Raft的安全性保证:已提交的日志永远不会丢失。"


🎯 Raft vs Paxos vs ZAB

三种算法对比

特性PaxosRaftZAB
提出时间1990年2014年2011年
易理解性⭐ 难⭐⭐⭐⭐⭐ 易⭐⭐⭐
角色Proposer、Acceptor、LearnerLeader、Follower、CandidateLeader、Follower
是否有Leader❌ 无(Multi-Paxos有)✅ 有✅ 有
日志复制复杂简单清晰简单
应用Chubby(Google)etcd、ConsulZookeeper

为什么Raft更易懂?

Paxos的问题

Paxos:
1. 概念抽象(Proposer、Acceptor、Learner)
2. 多阶段协议(Prepare、Promise、Accept、Accepted)
3. 需要Multi-Paxos才实用(原始Paxos只能达成一次共识)

论文:Leslie Lamport用希腊议会的故事类比,更难懂

Raft的优势

Raft:
1. 概念清晰(Leader选举、日志复制、安全性)
2. 强Leader模型(所有请求走Leader)
3. 随机超时避免冲突
4. 论文通俗易懂(配图丰富)

设计目标:Understandable(可理解)

南北绿豆:"Raft把复杂的一致性问题拆分成3个独立的子问题:选举、复制、安全,逐个击破。"


🎯 Raft的实际应用

etcd(Kubernetes的核心)

etcd:
- 分布式KV存储
- 基于Raft实现
- Kubernetes用etcd存储集群状态

特点:
- 强一致性(CP模型)
- 高可用(3-5个节点)
- 支持watch机制

Consul(服务发现)

Consul:
- 服务注册与发现
- 健康检查
- KV存储
- 基于Raft实现

特点:
- 强一致性
- 多数据中心

🎯 手写Raft选举模拟器

哈吉米:"能不能自己模拟一下Raft选举?"

阿西噶阿西:"来,200行代码模拟核心流程!"

/**
 * Raft选举模拟器(简化版)
 */
public class RaftElectionSimulator {
    
    // 节点角色
    enum Role {
        FOLLOWER, CANDIDATE, LEADER
    }
    
    // 节点
    static class Node {
        String id;
        Role role = Role.FOLLOWER;
        int currentTerm = 0;           // 当前任期
        String votedFor = null;        // 投票给谁
        int voteCount = 0;             // 收到的票数
        
        public Node(String id) {
            this.id = id;
        }
        
        /**
         * 发起选举
         */
        public void startElection(List<Node> allNodes) {
            System.out.println("\n[" + id + "] 超时,发起选举");
            
            // 1. 变成Candidate
            role = Role.CANDIDATE;
            currentTerm++;
            votedFor = id;  // 给自己投票
            voteCount = 1;
            
            System.out.println("[" + id + "] term=" + currentTerm + ", 给自己投票");
            
            // 2. 向其他节点请求投票
            for (Node node : allNodes) {
                if (node == this) continue;
                
                boolean granted = node.requestVote(this);
                if (granted) {
                    voteCount++;
                    System.out.println("[" + id + "] 收到[" + node.id + "]的投票,当前票数: " + voteCount);
                }
            }
            
            // 3. 判断是否当选
            int majority = (allNodes.size() / 2) + 1;
            if (voteCount >= majority) {
                role = Role.LEADER;
                System.out.println("[" + id + "] 当选Leader!票数: " + voteCount + "/" + allNodes.size());
            } else {
                role = Role.FOLLOWER;
                System.out.println("[" + id + "] 选举失败,回到Follower");
            }
        }
        
        /**
         * 接收投票请求
         */
        public boolean requestVote(Node candidate) {
            // 判断是否投票
            if (candidate.currentTerm < this.currentTerm) {
                // 候选人的term太旧,拒绝
                System.out.println("[" + id + "] 拒绝[" + candidate.id + "],term太旧");
                return false;
            }
            
            if (candidate.currentTerm > this.currentTerm) {
                // 候选人的term更新,更新自己的term
                this.currentTerm = candidate.currentTerm;
                this.votedFor = null;
                this.role = Role.FOLLOWER;
            }
            
            if (this.votedFor == null || this.votedFor.equals(candidate.id)) {
                // 还没投票,或者已经投给他了
                this.votedFor = candidate.id;
                System.out.println("[" + id + "] 投票给[" + candidate.id + "]");
                return true;
            }
            
            System.out.println("[" + id + "] 拒绝[" + candidate.id + "],已投给[" + votedFor + "]");
            return false;
        }
    }
    
    /**
     * 测试
     */
    public static void main(String[] args) {
        // 创建3个节点
        List<Node> nodes = Arrays.asList(
            new Node("A"),
            new Node("B"),
            new Node("C")
        );
        
        // 模拟节点A发起选举
        nodes.get(0).startElection(nodes);
    }
}

运行结果

[A] 超时,发起选举
[A] term=1, 给自己投票
[B] 投票给[A]
[A] 收到[B]的投票,当前票数: 2
[C] 投票给[A]
[A] 收到[C]的投票,当前票数: 3
[A] 当选Leader!票数: 3/3

哈吉米:"原来Raft的选举就是:term+1、给自己投票、请求其他节点投票、过半数当选!"


🎯 Raft如何处理脑裂?

什么是脑裂?

网络分区场景:
节点AB在分区1(网络正常)
节点C、D、E在分区2(网络正常)
分区1和分区2网络断开

问题:
- 分区1可能选出Leader A
- 分区2可能选出Leader C
- 同时存在2个Leader(脑裂)❌

Raft的解决方案

过半数机制

5个节点的集群:
需要3票才能当选(5/2 + 1 = 3)

网络分区:
分区1:A、B(2个节点)
分区2:C、D、E(3个节点)

分区1选举:
- 最多2票(A和B)
- 2票 < 3票(过半数)
- 无法选出Leader ❌

分区2选举:
- 可以获得3票(C、D、E)
- 3票 >= 3票(过半数)
- 可以选出Leader ✅

结果:
- 只有分区2有Leader
- 分区1的节点是Follower,无法处理写请求
- 避免了脑裂

时序图

sequenceDiagram
    participant A as 节点A(分区1)
    participant B as 节点B(分区1)
    participant C as 节点C(分区2)
    participant D as 节点D(分区2)
    participant E as 节点E(分区2)

    Note over A,E: 网络分区发生
    
    rect rgb(255, 230, 230)
        Note over A,B: 分区1(2个节点)
        A->>A: 发起选举
        A->>B: 请求投票
        B->>A: 投票
        Note over A: 只有2票,无法当选 ❌
    end
    
    rect rgb(230, 255, 230)
        Note over C,E: 分区2(3个节点)
        C->>C: 发起选举
        C->>D: 请求投票
        C->>E: 请求投票
        D->>C: 投票
        E->>C: 投票
        Note over C: 3票,当选Leader ✅
    end
    
    Note over C,E: 分区2可以正常工作<br/>分区1无法处理写请求<br/>避免了脑裂

南北绿豆:"所以Raft通过过半数机制,保证同一时刻最多只有1个Leader。"


🎓 面试标准答案

题目:Raft算法的核心原理是什么?

答案

Raft是分布式一致性算法,保证多个节点的数据一致。

3大核心机制

1. Leader选举

  • 3种角色:Leader、Follower、Candidate
  • Follower超时 → 变Candidate → 发起选举
  • 获得多数票 → 成为Leader
  • 随机超时避免冲突

2. 日志复制

  • 客户端写请求发给Leader
  • Leader写本地日志,复制给Follower
  • 收到多数确认 → 提交日志
  • Leader通知Follower提交

3. 安全性

  • 只有日志最新的节点能当选Leader
  • 已提交的日志永不丢失
  • Leader的日志是权威的

过半数机制

  • 选举需要过半数投票
  • 日志提交需要过半数确认
  • 防止脑裂

应用

  • etcd(Kubernetes)
  • Consul(服务发现)
  • TiKV(分布式数据库)

题目:Raft和Paxos的区别?

答案

核心区别

特性PaxosRaft
易理解性难(抽象)易(具象)
Leader无(Multi-Paxos有)有(强Leader)
日志顺序可以乱序必须顺序
设计目标理论完备工程实用

Raft的优势

  • 概念清晰(Leader选举、日志复制)
  • 强Leader模型(简化设计)
  • 论文易懂(配图丰富)

适用场景

  • 工程实现:Raft
  • 学术研究:Paxos

题目:Raft如何防止脑裂?

答案

过半数机制

  • 选举需要过半数投票(N/2 + 1)
  • 网络分区时,最多只有一个分区能满足过半数
  • 只有满足过半数的分区能选出Leader
  • 其他分区无法选出Leader,也无法处理写请求

示例:5个节点,分成2+3

  • 2个节点的分区:最多2票,无法过半
  • 3个节点的分区:可以3票,能过半,能选出Leader

结论:同一时刻最多只有1个Leader


🎉 结束语

晚上10点,哈吉米终于理解了Raft算法。

哈吉米:"原来Raft就是:Leader选举、日志复制、安全性保证,三个机制组合起来!"

南北绿豆:"对,Raft把复杂的一致性问题拆分成3个子问题,逐个解决。"

阿西噶阿西:"记住:强Leader模型、过半数机制、随机超时,这是Raft的3个关键设计。"

哈吉米:"还有Raft比Paxos易懂多了,难怪etcd和Consul都用Raft!"

南北绿豆:"对,工程实践中,Raft是更好的选择!"


记忆口诀

Raft一致性算法,三种角色选Leader
Follower超时变Candidate,发起选举拉票
过半数票当选Leader,强Leader处理请求
日志复制保一致,过半确认才提交
随机超时避冲突,脑裂问题过半解