图 1:本文封面,Leader 向 Follower 发心跳并推进日志复制。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。Raft 是分布式面试的分水岭题目——它背后站着 etcd、Consul 和一大批控制面组件,讲得清 Raft,就拿到了理解这一整类系统的钥匙。
想象面试现场:「多个节点的集群怎么对一份数据达成一致?讲讲 Raft。」
第一步,先讲问题本身。
一台服务器存一份数据,机器挂了数据就没了。于是把同一份数据复制到多台机器上,客户端写一次,集群里所有节点都记下——听起来简单,难在「所有节点记下的必须是同一份、同一顺序」。机器会宕机、网络会分区、消息会迟到乱序,在这样的环境里让多数节点始终 agree 于同一条日志序列,就是共识问题。共识算法的目标用一句话说:只要多数节点活着,集群就能继续工作,并且永远不会给出错误结果。
第二步,Raft 的解题思路:先选一个说了算的。
Raft 的聪明之处在于把共识拆成了两个相对独立的子问题,并且引入强 Leader:任何时刻集群里最多一个 Leader,所有写请求都找它,一致性从「大家互相说服」简化为「大家跟着 Leader 走」。
每个节点处于三种角色之一:Follower(跟随者)、Candidate(候选者)、Leader(领导者)。所有节点从 Follower 出发;在一段时间内没收到 Leader 的心跳,就怀疑 Leader 死了,于是自增任期号(term)、变成 Candidate 发起选举;拿到多数节点的票就当选 Leader。任期号是 Raft 的逻辑时钟:任何节点发现别人的 term 比自己高,立刻退回 Follower——过期的 Leader 自动失效,这是防止两个「皇帝」并存的关键机制。
第三步,日志复制:写请求的五步旅程。
客户端的写请求只发给 Leader。Leader 先把这条操作追加到自己的日志里(此时未提交),然后并行地广播给所有 Follower;Follower 落盘后回复确认;当多数节点(含 Leader 自己)都记下这条日志,它就算「已提交」,Leader 把它应用到状态机并回复客户端,随后的心跳会通知 Follower 跟进应用。「多数落盘才算提交」是安全性的核心:这条日志之后绝不会丢失或改序。
第四步,脑裂:网络分区时怎么办。
网络把五节点集群切成三加二两半,会发生什么?含三节点的多数派分区能正常选出 Leader、继续服务;两节点的少数派分区里,Candidate 永远凑不齐三票,选不出 Leader,也就拒绝写入。分区恢复后,少数派发现外面有更高的 term,自动退回 Follower 并补齐落后的日志。所以 Raft 的脑裂防护不是「检测分区」,而是两条朴素规则:多数派才能决策,任期高者为准。
下面这个最小模拟把「多数派门槛」算给你看,用 Node.js 22.15.0 直接运行(node raft-sim.js)即可:
'use strict';
// Raft 选举的最小模拟:拿到多数派(majority)选票才选得出 Leader
function elect(aliveCount, totalCount) {
const majority = Math.floor(totalCount / 2) + 1;
// 候选者向所有存活节点(含自己)拉票,简化为一轮收齐全部存活节点的票
const votes = aliveCount;
return { votes, majority, elected: votes >= majority };
}
function show(label, alive, total) {
const r = elect(alive, total);
console.log(`${label}:存活 ${alive}/${total} 节点,得票 ${r.votes},多数派需要 ${r.majority} 票 → ${r.elected ? '选出 Leader,集群可写' : '选不出 Leader,集群停止写入'}`);
}
// 五节点集群
show('全部健康', 5, 5);
show('宕机 2 台', 3, 5);
show('宕机 3 台', 2, 5);
// 网络分区(脑裂场景):5 台分成 3 + 2 两半
console.log('--- 网络分区 ---');
show('分区 A 侧', 3, 5);
show('分区 B 侧', 2, 5);
实测输出如下:
全部健康:存活 5/5 节点,得票 5,多数派需要 3 票 → 选出 Leader,集群可写
宕机 2 台:存活 3/5 节点,得票 3,多数派需要 3 票 → 选出 Leader,集群可写
宕机 3 台:存活 2/5 节点,得票 2,多数派需要 3 票 → 选不出 Leader,集群停止写入
--- 网络分区 ---
分区 A 侧:存活 3/5 节点,得票 3,多数派需要 3 票 → 选出 Leader,集群可写
分区 B 侧:存活 2/5 节点,得票 2,多数派需要 3 票 → 选不出 Leader,集群停止写入
五行输出就是 Raft 可用性结论的速查表:五节点集群容忍两台宕机;脑裂时多数派分区继续服务,少数派分区安全地停摆——宁可不可用,绝不出错。
图 2:角色状态机、日志复制五步与脑裂防护的两条规则。
可能的追问与易错点。
「选举超时设多长?」太短会频繁误触发选举,太长会让故障恢复变慢,工程实现里通常给每个节点随机化超时时间,避免大家同时发起选举互相抢票导致反复平票。「新 Leader 的日志不全怎么办?」选举时有额外约束:只有日志不落后(按 term 和日志长度比较)的节点才能拿到选票,保证选出的 Leader 一定包含所有已提交日志。
易错点主要有三:一,以为「多数派」是存活节点的一半,其实它永远按集群总配置数算——五台集群挂了四台,剩下那一台哪怕活着也做不了决策。二,讲选举时忘了 term 的作用,说不出过期 Leader 如何被推翻。三,把「复制到多数节点」和「提交」混为一谈,漏掉「先提交再应用」的顺序。
回到面试现场,建议的答题节奏:一句话定义共识问题,讲三种角色与选举流程,走一遍日志复制五步,最后主动用脑裂场景收尾。这套机制在工程里随处可见:模型注册中心、特征存储、向量库集群的元数据一致性,背后都是同一类共识协议。
你排查过 etcd 或类似组件的选主抖动吗?当时是什么原因?欢迎分享。