想象一场盛大的合唱演出:指挥家挥动指挥棒,几十位乐手各拿一份谱子,同一瞬间发出同一个音,才能成就一段和谐旋律。可如果每个乐手各唱各调,或者临时换了指挥却没人知道该听谁的,舞台瞬间就会变成灾难现场。分布式系统的世界,正是这样一个“合唱团”:几十台服务器(乐手)必须对同一份“乐谱”(日志)达成一致,才能对外提供可靠服务。Raft,就是那套让整个合唱团不跑调、不乱序的“排练总规则”。 Raft把“如何让一堆机器达成一致”这个看似高深的问题,拆成了三件老百姓都听得懂的小事:选指挥(Leader选举)、发谱子(日志复制)、守规矩(安全性约束)。它不追求刻意刁钻的数学模型,而是坚持“人一定要看得懂”——正因如此,它比曾经高高在上的Paxos亲民得多,也成了etcd、Consul等现代系统验证过的“实战指挥棒”。接下来,让我们从合唱团的第一次排练开始,一步步揭开Raft日志复制的全部秘密。

岗位关联

学会Raft日志复制,你就握住了分布式共识的钥匙——这是理解etcd、Kubernetes等工业级系统内幕的根基,也是面向高可用架构师岗位必考的硬核技能。

目录
  1. 可视化演示
  2. 1. 角色与转换
  3. 2. 任期与选举
  4. 3. 日志结构
  5. 4. 复制流程
  6. 5. 日志匹配
  7. 6. 安全性规则
  8. 7. 对比Paxos
  9. 8. 工业应用
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

1. 角色与转换

合唱团里,指挥家挥舞指挥棒,乐手们安静等待指令,而一旦指挥家缺席,总得有人站出来临时带队。Raft把集群中的每台服务器也划成了三种身份:Leader(指挥家)、Follower(乐手)、Candidate(候选人)。Leader是唯一能向客户端承诺写入的人;Follower只是被动响应Leader的指令;Candidate则是选举阶段“站出来参选”的临时身份。

把视角从舞台搬到机房:每台服务器都运行着Raft模块,它们通过日志复制共同维护一份状态机。客户端只把写请求发给Leader,Leader把操作封装成日志条目广播给所有Follower。Follower收到后写入本地日志并返回确认。整个过程就像指挥家把新乐谱逐页传给乐手,乐手们逐一确认“谱子收到”。

三种角色并非一成不变。Raft用状态机描述转换关系:Follower在一段时间内收不到合法Leader的心跳,就转为Candidate并发起选举;Candidate获得多数选票后转正为Leader;如果选举失败,则退回Follower等待下一轮机会。任何时刻集群中最多只有一个有效的Leader,这是整个共识机制的基石。

三种角色的切换就像乐团的岗位轮换:人人皆可竞选,但同一时刻只能有一位上台执棒。这个简单直观的状态机,为后续日志复制、安全性规则搭建好了舞台。

Follower→选举超时Candidate→多数票Leader\text{Follower} \xrightarrow{\text{选举超时}} \text{Candidate} \xrightarrow{\text{多数票}} \text{Leader}
这台“角色状态机”说明了Raft最核心的思路:用最简单的三角色轮转,替代Paxos中纷繁复杂的角色关系。
  1. 初始状态(Follower)所有节点启动时都是Follower,像刚入团的乐手,先安静等待指挥。
    Follower: 等待心跳或选举超时\text{Follower: 等待心跳或选举超时}
  2. 触发转换(选举超时)Follower在随机超时内没有接收到Leader的有效心跳,就举起手宣布“我要竞选”。
    超时未收到心跳⇒转为 Candidate\text{超时未收到心跳} \Rightarrow \text{转为 Candidate}
  3. 投票环节(多数票)Candidate把当前任期加一,先投自己一票,再向所有其他节点广播RequestVote请求。
    votes≥⌊N2⌋+1⇒当选\text{votes} \ge \lfloor \tfrac{N}{2} \rfloor + 1 \Rightarrow \text{当选}
  4. 转正上台(Leader)一旦票数过半,Candidate立即成为新一任Leader,开始向全员发送心跳确立权威。
    Candidate→选票过半Leader\text{Candidate} \xrightarrow{\text{选票过半}} \text{Leader}

