RAFT 论文——村委会版
写给完全忘记 RAFT 的自己。读完这篇,等于把论文学了一遍。
登场角色
| 村委会里的人/物 | 论文里叫什么 |
|---|---|
| 村长 | Leader(领导者) |
| 委员 | Follower(跟随者) |
| 正在竞选的委员 | Candidate(候选人) |
| 小张(来办事的村民) | Client(客户端) |
| 记录本 | Log(日志) |
| 记录本里的一条 | Log Entry(日志条目) |
| 村长盖章生效 | Committed(已提交) |
| 本届任期编号 | Term |
| 村长定期打招呼 | Heartbeat(心跳) |
| 村长同步记录的消息 | AppendEntries RPC |
| 候选人拉票的消息 | RequestVote RPC |
| 发快照的消息 | InstallSnapshot RPC |
| 小张给消息贴的编号 | Session ID(流水号) |
第一章:RAFT 是什么
想象一个村委会,有 1 个村长、4 个委员,共 5 人。
村里所有决定——批地、盖房、开店——都要记在记录本里。
问题是:这 5 个人每人手里有一本,怎么保证 5 本始终一样?万一村长突然晕倒,怎么办?
RAFT 就是解决这个问题的方法。
核心思路三句话:
- 所有写入都经过村长,村长说了算
- 超过半数的人记下来,才算正式生效
- 记录最全的人才有资格当村长
第二章:正常运转——日志复制
小张来申请建新房
第一步:小张找到村长
小张只能找村长办事,不能找委员。
村长在自己的记录本上写下:
第88条:批准小张建房(未盖章)
第二步:村长通知所有委员
村长给 4 个委员发消息(AppendEntries RPC):
“大家在第88条写上:批准小张建房”
第三步:超过半数回复"已记"
| 委员 | 回复 |
|---|---|
| 委员A | ✅ 已记 |
| 委员B | ✅ 已记 |
| 委员C | ❌ 没反应(在睡觉) |
| 委员D | ✅ 已记 |
5 人里 4 人写好了,超过半数 → 可以生效。
第四步:村长盖章,通知大家也盖章
村长在自己本子上盖章(Commit),通知委员们也盖章,再告诉小张:“批了!”
那个睡觉的委员C呢?
不用管他。村长下次发消息时顺带把他落下的条目补给他,他自己追上来。
关键原则
超过半数写好就算数,不需要所有人都确认。
5 人村:3 人确认就够。
7 人村:4 人确认就够。
第三章:村长失联——领导者选举
村长定期"打招呼"
正常情况下,村长每隔一小段时间给所有委员发一条空消息(Heartbeat),意思是"我还在,别慌"。
委员们收到后重置自己心里的倒计时。
村长突然失联
委员们停止收到心跳,各自的倒计时开始走。
倒计时是随机的,150 到 300 毫秒之间,每个人不一样。
为什么随机?防止大家同时举手、同时拉票,谁也选不上。
先数完的人举手拉票
委员B的倒计时先到 → 升级为Candidate→ 把任期编号(Term)加一 → 给所有人发消息(RequestVote RPC):
“村长失联了!我要竞选,请投我一票!”
其他委员怎么投票?
两个条件都满足才投:
| 条件 | 说明 |
|---|---|
| 这轮没投过票 | 每轮每人只能投一票 |
| 候选人记录本不比我旧 | 记录落后的人没资格当村长 |
两个都满足 → 投票。任意一个不满足 → 拒绝。
当选
委员B收到超过半数的票 → 当选村长 → 立刻发心跳,告诉大家"我是新村长"。
平票或没人过半怎么办?
这轮作废,所有人重新随机倒数,再来一轮。
会不会永远选不出来?极低概率。每次碰撞都要求大家倒计时恰好一样长,连续碰撞的概率像连续中彩票,实际上几乎不发生。
第四章:新村长上任——记录怎么保持正确
为什么新村长记录一定最新?
选举时,候选人要"亮记录本"——通过 RequestVote RPC 告诉大家自己的最后一条记录编号。
委员的规则:你的记录比我旧,我不投你。
所以记录落后的候选人拿不到多数票,天然被淘汰。当选的人必然是记录最新的。
新村长上任后做什么?
把自己记录本里还没盖章的条目继续同步给所有委员,重新走一遍"超过半数确认 → 盖章"的流程,把之前没完成的事情收尾。
第五章:小张怎么找村长
正常情况
小张直接找村长办事。
村长挂了,小张不知道新村长是谁
小张随机挑一个委员去问。委员会拒绝办理,但告诉小张:
“我不是村长,去找B,他是新村长。”
委员通过收到的心跳消息知道谁是现任村长(AppendEntries RPC 里附带村长地址)。
选举还没结束,委员也不知道新村长是谁
委员无法提供地址,小张的请求超时,没有回应。
小张随机换一个委员再问,不是反复问同一个人。
选举通常在 150~300 毫秒内结束,多问几次就能得到答案。
第六章:村子要加人减人——集群成员变更
为什么不能直接换名单?
原来 5 人,加 2 人变 7 人。
如果消息发出去,有人早收到、有人晚收到,可能同一时刻:
- 用旧名单(5人)的人:3票过半 → 选出村长甲
- 用新名单(7人)的人:4票过半 → 选出村长乙
两个村长同时存在,记录本就乱了。
解法:联合共识(Joint Consensus)——过渡期
村长不发"换成新名单",而是发一张过渡公告,同时写着新旧两份名单:
过渡公告:
旧名单:A B C D E
新名单:A B C D E F G
收到此公告的人,两份名单都要用
过渡期投票规则:一票同时计入两份名单,两边都要超过半数才算通过。
为什么这样就安全了?(鸽巢原理)
用旧规则选出的村长:必须拿到旧名单里的多数(≥3票)
用新旧双规则选出的村长:也必须拿到旧名单里的多数(≥3票)
两边合计需要至少 3+3=6 票,但旧名单只有 5 个人,每人只有一票,6>5,票不够分。
结论:两个"多数派"必然重叠,不可能同时存在两个合法村长。
过渡期结束
村长确认过渡公告被多数人收到后,发第二份公告,切换到纯新名单。过渡期通常只持续几百毫秒。
第七章:记录本太长怎么办——快照
问题
村子运行 3 年,记录本写了 10 万条。新来的委员 F 要从头抄,抄到什么时候?
解法:定期拍快照(Snapshot)
每隔一段时间,把前面所有记录压缩成一张现状表:
| 村民 | 当前状态 |
|---|---|
| 小张 | 已批建房 |
| 小李 | 已批开店 |
| 小王 | 申请中 |
这张表代替了前面所有记录,旧记录直接删掉。
快照里还要记两个数字:
- 压缩到第几条(比如第400条)
- 那条记录的任期编号
用来告诉别人"从第401条开始接着抄"。
每个委员自己决定拍快照的时机
不需要村长统一指挥,记录本超过一定大小就自己拍。
新委员落后太多怎么办?
村长通过InstallSnapshot RPC把快照切成小块发给委员 F:
村长 → F:第1块(共10块) 村长 → F:第2块 ... 村长 → F:第10块,发完了F 收齐后整体替换自己的记录本,从快照最后一条编号接着同步新记录。
收到一半村长挂了?F 把收到的块存在草稿里,没收齐不替换正式记录本,新村长上任后重新发一遍。
第八章:小张重复发消息怎么办——线性化语义
问题
- 小张发"批准建房"
- 村长处理了,盖章生效
- 村长回复的消息路上丢了
- 小张没收到,再发一次
- 会不会被批两次?
解法:流水号(Session ID)
小张给每条消息贴唯一编号:
消息#001:批准建房
村长处理完,把"#001已处理"记在本子上。
下次再收到"消息#001",查一下,发现处理过了,直接回放上次结果,不重新执行。
村长换人了也没事——流水号记录随日志同步,新村长也知道哪些已处理。
论文把这叫做Linearizability(线性化语义):每条操作恰好执行一次,不多不少。
第九章:三条铁律——安全性保证
| 铁律 | 含义 | 村委会说法 |
|---|---|---|
| Election Safety | 每个 Term 最多只有一个 Leader | 同一届任期只能有一个村长 |
| Leader Completeness | 已提交的条目未来所有 Leader 都有 | 盖了章的记录永远不消失 |
| State Machine Safety | 所有节点在同一编号位置执行的命令相同 | 所有记录本在同一行写的内容完全一样 |
最后一句话
强领导者统一写入,随机超时避免选举冲突,多数派确认保证持久,记录越新越有资格当选——四点合在一起,让分布式系统的记录本永远不乱。
基于 RAFT 论文(In Search of an Understandable Consensus Algorithm,Diego Ongaro & John Ousterhout)整理