摘要:从一次"为什么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
→ 发起选举
选举流程
初始状态:
节点A(Follower)
节点B(Follower)
节点C(Follower)
T1: 节点A超时,变成Candidate
↓
T2: 节点A:term+1(任期号),给自己投票
term = 1, vote = A
↓
T3: 节点A向B和C发起投票请求(RequestVote)
↓
T4: 节点B收到请求,判断:
- A的term >= B的term? 是
- B还没给别人投票? 是
→ 投票给A
↓
T5: 节点C收到请求,也投票给A
↓
T6: 节点A收到2票(B和C)+ 自己1票 = 3票
总共3个节点,3票 > 3/2(过半数)
↓
T7: 节点A成为Leader ✅
↓
T8: 节点A向B和C发送心跳(AppendEntries)
↓
T9: B和C收到心跳,变成Follower,承认A是Leader
选举时序图
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票
场景:
节点A和B同时发起选举(term=2)
节点C先收到A的请求 → 投票给A ✅
节点C再收到B的请求 → 拒绝(已经投给A了)❌
结果:A获得多数票,B失败
规则3:随机超时
每个Follower的超时时间是随机的(150-300ms)
目的:避免同时发起选举(split vote)
示例:
节点A超时时间:180ms
节点B超时时间:250ms
节点C超时时间:200ms
→ A先超时,发起选举
→ B和C投票给A
→ A当选,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
三种算法对比
| 特性 | Paxos | Raft | ZAB |
|---|---|---|---|
| 提出时间 | 1990年 | 2014年 | 2011年 |
| 易理解性 | ⭐ 难 | ⭐⭐⭐⭐⭐ 易 | ⭐⭐⭐ |
| 角色 | Proposer、Acceptor、Learner | Leader、Follower、Candidate | Leader、Follower |
| 是否有Leader | ❌ 无(Multi-Paxos有) | ✅ 有 | ✅ 有 |
| 日志复制 | 复杂 | 简单清晰 | 简单 |
| 应用 | Chubby(Google) | etcd、Consul | Zookeeper |
为什么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如何处理脑裂?
什么是脑裂?
网络分区场景:
节点A、B在分区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的区别?
答案:
核心区别:
| 特性 | Paxos | Raft |
|---|---|---|
| 易理解性 | 难(抽象) | 易(具象) |
| 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处理请求
日志复制保一致,过半确认才提交
随机超时避冲突,脑裂问题过半解