2. 任期与选举

每届指挥家都有自己独一无二的“任期拍数”:第一届拍一下,第二届拍两下。乐手们只要听拍数,就知道现在该听谁的。Raft的任期(Term)正是这样的拍数——一个全局单调递增的整数,每次选举失败或成功都会加一,用来标记“这是第几届领导”。

选举是怎么打起来的?Leader会周期性向所有Follower发送心跳(AppendEntries空请求),像指挥家时不时敲一下指挥棒。只要Follower在随机设定的选举超时(通常150~300毫秒)内收到心跳,就继续保持Follower。可一旦超时仍未收到心跳,Follower会断定“指挥家可能倒下了”,于是任期加一,变成Candidate开始拉票。

拉票规则很直白:Candidate先投自己一票,再向所有节点发送RequestVote RPC。每个节点在同一任期内只投一票。票数率先过半的候选人直接转正为Leader,并立即开始发送新任期的心跳,阻止新一轮选举。这个“过半”条件,正是分布式系统的多数派(Quorum)思想——只要有超过一半的节点认可,就认为整个系统做了决定。

任期像指挥家的“当值拍数”一样,每次换人都要递增。随机超时和先到先得的设计,确保了选举不会陷入“两人对峙”的僵局——这正是Raft在实战中稳定可靠的关键细节。

