想象一个山脚下的村庄,全村人共同放牧,一年到头要在一本《收成账本》上记录“谁添了几只羊、谁卖了几袋粮”。账本只有一本,谁记?怎么保证没人篡改?有人提议每人家里都抄一份副本,可副本多了又会对不上号。Raft 就是一套村规:平时由大家选出的村长统一记账,其他人跟着抄;村长失联了就重新选举;任何一笔记录必须超过半数村民确认才算数。这套村规把“选村长”和“记日志”讲得清清楚楚,让分布式系统里的多台机器像村民一样有序协作。读完本文,你会彻底理解 Raft 为什么既能保证一致性,又简单到可以亲手实现。
掌握 Raft 是理解 etcd、Kubernetes、分布式数据库的基石。面试高频、工程常用,学会它等于拿到了分布式一致性的“通用钥匙”。
可视化演示
演示 Raft 共识算法:任期驱动 Leader 选举,多数派投票确定领导者,日志复制保证一致性,Leader Completeness 与日志匹配不变量保障安全性。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Leader 选举 候选者请求投票,多数派胜出 Term 递增驱动 每任期最多一个 Leader,多数派选主
Raft基本概念
山脚下的村庄需要一本不会出错的《收成账本》。过去试过让村长一个人记——账本烧了就全完了;又试过让所有村民互相核对——吵成一锅粥也没结果。最后大家立下村规:选出一位村长统一记账,其余村民负责抄录和确认。这套村规要解决的核心问题,就是分布式系统里的“共识”:一堆机器如何对同一份数据达成一致。
在 Raft 的世界里,村民就是服务器(节点),账本就是日志(log),村长是领导者(Leader),普通村民是跟随者(Follower),而候选人(Candidate)是选举时暂时举手的村民。Raft 把复杂的一致性拆成三个子问题:领导者选举(谁当村长)、日志复制(怎么记账)、安全性(账本如何永不损坏)。每个服务器任一时刻处于这三种角色之一,角色之间可以转换。
既然要以多数人的意见为准,就需要一个明确的门槛:多少票才算通过?答案是“超过一半”的最小整数。这就是多数派阈值,也是整份村规的基石。
就像 3 人的村庄需要 2 人点头、5 人的村庄需要 3 人点头。因为只需要多数派,即使有人出门放羊不在家,账本也能继续往下记,这就是 Raft 在可用性与安全性之间找到的平衡点。
- 节点总数 nn 是集群中服务器的数量,例如 5 台机器组成集群。
- 向下取整$\lfloor n/2 \rfloor$ 表示 n 除以 2 后向下取整,4 的一半取 2,5 的一半也取 2。
- 加一得到门槛在取整结果上加 1,得到“严格超过一半”的最小票数,3 台需 2 票,5 台需 3 票。
领导者选举
村长每天早上敲锣三声宣告“我还在”。如果某一天锣声停了,村民们各怀心思地等了一炷香,还是没动静,就会有人站出来喊:“我来当村长!”其他人听见后开始投票。这一炷香,就是 Raft 里的选举超时(election timeout)。
选举流程分三步。第一步,触发:Follower 在选举超时内没收到 Leader 的任何消息,就推测 Leader 已失联,将任期 term 加 1,转为 Candidate。第二步,自荐:Candidate 先投自己一票,然后并行向所有其他节点广播 RequestVote RPC(拉票喊话),请求大家投票。第三步,裁定:如果获得超过半数的选票,就当选为新的 Leader;如果收到了更高任期 Leader 的消息,就乖乖退回 Follower;如果超时了票数依然不够,就在下一个任期重新发起选举。
投票规则讲究“每届一票”:每个任期内,每台服务器最多投出一张票,先到先得。但并非谁喊得响就投给谁——投票者只会支持“日志至少和自己一样新”的候选人。所谓的“新”,不是看日志条数多少,而是比较最后一条日志的任期与索引:
这条选举限制极具深意:新村长账本至少和大多数村民一样全,才能保证不会在上任后把已经定案的旧账目“抹掉”。这正是 Raft 安全性的第一道保险。
- 比较最后日志任期候选人最后一条日志的任期 T_c 必须大于投票者的 T_v,说明它经历过更多届、知道更多事。
- 任期相等时再看索引如果两者最后日志任期相同,则比较最后一条日志的索引,索引大表示账本记到了更后面的行。
- 两个条件满足其一用“或”连接,构成完整的新旧判断标准。
心跳任期超时
村长当选后,必须每隔一小段时间敲一次锣,告诉村民“村长还在,大家都听我的”。这阵锣声就是 Raft 的心跳(heartbeat),由 Leader 周期性地向所有 Follower 发送。而村民心里那炷香——选举超时——就是“多久没听到锣声就认为村长没了”的忍耐极限。
心跳间隔(如 100ms)必须远小于选举超时(如 150ms 起),否则正常的网络抖动会让村民误以为村长失联,引发不必要的选举。更关键的是,选举超时绝不能全村统一,否则所有村民会在同一时刻烧完香、同时举手竞选,导致票数被分散成好几份,谁也凑不齐多数派——这就是选票分裂(split vote)。Raft 的解法简单而巧妙:每家香的长短随机。
任期(term)就是“第几届”。它是一个单调递增的整数,相当于分布式世界里的逻辑时钟。任何 RPC 消息都携带任期号:接收方发现消息里的任期更大,就立即更新自己的任期、并降为 Follower;Leader 发现自己任期落后于别人的回复,也会立刻退位。这就保证了“永远只认最新一届的村长”。
- 基础超时 T系统预设的基准超时,例如 150ms,这是最短的耐心极限。
- 随机增量在 0 到 T 之间均匀随机取一个值,让每台服务器节奏错开。
- 实际超时区间两者相加,每台服务器的超时落在 [T, 2T] 之间,同一个集群里几乎不会有两台同时超时。
日志复制机制
牲畜交易发生了:张三卖出 2 只羊。村长在自家账本上工工整整新起一行——第 10 行、第 3 届、内容“张三卖出 2 只羊”——然后向全村广播:“大家把这一行抄下来!”过半数的村民抄写完成并举手确认后,村长敲锣宣布:“这一行正式生效!”这就是日志复制的全过程。
每条日志条目(LogEntry)由三个要素构成:索引 index(行号,递增)、任期 term(哪一届记的)、命令 command(具体操作)。Leader 通过 AppendEntries RPC 把新条目推送给所有 Follower。消息里还带着前一条日志的索引和任期(prevLogIndex、prevLogTerm),用于让 Follower 检查自己的账本是否接得上——这就是接下来要讲的一致性检查。
一条日志何时才算“定案”?Raft 的规矩是:不要求全部村民抄完,只要复制到多数派就可以提交(commit)。提交后,这条命令就会被应用到状态机上(比如真正改掉羊群数量),并向客户端返回成功。
新 Leader 上任时,并不知道每台 Follower 抄到了哪一行,于是给每个 Follower 设一个 nextIndex(“我猜你抄到了第几行”),初始值是自己日志长度加 1。如果 AppendEntries 因前一条日志不匹配而被拒绝,Leader 就把 nextIndex 减一,再试,再减一再试——像查户口一样一路回溯,直到找到两边能对齐的那一行。这就是 nextIndex 回退。
- 集合计数统计集群 S 中满足条件的服务器个数。
- 成员条件服务器 s 的日志中已经包含了条目 e,即这户人家的账本上抄到了这一行。
- 与多数派阈值比较计数结果必须不小于 N 台服务器的一半加一,才允许提交。
日志一致性
各家各户的账本抄来抄去,难免有人抄串了行:张三的账本在第 7 行写着“卖出 2 只羊”,李四却写成了“卖出 3 只羊”。村规规定:以村长的账本为正本,谁的账对不上,就从第一个错行开始,把后面全部划掉,照着正本重新抄。Raft 将这个朴素原则上升为一条严谨的性质——日志匹配性质(Log Matching Property)。
日志匹配性质包含两层含义:第一,如果两条日志在相同索引处有相同的任期,那么这两条日志的内容一定完全相同;第二,如果两条日志在某个索引处相同,那么该索引之前的所有日志也完全相同。第二条由 AppendEntries 的一致性检查保证:Leader 发送新条目时,会携带前一条日志的 index 和 term,Follower 发现对不上就拒绝,于是分叉绝不可能悄无声息地产生。
当 Follower 的日志与 Leader 冲突时(同一索引、不同任期),Leader 通过回退 nextIndex 找到第一个匹配点,然后删除 Follower 从冲突点往后的所有条目,用自己日志覆盖。这里有一条红线:已经被提交的条目永远不会被删除。因为新 Leader 一定包含所有已提交条目(下一节的安全性保证),所以“以正本覆盖副本”永远不会伤及已定案的账目。
- 任意两条日志对日志中的任意两条记录 i、j 进行考察,不限于相邻条目。
- 索引与任期同时相等两个前提条件同时成立,即行号和届数都一样。
- 蕴含结论由前提推出结果——条目内容必然相等。
安全性保证
村规能有公信力,靠的是三条铁律:第一,同一届只认一位村长,绝不允许出现“两个村长同时发号施令”;第二,新村长手里的账本必须包含所有已生效的记录,不许“新官不认旧账”;第三,已经宣布生效的记录永远不许改动。Raft 把这三条铁律称为选举安全性、领导者完整性、状态机安全性。
- 选举安全:每个任期内最多只有一个 Leader 被选出。因为当选需要多数派票,而每个节点每届只有一票,不可能出现两个候选人都拿到多数。
- 领导者完整性:如果一条日志在任期 T 被提交,那么之后任何一个任期的 Leader 都包含这条日志。这是日志不被“抹掉”的核心保证。
- 状态机安全:服务器不会对同一个索引应用两条不同的命令。正是因为日志有序一致,状态机的执行结果才能完全确定。
这三条铁律看似理所当然,实则依赖一个巧妙的数学事实:任意两个多数派集合必有交集。旧一届提交日志时服用的多数派,与新一届选举时支持的多数派,至少重叠一个节点;而这个“跨届老人”一定认识那条已提交日志。既然新 Leader 是由它投票产生的,而投票资格要求“日志至少一样新”,新 Leader 自然也就继承了全部已提交条目。
就像两届村民投票,过半的人群里总有至少一个“跨届老人”,新旧账目由此传承。有了这条传递链,已定案的账目就真正成了铁案,任谁上台也翻不了。
- 两个多数派集合Q1、Q2 分别是两届决策中过半数的节点集合,各自大小不小于阈值。
- 交集非空两个集合的交集不可能为空——过半对过半,必定撞车。
- 传话人存在交集中的节点既是旧账的见证者,又投票给了新 Leader,于是已提交条目随它“过继”给新村长。
持久化与容错
村民会打盹、会出远门,但家里的账本要是被风吹走了,一切就前功尽弃。于是村规立了一条硬性要求:每个村民家里的账本、以及“本届投给了谁”的记录,都必须锁进柜子里(持久化到磁盘),醒来后先检查柜子再干活。Raft 中需要在响应任何 RPC 之前落盘的状态正是这三样:
有了这三样“保险柜凭证”,容错就有了章法。Follower 崩溃重启:从磁盘恢复三件套,继续当普通村民。Leader 崩溃:其他节点超时后发起新选举,系统很快恢复写入能力。网络分区:假设 5 个节点分成 3+2 两侧,3 节点一侧能凑够多数派、正常提交日志;2 节点一侧连新 Leader 都选不出来,无法提交任何新账目,旧 Leader 也会在收到更高任期的心跳后主动退位。分区恢复后,少数派被新 Leader 的日志覆盖,而已提交数据因为“领导者完整性”毫发无损——村里永远以多数派的正本为准。
- currentTerm 当前任期记录自己知道的最新届数,重启后若发现别人任期更大,立即让位。
- votedFor 投给了谁防止重启后在同一任期内重复投票,破坏“每届一票”原则。
- log 日志账本本身,包括已提交和未提交的条目,丢了就无法恢复一致性。
AI 应用场景
场景一:Kubernetes 的 etcd——训练集群控制面的“村庄账本”
阶段:分布式训练基础设施的元数据一致性阶段
解决的问题:管理 100 台 GPU 服务器进行大模型训练时,控制面必须知道“哪个节点在跑哪个任务、集群期望状态是什么”。传统单点数据库一旦宕机,整个调度系统就瘫痪;而 Raft 让多个控制面节点互为备份,少数节点故障不影响整体。
具体案例:训练一个推荐大模型需要 100 台 GPU 服务器协同,Kubernetes 把作业调度信息写入由 Raft 驱动的 etcd。3 个控制节点中挂掉 1 个,其余 2 个依然能选出 Leader 并继续调度,训练任务不中断。
场景二:TiKV 分布式数据库——存储层的“账本复制”
阶段:特征数据存储的副本一致性阶段
解决的问题:单机数据库容量和吞吐有上限,把数据分片到多台机器后,每份数据需要多个副本,且副本之间必须严格一致。Raft 负责每个分片(Region)的主副本选举与日志复制,保证任何副本写入都不乱序、不丢失。
具体案例:AI 模型的特征数据存储在 TiKV 中,每个 Region 有 3 个副本。某台存储节点宕机,该 Region 自动选出新 Leader,读写请求在毫秒级切换,线上模型服务毫无感知。
场景三:微服务配置中心——配置发布的“广播记账”
阶段:模型服务配置管理与热更新阶段
解决的问题:几十个模型服务实例的开关、参数需要“一改全改”,还要保证旧配置不残留、新配置不部分生效。基于 Raft 的配置中心把每一次变更当作一条日志,复制到多数派节点后提交,所有实例通过 Watch 机制感知并热加载。
具体案例:推荐系统上线新开关,运维把配置写入 etcd,Raft 把这条配置日志复制到 5 个节点中的 3 个即提交成功;所有服务实例随即监听到变更并平滑切换,全程无需重启。
Python 演示
"""简单Raft演示: 随机超时触发选举, 多数派获胜, 领导者复制日志"""import random class RaftNode: def __init__(self, node_id, total): self.node_id = node_id self.total = total self.role = "follower" # follower / candidate / leader self.term = 0 # 当前任期 self.voted_for = None # 本届投给了谁 self.log = [] # 日志条目列表 self.timeout = random.uniform(1.0, 2.0) self.remain = self.timeout self.election_count = 0 # 发起选举次数 def tick(self, delta=0.1): if self.role != "follower": return self.remain -= delta if self.remain <= 0: self.start_election() def start_election(self): self.role = "candidate" self.term += 1 self.voted_for = self.node_id self.election_count += 1 votes = 1 # 自己的一票 for peer in peers: if peer.node_id != self.node_id and peer.term <= self.term and peer.voted_for is None: peer.voted_for = self.node_id votes += 1 if votes >= self.total // 2 + 1: self.role = "leader" self.remain = float("inf") else: self.remain = self.timeout def replicate(self, command): if self.role != "leader": return None entry = (len(self.log) + 1, self.term, command) self.log.append(entry) ack = 1 # 领导者自己确认 for peer in peers: if peer.node_id != self.node_id and len(peer.log) >= len(self.log): ack += 1 committed = ack >= self.total // 2 + 1 return committed, entry, ack def main(): global peers random.seed(42) peers = [RaftNode(i, 5) for i in range(5)] for _ in range(100): for node in peers: node.tick() if any(node.role == "leader" for node in peers): break leader = next(n for n in peers if n.role == "leader") print("选举完成: term=%d, leader=%d, 发起选举次数=%d" % (leader.term, leader.node_id, leader.election_count)) for cmd in ("add_sheep", "sell_grain", "pay_tax"): ok, entry, ack = leader.replicate(cmd) print("日志 #%d term=%d 命令=%s -> 确认数=%d, 提交=%s" % (entry[0], entry[1], entry[2], ack, ok)) if __name__ == "__main__": main()
self.timeout = random.uniform(1.0, 2.0)每台服务器随机选举超时,避免同时起哄造成选票分裂self.remain -= delta村民心里那炷香一点点烧完if self.remain <= 0:香烧完还没听到村长锣声,进入选举self.term += 1竞选时给自己届数加一,宣告“新一届开始”self.voted_for = self.node_id候选人先投自己一票peer.voted_for = self.node_id向还未投票的村民拉票votes >= self.total // 2 + 1票数过半即当选村长entry = (len(self.log) + 1, self.term, command)日志条目三要素:行号、届数、操作ack = 1村长自己永远确认自己的日志len(peer.log) >= len(self.log)村民账本已抄到这一行才算一票确认ack >= self.total // 2 + 1确认数过半,条目提交生效
常见误区
误区Raft 为了保证一致性,日志条目必须等所有节点都复制成功才能提交吧?哪怕只有一台节点没确认都不能算数。
正解不需要全部节点。只要超过半数节点确认(例如 5 节点中 3 个)就可以提交并告知客户端成功。就像村里记账,多数人亲眼看到就行,不必等全村人集合。少数节点慢或宕机完全不影响提交,它们后续会通过日志追赶补齐。
为何常见:直觉上“越多越安全”,但 Raft 刻意用多数派而非全体,正是为了在可用性与安全性之间取舍——过半必然重叠保安全,少数离线不阻塞可用性。
误区Raft 选举肯定是所有人投票给日志条数最多的那个节点,因为信息最全最靠谱。
正解Raft 的投票规则是“每届一票、先到先得”,投票者只会把票投给“日志至少和自己一样新”的候选人。当选的不一定是全局日志最长,而是“足够新且先拉到票”的人。日志新旧比较的是最后一条日志的 term 和 index 组合,不是单纯比较条数。
为何常见:把“选举限制”误解成了“按日志长度排序选最优”。实际上 Raft 是“资格门槛 + 随机公平”,门槛保证安全,随机性保证公平。
误区term 是逻辑时钟,term 大的节点一定经历过更多届、知道更多日志,自然更有资格当村长。
正解term 和日志新旧是两个完全独立的维度。一个 term 很大的节点可能一直在反复竞选失败,日志很短;而一个 term 较小的老节点可能参与了所有日志提交。判断日志新旧要看“最后一条日志的任期 + 索引”,绝不能只看当前 term。
为何常见:把“时间顺序”与“数据完整性”混为一谈。任期是时钟,不是内容本身。
误区5 个节点分成 3+2 两个区,两边各选出一个 Leader,各自记账,数据从此分叉乱套。
正解分区后,只有 3 节点一侧凑得齐多数派、能正常提交新日志;2 节点一侧选不出获得多数票的 Leader,无法提交任何新条目,其旧 Leader 还会因收到更高任期的心跳而退位。恢复后少数派日志被新 Leader 覆盖,已提交数据不会丢。
为何常见:只看到“两个 Leader 都在发心跳”的表面,没意识到少数派 Leader 根本无法完成任何写入,“光杆村长”形同虚设。
误区学了 Raft 感觉它最大的作用就是选出 Leader,用 etcd 也不过是选个主节点而已。
正解选举只是手段,日志复制才是核心。Raft 的本质是让所有节点的日志有序一致,从而让每个节点上的状态机(数据库、配置、计数器)同步执行相同命令。选主只是为了保证同一时刻只有一个写入者,避免日志产生分叉。
为何常见:被“Leader 选举”这个名字带偏了注意力,忽略了投票限制、提交条件、日志冲突处理等真正决定一致性的部分。
面试题
Q1. 一个 5 节点 Raft 集群中,日志条目至少复制到几个节点才能提交?
- A.需要 1 个节点确认
- B.需要 2 个节点确认
- C.需要 3 个节点确认← 正确答案
- D.需要 5 个节点确认
答案:C。提交需要多数派确认,5 节点集群阈值是 ⌊5/2⌋+1=3。像村庄记账一样,过半数村民确认即可生效,不需要全村人都抄到。
Q2. Raft 中服务器共有多少种角色?
- A.仅有 2 种角色
- B.共有 3 种角色← 正确答案
- C.共有 4 种角色
- D.共有 5 种角色
答案:B。Leader、Follower、Candidate 三种。Candidate 是选举时的临时状态,当选后转为 Leader,落选或让位后退回 Follower。
Q3. Follower 在选举超时内未收到心跳会如何?
- A.继续安静等待心跳
- B.转为候选者发起选举← 正确答案
- C.自动升级成为领导者
- D.停止参与并退出集群
答案:B。超时未收到心跳就推测 Leader 失联,自增任期并转为 Candidate 发起选举。就像村民一炷香没听到锣声就要选新村长,而不是原地傻等。
Q4. Raft 将选举超时设为随机值,主要目的是什么?
- A.让所有节点的超时完全一致
- B.避免候选人同时竞选选票分裂← 正确答案
- C.使日志复制速度变得更快
- D.让任期编号可以单调递增
答案:B。随机超时让不同节点在不同时刻发起选举,避免同时竞选导致票数分散、无人过半。若超时固定,系统会周期性地发生“全村同时举手”的选票分裂。
Q5. 新 Leader 对每个 Follower 的 nextIndex 初始值是多少?
- A.从 0 开始逐个条目探测
- B.从 1 开始逐条日志探测
- C.自己最后日志的索引值
- D.自己最后日志索引加一← 正确答案
答案:D。新 Leader 不知道各 Follower 抄到哪一行,按最乐观的估计,从自己日志末尾的下一条开始下发;AppendEntries 被拒绝时再逐次回退,直到找到匹配点。
Q6. Follower 收到更低任期 Leader 的追加请求应如何处理?
- A.接受并立即复制日志
- B.拒绝该过期的请求← 正确答案
- C.把自己转为候选者
- D.更新任期并照常复制
答案:B。RPC 携带任期号,较低任期的消息属于“过期村长”发号施令,Follower 直接拒绝。任期是逻辑时钟,过期者没有权威。
Q7. 新 Leader 想提交前任任期遗留的、已复制到多数派的旧条目时,应该怎么做?
- A.直接将其标记为已提交
- B.先提交一条当前任期的日志← 正确答案
- C.请求所有节点进行二次确认
- D.等待该条目的任期重新出现
答案:B。直接按副本数判断旧任期条目是否提交是危险的,必须先把一条当前任期的日志提交成功,旧条目的提交状态才会随之固定。这就像新任村长必须先把本届第一笔账记到多数派手里,前任的账才算真正钉死。
Q8. Raft 中哪些状态在响应任何 RPC 之前必须持久化?
- A.currentTerm 当前任期← 正确答案
- B.votedFor 本届票投给了谁← 正确答案
- C.log 完整的日志列表← 正确答案
- D.commitIndex 已提交的索引
答案:ABC。currentTerm、votedFor、log 是崩溃恢复的“三件套”,必须先落盘再响应投票或追加请求。commitIndex 可以在重启后根据日志重新计算,不需要持久化。
Q9. 关于 Raft 日志提交的说法,哪些是正确的?
- A.复制到多数派即可提交← 正确答案
- B.已提交条目之后可能被覆盖
- C.后续领导者一定包含已提交条目← 正确答案
- D.提交动作只能由领导者完成← 正确答案
答案:ACD。提交由 Leader 主导、以多数派为条件;已提交条目因“领导者完整性”会被后续所有 Leader 继承,绝不会被覆盖(B 错误)。
Q10. 5 节点集群发生 3+2 网络分区时,哪些说法是正确的?
- A.三节点一侧能正常提交新日志← 正确答案
- B.两侧能同时提交不同的日志
- C.分区恢复后少数派日志被覆盖← 正确答案
- D.已提交日志在恢复后仍然存在← 正确答案
答案:ACD。只有凑齐多数派的 3 节点一侧能提交;2 节点一侧无法达到多数派,选不出 Leader,更不可能提交(B 错误)。恢复后少数派被新 Leader 覆盖,而已提交条目因安全性保证不会丢失。