深蓝黑背景品牌封面:上方居中是大标题「Raft:多节点怎么达成一致」,副标题「Leader 选举与日志复制」;下方横贯画面是原创装饰——三个服务器节点排开,中间节点以紫色高亮并标注 Leader,向左右两个 Follower 各发出一道心跳箭头,Leader 下方有一条逐渐变实的日志条示意复制进度;左上标注「经典题解析 · 系统设计」,右上标注「Day 02 · 10-10」 图 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 可用性结论的速查表:五节点集群容忍两台宕机;脑裂时多数派分区继续服务,少数派分区安全地停摆——宁可不可用,绝不出错。

状态机加流程图:左侧是 Raft 三种角色的状态机——所有节点从 Follower 出发,超时没收到心跳就变成 Candidate 发起选举,拿到多数票当选 Leader;任何角色发现更高任期号就退回 Follower。右侧是日志复制五步——客户端写请求只找 Leader,Leader 先追加到自己日志,再广播给所有 Follower,多数节点落盘后这一条才算提交,最后各节点按序应用到状态机。底部说明脑裂防护:任期号让过期 Leader 自动失效,多数派原则让网络分区中的少数派永远选不出 Leader,分区恢复后以任期高者为准 图 2:角色状态机、日志复制五步与脑裂防护的两条规则。

可能的追问与易错点。

「选举超时设多长?」太短会频繁误触发选举,太长会让故障恢复变慢,工程实现里通常给每个节点随机化超时时间,避免大家同时发起选举互相抢票导致反复平票。「新 Leader 的日志不全怎么办?」选举时有额外约束:只有日志不落后(按 term 和日志长度比较)的节点才能拿到选票,保证选出的 Leader 一定包含所有已提交日志。

易错点主要有三:一,以为「多数派」是存活节点的一半,其实它永远按集群总配置数算——五台集群挂了四台,剩下那一台哪怕活着也做不了决策。二,讲选举时忘了 term 的作用,说不出过期 Leader 如何被推翻。三,把「复制到多数节点」和「提交」混为一谈,漏掉「先提交再应用」的顺序。

回到面试现场,建议的答题节奏:一句话定义共识问题,讲三种角色与选举流程,走一遍日志复制五步,最后主动用脑裂场景收尾。这套机制在工程里随处可见:模型注册中心、特征存储、向量库集群的元数据一致性,背后都是同一类共识协议。

你排查过 etcd 或类似组件的选主抖动吗?当时是什么原因?欢迎分享。