votes=∑i=1N1[vi=c]>N2\text{votes} = \sum_{i=1}^{N} \mathbb{1}[v_i = c] > \frac{N}{2}
这一公式是Raft选举铁律:候选人必须拿到过半票数才能上台。过半既保证了唯一Leader,也保证了“多数派之间必有交集”,这是后续安全性的数学根源。
  1. 投票指示函数\\( \mathbb{1}[v_i = c] \\) 表示第 i 个节点是否投票给候选人 c,投了记 1,没投记 0。
    1[vi=c]={1,vi=c0,vi≠c\mathbb{1}[v_i = c] = \begin{cases} 1, & v_i = c \\ 0, & v_i \ne c \end{cases}
  2. 求和统计对全部 N 个节点的投票结果求和,得到候选人 c 获得的总票数。
    votes=∑i=1N1[vi=c]\text{votes} = \sum_{i=1}^{N} \mathbb{1}[v_i = c]
  3. 多数判定票数必须严格大于 N/2。N=5时至少3票,N=3时至少2票。
    votes>N2⇒c 当选 Leader\text{votes} > \frac{N}{2} \Rightarrow \text{c 当选 Leader}

3. 日志结构

翻开合唱团的乐谱,每一页都标着页码和拍号。Raft的日志也像一本乐谱,由许多“日志条目”组成,每个条目都带着三个关键信息:自己的索引号(页码)、所在任期(拍号)、以及要执行的命令(音符)。日志条目一旦被Leader创建,就会按顺序追加到日志末尾。

日志索引(log index)从1开始连续递增,代表“这是第几条乐谱”。任期编号则记录了这条日志是哪一任Leader创建的,用来区分新旧。命令(command)才是真正的业务操作,比如“把user的余额设为100”,它会在日志提交后被应用到每个节点的状态机中。

正因为每个节点都保存着这样一串日志,且按照完全相同的顺序执行命令,各节点的状态机才能保持一致。日志必须持久化存储——哪怕节点重启,也要重新读回本地日志继续排练,绝不能因为断电而丢失已经写下的谱子。

一本乐谱最重要的就是“页码不乱、拍号清楚、音符准确”。Raft的日志条目把这三件事固化成数据结构,使得任何节点拿出日志都能对齐位置、分清新旧、统一执行。

entryi=(termi, indexi, cmdi)\mathrm{entry}_i = (\mathrm{term}_i,\ \mathrm{index}_i,\ \mathrm{cmd}_i)
日志条目是Raft复制的“最小音符”。任期、索引、命令三元素共同决定了一条日志的合法性与执行方式。
  1. 索引定位\\( \mathrm{index}_i \\) 是条目在日志中的位置,从 1 开始连续编号,不可跳跃。
    indexi=i(i=1,2,3,… )\mathrm{index}_i = i \quad (i = 1, 2, 3, \dots)
  2. 任期标记\\( \mathrm{term}_i \\) 记录了创建该条目时的Leader任期,用于跨节点比较新旧。
    termi∈{1,2,3,… }\mathrm{term}_i \in \{1, 2, 3, \dots\}
  3. 业务命令\\( \mathrm{cmd}_i \\) 是客户端写入的具体操作,最终被送入状态机执行。
    cmdi="set key = value"\mathrm{cmd}_i = \text{"set key = value"}

4. 复制流程

排练开始了。指挥家把新乐谱第5页发给每位乐手,乐手们翻到第4页确认无误后,才把第5页夹进谱夹。Raft的日志复制也是这个流程:Leader通过AppendEntries RPC把新日志条目发给Follower,同时附上“前一条日志的索引和任期”,要求Follower先做一致性检查。

Follower收到AppendEntries后,先检查自己的日志在prevLogIndex位置是否存在、且任期与prevLogTerm一致。这就相当于乐手先翻到指定页码确认内容,再决定要不要夹进新的一页。如果检查通过,Follower把新条目追加到日志末尾,返回成功;不通过则直接拒绝,让Leader知道“你的谱子跟我的对不上”。

Leader维护着每个Follower的nextIndex(下一个要发送的日志索引)。一旦发现Follower拒绝了某次AppendEntries,Leader就把该Follower的nextIndex减一,回退到更早的位置重新发送,直到找到双方一致的公共前缀,再一次性补齐后面的日志。当一条日志被复制到多数节点,Leader就把它标记为已提交(commit),并推进commitIndex,通知所有节点应用这条命令。

整个复制过程就像师生对谱:每一页都要先确认前一页无误,才敢翻下一页。这种“保守对齐”虽然看似笨拙,却是Raft在乱序网络中依然能保持日志一致的定海神针。

log[prevIndex].term=prevTerm⇒追加成功\mathrm{log}[\mathrm{prevIndex}].\mathrm{term} = \mathrm{prevTerm} \Rightarrow \text{追加成功}
一致性预检查是复制流程的灵魂:先对齐前一条,再追加新一条。这个“步步为营”的策略保证了日志永远不会乱序拼接。
  1. 前序定位Leader 在 RPC 中携带 \\( \mathrm{prevIndex} \\) 和 \\( \mathrm{prevTerm} \\),告诉 Follower“新条目应该接在哪个音符后面”。
    prevLogIndex=新条目前一条的索引\text{prevLogIndex} = \text{新条目前一条的索引}
  2. 任期核对Follower 检查自己日志中该索引处条目的任期是否等于 \\( \mathrm{prevTerm} \\)。
    log[prevIndex].term=?prevTerm\mathrm{log}[\mathrm{prevIndex}].\mathrm{term} \stackrel{?}{=} \mathrm{prevTerm}
  3. 追加与返回任期一致则删除冲突条目并追加新条目,返回成功;否则返回失败让 Leader 回退重试。
    一致⇒追加;不一致⇒拒绝\text{一致} \Rightarrow \text{追加}; \quad \text{不一致} \Rightarrow \text{拒绝}

5. 日志匹配

合唱团里有个绝妙的默契:只要两位乐手在第10页看到的音符完全一致,那么他们前9页一定也完全一致。Raft把这种“前缀一致性”上升为一条铁律,称为日志匹配特性(Log Matching Property)。它的成立不靠猜测,而是靠复制流程中的强制检查一步步推导出来的。

为什么这个性质必然成立?关键在于每一条新日志都必须通过前一条的“索引+任期”检查才能被追加。如果两个节点在索引 i 处的条目相同,说明该条目是通过同一次AppendEntries把前一条也一起对齐后写入的;于是可以往前递推,索引 i-1 处必然也相同,再推到 i-2,直到日志开头。这就是数学归纳法的力量。

日志匹配特性带来了一个杀手级应用:Leader只需找到与Follower第一个匹配的索引,就能确定双方公共前缀,直接删除Follower中所有冲突的后续条目,补齐缺失部分。它让日志对账从“逐个比较所有条目”变成“从后往前快速定位”,极大降低了网络开销。

这段推理如同多米诺骨牌:推倒第一块(找到匹配的索引),后面的所有骨牌(更早的日志)都会依次倒下。日志匹配特性让Raft在日志修复时不必“从头对到底”,成为一个既有数学美感又极具工程效率的设计。

log[i]=log′[i]  ⇒  ∀j<i, log[j]=log′[j]\mathrm{log}[i] = \mathrm{log}'[i] \;\Rightarrow\; \forall j < i,\ \mathrm{log}[j] = \mathrm{log}'[j]
日志匹配是Raft最优雅的数学结论:一处相同,处处相同。它把“保证全部日志一致”简化为“保证某个安接点一致”。
  1. 前提假设假设两个节点的日志在索引 i 处条目完全相等——包括任期和命令。
    log[i]=log′[i]\mathrm{log}[i] = \mathrm{log}'[i]
  2. 递推论证由于任何条目的写入都要求前一条匹配,所以索引 i-1 处也必然相等。
    log[i]=log′[i]⇒log[i−1]=log′[i−1]\mathrm{log}[i] = \mathrm{log}'[i] \Rightarrow \mathrm{log}[i-1] = \mathrm{log}'[i-1]
  3. 归纳延伸反复使用该推理,直至索引 1,得到所有前序条目都相同。
    ∀j≤i, log[j]=log′[j]\forall j \le i,\ \mathrm{log}[j] = \mathrm{log}'[j]

6. 安全性规则

有了选举、复制和匹配,Raft还差最后一道防线——安全性规则。就像合唱团规定“只有熟练所有旧曲目的乐手才能当指挥”,Raft要求投票者只把票投给日志“足够新”的候选人。这保证了新Leader一定拥有所有已提交的日志条目,绝不带着“残缺的乐谱”上台。

选举限制的具体规则是:候选人必须拥有比投票者更新的日志才能获得选票。日志谁更新?先比较最后一个条目的任期,任期大的更新;任期相同则比较索引,索引大的更新。这条规则从源头杜绝了“日志落后的节点当选Leader”的可能,也因此保证新Leader一定包含所有已提交的日志。

此外还有提交限制:Leader不能通过统计旧任期日志的数量来直接提交旧日志,只能通过提交当前任期的新日志来“顺带”推进旧日志的提交。这个微妙设计防止了一个经典陷阱——旧Leader带着旧日志复活,在分区期间提交了多数但最终被新Leader覆盖。这两条规则合在一起,构成了Raft的安全性基石:已提交的日志永远不可能丢失。

这就像指挥家竞选时的“资历审查”:乐手们只认那个练得最多、谱子最全的人。看似苛刻的条件,恰恰是保证全部乐手最终能回到同一节奏的安全锁。

(termclast, indexclast)≥(termvlast, indexvlast)⇒voter 投给 candidate(\mathrm{term}_c^{\mathrm{last}},\ \mathrm{index}_c^{\mathrm{last}}) \ge (\mathrm{term}_v^{\mathrm{last}},\ \mathrm{index}_v^{\mathrm{last}}) \Rightarrow \text{voter 投给 candidate}
选举限制 + 提交限制双剑合璧:前者保证新Leader不缺课,后者防止旧Leader乱补课。已提交的乐谱永远生效,是安全性的最终承诺。
  1. 定义新旧\\( \mathrm{term}^{\mathrm{last}} \\) 和 \\( \mathrm{index}^{\mathrm{last}} \\) 分别表示节点日志最后一条的任期与索引。
    loglast=(termlast, indexlast)\mathrm{log}_{\mathrm{last}} = (\mathrm{term}^{\mathrm{last}},\ \mathrm{index}^{\mathrm{last}})
  2. 先比任期候选人的最后任期大于投票者,即认为候选人日志更新;相等再看索引。
    termclast>termvlast⇒候选者更新\mathrm{term}_c^{\mathrm{last}} > \mathrm{term}_v^{\mathrm{last}} \Rightarrow \text{候选者更新}
  3. 再比索引任期相等时,索引大的日志更长,视为更新。
    termclast=termvlast∧indexclast>indexvlast⇒候选者更新\mathrm{term}_c^{\mathrm{last}} = \mathrm{term}_v^{\mathrm{last}} \wedge \mathrm{index}_c^{\mathrm{last}} > \mathrm{index}_v^{\mathrm{last}} \Rightarrow \text{候选者更新}
  4. 投票决策只有满足上述比较结果时,投票者才会投出宝贵一票。
    候选者日志更旧⇒拒绝投票\text{候选者日志更旧} \Rightarrow \text{拒绝投票}

7. 对比Paxos

在Raft诞生前,Paxos统治着共识算法领域。但Paxos像一份由数学教授写给小众同行的论文级谱面——概念高度抽象,把单轮共识拆成Prepare、Accept、Learn等多个协议,难度极大,以至于连Lamport自己都承认“大多数人第一遍都看不懂”。工程界迫切需要一套“人人都能照做”的指挥手册。

Raft的战略就是“分而治之”:把共识问题拆成领导者选举、日志复制、安全性三个相对独立的子问题。每个子问题都有直观的日常类比(指挥家、乐谱、规矩),整体复杂度大幅下降。这种“化整为零”正是Raft的核心理念:同样是解决共识,Raft选择了一条让工程师愿意读、读得懂、也改得动的路。

从消息开销上看,Paxos正常运行时需要多轮RPC才能完成一次共识;Raft的正常路径只要一轮AppendEntries广播,加上多数派确认即可提交。虽然极端场景下Paxos也未必慢多少,但Raft把“常用路径”做到了最简单,用工程上的绝大多数情况换取可维护性——这笔账在工业界算得明明白白。

如果说Paxos是一本让人望而却步的“天书”,Raft就是那本装订清晰、循序渐进的操作指南。它牺牲了一点点理论优雅,换来了十倍的可理解性——这也是它在工程界被广泛验证、快速胜出的根本原因。

MRaft 一轮复制=2×(N−1)M_{\text{Raft 一轮复制}} = 2 \times (N - 1)
一次日志复制只需Leader向N-1个Follower各发一条RPC,并等待对应响应。极简的消息模式,正是Raft大规模落地的底气。
  1. 发送广播Leader 向除自己外的 N-1 个 Follower 发送 AppendEntries RPC。
    发送消息数=N−1\text{发送消息数} = N - 1
  2. 接收响应每个收到请求的 Follower 返回一个确认响应,Leader 共接收 N-1 条响应。
    响应消息数=N−1\text{响应消息数} = N - 1
  3. 合计开销一轮完整复制消耗的消息总数为两者之和。
    M=(N−1)+(N−1)=2(N−1)M = (N - 1) + (N - 1) = 2(N - 1)

8. 工业应用

当合唱团的经验足够成熟,就会被请进真正的音乐厅演出。Raft同样在工业界找到了自己的舞台:etcd和Consul就是最闪亮的两位“独奏家”。etcd是Kubernetes背后的数据中枢,把集群配置、服务发现信息以键值对形式存储在Raft日志中;Consul则利用Raft实现跨数据中心的服务注册与健康检查,保证“无论哪个节点宕机,服务目录依旧完整”。

为什么要“多数派”?因为它天然赋予了系统容错能力。一个5节点的Raft集群,只要仍有3个节点存活,就能正常选举Leader并提交日志。也就是说,最多允许2台节点同时宕机而系统不停止服务。这个“过半即活”的特性被广泛应用于生产环境的容灾设计:3节点容忍1故障,5节点容忍2故障,以此类推。

工程实践进一步丰富了Raft的细节:Leader需要定期持久化当前任期和日志,避免重启后产生混淆;选举超时要加入随机抖动,防止多个Candidate同时分裂选票;网络分区时,少数派一侧会不断选举失败,恢复后自动回滚日志与多数派对齐。这些细节让Raft从“纸面正确”走向“生产可用”。

从合唱团排练到音乐厅演出,Raft用工业级实践证明了自己不只是“好看的算法”——在etcd的键值存储、Consul的服务目录、各类分布式数据库的日志层中,它都是那根最可靠的指挥棒,让成千上万台服务器始终演奏同一首曲目。

最大容忍宕机数=⌈N2⌉−1\text{最大容忍宕机数} = \left\lceil \frac{N}{2} \right\rceil - 1
该公式直接回答了一个经典容灾问题:N台节点能扛住几台故障?答案不是N-1,而是“N的半数向上取整再减一”。
  1. 半数取整先计算 \\( \lceil N/2 \rceil \\),即 N 除以 2 后向上取整,得到多数派所需最少节点数。
    Quorum=⌈N2⌉\text{Quorum} = \left\lceil \frac{N}{2} \right\rceil
  2. 减去自身多数派以外的节点允许离线,数量为 Quorum 减 1。
    可宕机数=Quorum−1\text{可宕机数} = \text{Quorum} - 1
  3. 代入实例N=5 时,Quorum = 3,最多容忍 2 台宕机;N=3 时,最多容忍 1 台。
    N=5⇒2;N=3⇒1N=5 \Rightarrow 2;\quad N=3 \Rightarrow 1

AI 应用场景

etcd与Kubernetes的配置中枢

阶段:系统架构与高可用设计

解决的问题:Kubernetes集群需要所有节点对“当前状态”达成一致。若每台机器各自为政,调度器、控制器和节点之间立刻陷入混乱。Raft通过日志复制让etcd成为唯一可信的配置真相源——所有写请求经Leader落盘到多数节点后提交,系统永远对外呈现一致状态。

具体案例:你在Kubernetes中创建Deployment,API Server把资源对象写入etcd。Raft负责在etcd集群内部复制这条写入日志,3节点etcd中2个确认即返回成功。即便1台etcd节点宕机,集群仍能继续读写,Kubernetes控制面因此保持全部功能可用。

Consul服务发现与容灾切换

阶段:服务网格与控制面一致性

解决的问题:微服务架构中,服务注册表一旦不一致,调用方可能把流量打到已宕机的实例上。Consul利用Raft日志复制,在服务实例注册、注销时同步到多数节点,确保所有数据中心(Data Center)的查询都能看到同一个稳定视图。

具体案例:某金融系统用Consul管理3个数据中心的2000个微服务实例。当某实例宕机,健康检查触发注销操作,Raft把注销日志复制到多数节点后才响应客户端,从而避免“部分节点以为服务还活着”的脏读问题。

Python 演示

python59 行
class LogEntry:    """日志条目:相当于乐谱上的一个音符"""    def __init__(self, index, term, command):        self.index = index        self.term = term        self.command = command  class RaftNode:    """简化版 Raft 节点:只关注日志复制与多数提交"""     def __init__(self, node_id, peers):        self.node_id = node_id        self.peers = peers        self.log_list = []        self.term = 0        self.commit_index = 0        self.matched_index_map = {}     def become_leader(self, term):        self.term = term        for peer in self.peers:            self.matched_index_map[peer] = 0     def append(self, command):        entry = LogEntry(len(self.log_list) + 1, self.term, command)        self.log_list.append(entry)        return entry     def on_append_response(self, peer, acked_index):        if acked_index > self.matched_index_map.get(peer, 0):            self.matched_index_map[peer] = acked_index        self.maybe_commit()     def maybe_commit(self):        count = 1        for peer, idx in self.matched_index_map.items():            if idx >= len(self.log_list):                count += 1        if count > len(self.peers) // 2:            self.commit_index = len(self.log_list)  def simulate():    leader = RaftNode(1, [2, 3])    leader.become_leader(1)    leader.append("set user=alice")     leader.on_append_response(2, 1)    print("Follower2 确认,票数 2/3,尚未构成多数")    print("commit_index =", leader.commit_index)     leader.on_append_response(3, 1)    print("Follower3 确认,3 节点中 2 个确认,已构成多数")    print("commit_index =", leader.commit_index)  if __name__ == "__main__":    simulate()
目的:用约60行Python模拟Raft日志复制的核心逻辑——Leader追加日志、Follower确认、多数派判定提交。
  1. self.matched_index_map = {}记录每个Follower已确认的最新日志索引,相当于乐谱对账表。
  2. def become_leader(self, term):新Leader上台时初始化任期,并把所有Follower的确认进度清零。
  3. entry = LogEntry(len(self.log_list) + 1, self.term, command)每个新条目都带着索引和任期,缺一不可。
  4. if acked_index > self.matched_index_map.get(peer, 0):Follower确认更高的索引时才更新对账表,防止旧确认覆盖新进度。
  5. count = 1多数统计从Leader自己开始,因为Leader本就持有这条日志。
  6. for peer, idx in self.matched_index_map.items():遍历所有Follower的确认情况。
  7. if count > len(self.peers) // 2:确认数严格过半,才推进commit_index。
  8. leader.append("set user=alice")模拟一次客户端写入操作,日志中增加一条命令。
  9. leader.on_append_response(2, 1)模拟Follower 2确认收到了索引1的日志条目。

常见误区

误区“Raft要等所有Follower都确认收到日志,才能算提交成功。不然没收到日志的节点不就丢数据了吗?”

正解Raft只需要多数派确认,标准是“过半即提交”。少数落后节点会在恢复后通过Leader的日志修复机制补齐缺失条目。比如3节点集群,2个Follower确认就能提交,第三个节点稍后追平即可。追求全量确认不仅慢,而且违背了分布式系统“部分节点故障不影响整体”的设计初衷。

为何常见:人们习惯用单机思维理解数据安全——总觉得“我自己的数据必须保存好”。但在分布式系统中,安全由多数派冗余保证,而不是单点完整性。

误区“既然已经选了Leader出来,只要它不宕机,就一直由它当家做主,不会变。”

正解Raft的Leader任期不是终身制。一旦Leader所在节点发生网络分区、长时间失控,其余节点的Follower会在选举超时后发起新一轮选举并产生新Leader。旧Leader恢复后即使仍认为自己是指挥家,也会因为任期落后而主动退位成Follower。领导地位永远属于“当前任期最高的多数派认可者”。

为何常见:人习惯性地认为“选出来就稳定了”,却没意识到网络环境天然存在抖动和分区。Raft恰恰要用不断更新的任期来矫正这类“权威幻觉”。

误区“所有Follower设置一样的选举超时,大家同步发起选举,效率更高。”

正解恰恰相反。如果所有Follower在相同时间点超时,它们会同时变成Candidate,票数被分票,谁也拿不到多数,最终进入反复选举的僵局。Raft引入随机化选举超时,让每个节点的超时时间略有差异,最先超时的Candidate有很大概率先发起拉票并胜出,避免撕裂选票。

为何常见:直觉上“同步”听起来高效,但分布式系统最怕的就是“一群节点同时行动”。随机化不是不严谨,而是防止共振的成熟工程智慧。

误区“上一任Leader已经把某条日志复制到了多数节点,那新Leader一上台看到这条日志已经过半确认,当然可以立刻把它标记为已提交。”

正解Raft明确规定:Leader不能直接通过“数旧任期日志的副本数”来提交该日志。它只能等到自己任期内产生了新日志,再把旧日志随新日志一并“顺手”提交。因为旧日志虽然可能过半,但无法确定是否会被更新的Leader覆盖;只有当前任期日志提交了,才能借提交推进机制确认旧日志安全。

为何常见:人们容易把“多数派存在”直接等同于“已提交”。Raft的微妙之处在于,旧任期的多数派并不可靠,必须用当前任期的新日志作为“安全引信”去点燃旧日志的提交。

面试题

Q1. Raft集群中的每个节点可能扮演哪些角色?

单选题 · easy

  • A.仅 Leader 与 Follower
  • B.Leader、Follower、Candidate← 正确答案
  • C.主节点、从节点、备份节点
  • D.生产、消费、协调节点

答案:B。Raft只有三种角色:Leader、Follower、Candidate。C的“备份”和D的“生产消费”都是干扰项,A缺少选举所需的Candidate,无法描述选举状态机。

Q2. Follower在选举超时后首先会变成什么?

单选题 · easy

  • A.直接变成 Leader
  • B.变成 Candidate 发起选举← 正确答案
  • C.继续保持 Follower
  • D.向其他节点请求提交

答案:B。Follower超时收不到心跳,会自增任期并转为Candidate,然后先投自己一票再请求他人投票。它不会直接变成Leader,必须经过选举并获得多数票。

Q3. Raft中候选人当选Leader的最基本条件是什么?

单选题 · mid

  • A.获得超过半数的节点投票← 正确答案
  • B.获得所有节点的投票
  • C.日志长度必须最长
  • D.任期编号必须最大

答案:A。选举只要求“过半选票”,即Quorum。日志最长和任期最大都不是直接条件,而是安全性规则约束候选人日志足够新,但最终获胜仍需多数票。

Q4. Leader向Follower发送日志复制请求使用的是哪个RPC?

单选题 · mid

  • A.RequestVote RPC
  • B.AppendEntries RPC← 正确答案
  • C.Heartbeat RPC
  • D.CommitEntry RPC

答案:B。AppendEntries RPC同时承担心跳和日志复制的双重职责。RequestVote用于选举投票,没有CommitEntry这种RPC,心跳只是AppendEntries的空条目形式。

Q5. 日志匹配特性最核心的保证是什么?

单选题 · mid

  • A.日志永远不会被删除
  • B.相同索引处的日志相同则前序全部相同← 正确答案
  • C.所有节点的日志长度始终一致
  • D.日志可以按照任何顺序执行

答案:B。日志匹配特性指若两个日志在某索引处条目相等,则所有更早索引的条目也相等。这是数学归纳的结果,也是Leader安全覆盖Follower冲突日志的前提。

Q6. 关于Raft的安全性规则,下列说法正确的有?

多选题 · mid

  • A.候选人的日志不能比投票者旧← 正确答案
  • B.新Leader一定包含所有已提交日志← 正确答案
  • C.Leader可以直接提交旧任期日志
  • D.已提交的日志永远不会丢失← 正确答案

答案:ABD。选举限制要求候选人日志足够新,保证新Leader包含全部已提交日志;已提交日志的持久性由安全性规则守护。C错误——Leader不能直接提交旧任期日志,必须先提交当前任期日志来间接推进。

Q7. 与Paxos相比,Raft最核心的设计理念是什么?

单选题 · mid

  • A.更高的吞吐性能
  • B.拆分子问题以提升可理解性← 正确答案
  • C.用更少的节点达成共识
  • D.完全不需要选举过程

答案:B。Raft把共识拆成选举、复制、安全三个子问题,大幅降低理解门槛。性能并非Raft的核心卖点,它和Paxos解决同一类问题、都需要选举,可理解性才是Raft胜出的关键。

Q8. 在一个5节点etcd集群中,下列哪些说法正确?

多选题 · hard

  • A.最多容忍2台节点同时宕机← 正确答案
  • B.最多容忍3台节点同时宕机
  • C.提交日志至少需要3个节点确认← 正确答案
  • D.提交日志至少需要2个节点确认

答案:AC。5节点集群多数派为3,因此至少3个节点确认才能提交,最多容忍2台宕机。选项B和D分别混淆了容忍度和确认数的计算。

Q9. 旧Leader恢复后与当前Leader共存时会发生什么?

单选题 · hard

  • A.继续以Leader身份发送指令
  • B.因任期落后而退位为Follower← 正确答案
  • C.与新Leader共同组成双主
  • D.自动成为Candidate再次竞选

答案:B。Raft通过任期区分新旧权威。旧Leader的任期小于当前任期,其AppendEntries会被Follower拒绝;收到更高任期的请求后,旧Leader也会发现自己任期落后,退位为Follower并同步日志。

Q10. Follower收到AppendEntries后发现prevLogIndex位置没有对应条目时,应该怎么做?

单选题 · hard

  • A.直接接受新日志并追加
  • B.返回成功让Leader继续发送
  • C.拒绝该请求并让Leader回退← 正确答案
  • D.立刻发起新一轮选举

答案:C。一致性预检查不通过时,Follower返回失败,Leader据此将该节点的nextIndex回退并重试,直到找到共同的日志前缀。直接接受会导致日志乱序,发起选举也不符合规则。