想象你加入了一家叫“星辰科技”的 5 人创业公司。公司有个朴素的规矩:任何重大决策都必须写进《项目决策纪要》,并且开会时全员点头才算生效。团队里最懂业务的大飞被推举为“总负责人”,他每周把纪要草稿分发给小美、阿强他们,大家确认后归档;新决定不能与已归档的纪要矛盾。可有一天大飞突然失联了——既不开会也不回消息。等了三周,小美站出来说“我来暂代”,阿强不服,两人各自拉票。为了让公司不瘫痪,大家约定:谁拉到的支持过半、谁最了解最近的纪要,谁就上位。更妙的是,这套“会议纪要制度”经过工程化打磨,正是开源分布式协调组件 etcd 中实现的 Raft 共识算法:定期选举负责人、纪要分布式复制、多数派拍板、防止乱选的安全规则、团队扩招的平滑过渡、季度总结式的快照压缩。本文就用这条故事线,带你把 Raft 在 etcd 里的落地细节一次看透。
对后端工程师而言,掌握 etcd 的 Raft 实现,意味着你能真正看懂分布式系统中的“主从切换、数据一致性、故障恢复”到底如何发生——这是排查分布式疑难杂症和设计高可用架构的硬核功底。
目录
可视化演示
\u901a\u8fc7\u4e09\u9636\u6bb5\u4ea4\u4e92\u5c55\u793aetcd\u4e2dRaft\u5171\u8bc6\u6838\u5fc3\u673a\u5236\uff1a\u9886\u5bfc\u8005\u9009\u4e3e\u3001\u65e5\u5fd7\u590d\u5236\u4e0e\u5b89\u5168\u6027\u7ea6\u675f\u3002
本节配有交互动效(进入视口后自动加载,约 3KB)。
\u9886\u5bfc\u8005\u9009\u4e3e\u6d41\u7a0b \u5019\u9009\u8005\u53d1\u8d77 \u83b7\u53d6\u591a\u6570 term++ \u8d85\u65f6\u89e6\u53d1\u9009\u4e3e
Raft核心概念——三顶帽子与届数
星辰科技的白板上永远画着三顶帽子的图案:普通成员的黑色工牌、竞选者的橙色胸牌、总负责人的金色工牌。每天早上晨会,大家第一件事就是看白板——今天谁戴哪顶帽子。帽子不是领导任命的,而是由“事件”自动决定的:大飞如果连续三周既不开会也不回消息,白板上的金色工牌就会被擦掉,团队进入“重新物色负责人”的流程。这套“谁戴帽子、何时换帽子”的规则,就是 Raft 最底层的角色状态机。
在 etcd 里,这套帽子规则被实现成一个有限状态机。每个节点在任意时刻必居其一:Follower 是普通成员,只被动响应;Candidate 是正在竞选的活跃者;Leader 是当前总负责人。etcd 的 raft 包用 term 字段记录“现在是第几届决策期”,任何通信都带着这个届数。届数奇妙之处在于它永远只增不减:你收到一条带着更大届数的消息,说明对方经历过更新的世界,你必须承认它、立刻把帽子换成 Follower。
日志是这套系统运转的记账本。客户端发来一个写请求,Leader 不会马上执行,而是把它包装成一条“纪要条目”追加到自己的日志末尾,然后才同步出去。etcd 中日志条目本质上是三元组:
理解了“三顶帽子”和“一届一届递增的届数”,你再看 etcd 的日志条目就不会迷糊:所谓共识,就是让所有成员在“哪一页纪要、哪一届、什么内容”上达成完全一致。接下来我们看最刺激的部分——当大飞失联,团队是怎么选出新负责人的。
- 任期 t_i记录这条纪要写于第几届决策期,届数越大信息越新鲜。
- 索引 i纪要在日志账本中的唯一页码,从 0 开始连续编号、永不复用。
- 命令 c_i真正要执行的动作,例如“把超时阈值调成 100ms”或“写入键 happy=1”。
领导者选举——失联三周后
现在白板上的金色工牌已经蒙尘。规矩说:超过三周没收到总负责人的任何消息,任何一位普通成员都可以摘下工牌,把橙色候选人胸牌戴到自己身上,然后向全体成员喊话:“我申请当负责人,支持我的请举手!”星辰科技的规矩是——先喊话、后举手,谁的举手数超过总人数的一半,谁就立刻转正。
etcd 对应这个流程的术语叫 RequestVote。触发条件是 Follower 的选举超时——etcd 中默认约 1 秒,比心跳间隔(默认 100ms)大一个量级。超时时间不是固定值,而是每次随机生成,像大家按不同的“三周零一两天”来判断失联,这样就不会所有人同时举手。候选人的第一个动作是把自己的任期加 1(代表开启新一届决策期),然后投自己一票,再并行给所有节点发投票请求。
投票请求里最关键的是“我这个候选人的日志有多新”,因为这决定了它是否够格。获得严格过半的选票才算赢:
在 etcd 中,投票结果一旦达到 quorum,候选人立刻把自己标记为 Leader 并开始周期性广播心跳。新 leader 上任的第一件事不是庆祝,而是把“我还在”的周报发给所有人,避免有人再次误判失联。这就是为什么 etcd 故障切换后几秒内就能恢复服务——选举机制的快与稳,决定了整个集群的可用性天花板。
- 集群规模 n当前参与决策的成员总数。
- 向下取整奇数除以二得到小数时去掉小数部分。
- 加一保严格过半必须比一半多一个以上才算多数派。
日志复制——纪要的分发与签字
大飞转正后,第一件事就是补纪要。他把这段时间大家口头商定的 5 条决定整理成一份《第 4 届决策纪要》,打印 4 份分发给小美、阿强他们。分发不是扔过去就完事,而是要对方先核对:我给你的第 3 条纪要,你手上第 3 条是不是同一届定的?对上了,你才能把新纪要钉在文件夹最后。
Raft 的日志复制流程与之完全吻合。Leader 收到客户端写请求,先在本地日志末尾追加一条 (term, index, command),然后构造 AppendEntries 消息发给所有 Follower。消息里除了新条目,还带着两个关键字段:prevLogIndex 和 prevLogTerm——也就是“你上一页纪要的页码和届数”。Follower 收到后先做一次“一致性检查”:翻到自己日志的 prev 位置,看那里存的任期是否等于 prevTerm。相等才追加;不相等就拒绝,Leader 收到拒绝后把 nextIndex 往后退一格,重新携带更早的 prev 信息再试,直到双方对齐。
这个检查是 Raft 日志一致性的第一道闸门:
当新纪要钉进超过半数成员的文件夹,Leader 才在例会上宣布“第 5 条纪要正式生效”——这个生效点叫提交(commit)。星辰科技的文件夹越来越厚,总有一天需要归档总结,这就是后面要讲的快照压缩。
- 定位上一页 prevLeader 要求 Follower 从 prev+1 开始追加,prev 是它认为的“公共前缀”的末尾。
- 任期比对Follower 取出自己的 log[prev].term,与 Leader 声称的 t_prev 做相等判断。
- 回退机制不一致时 Leader 将 nextIndex 减一,用更早的 prev 重新发送,直到一致。
安全性约束——不是谁举手多就能当
小美第一次竞选时差点翻车:她举手数已经够了,但老陈举手反对,理由一针见血——“她这三周在国外出差,最新的 4 条纪要她根本没看过!让她当负责人,她都不知道公司现在做到哪一步。”于是章程补了一条铁律:候选人必须保证自己的纪要不比半数以上成员旧。这条铁律,就是 Raft 的选举限制。
Raft 的选举限制把“日志新旧”作为投票的门槛。Candidate 发 RequestVote 时会带上自己最后一条日志的任期 lastLogTerm 和索引 lastLogIndex。Follower 收到请求后,先比较自己的最后一条日志:如果自己的日志比候选人的还新,就拒绝投票。只有候选人的日志“不旧于自己”时,才考虑投票。这样确保胜出的 Leader 至少拥有所有已提交日志的副本,新 Leader 永远不会弄丢已确认的数据。
“更新”在 Raft 里有严格定义——先比任期,再比索引:
有了“纪要不旧”的硬门槛,加上提交限制(只有当前任期的日志能凭多数直接提交),Raft 才拿到它的招牌安全保证:已提交的纪要永远不会被后来的负责人推翻。这就像星辰科技的档案室——一旦归档,只能追加,不许涂改。
- 先比任期届数大的一方信息更新,这是第一判据。
- 同届比索引届数相同时,页码大的(日志更长)更新。
- 逻辑或两个判据满足任意一个即判“更新”。
成员变更——团队扩招不停摆
公司业务扩张,要招两个新人。麻烦在于:如果直接从 5 人制切到 7 人制,中间会有一段时间大家数不清“多数派到底是 3 还是 4”,万一这时候老负责人失联,选举可能选出两个 leader。章程给出的办法是“双轨过渡”:先宣布一条“扩编决议”,在决议生效期间,旧 5 人的多数和新 7 人的多数都要点头,任何决策才算数;等所有人都确认了这条决议,再彻底切到 7 人制。
Raft 的成员变更完全复用日志复制机制:配置变更(ConfChange)本身也是一条日志条目。etcd 的 raft 包收到 raftpb.ConfChangeAddNode/RemoveNode 后,把它放进当前任期的日志里走一遍复制流程,提交成功后才修改节点列表。论文里的严谨做法叫 joint consensus(联合共识):从 C_old 切换到 C_new 中间经过 C_old∩C_new 的过渡态,任何决策都必须同时拿到新旧两个配置各自的多数派同意。
联合共识的提交条件:
等扩编决议生效,白板上的人数从 5 变成 7,quorum 自动变成 4。整个过程中老成员不需要停机,新成员通过日志追赶自动补齐历史纪要——这就是 etcd 做动态扩缩容(etcdctl member add/remove)时底层发生的事。
- 旧配置多数原班人马中超过半数同意。
- 新配置多数扩编后的新班底中超过半数也同意。
- 且关系缺一个都不行,这是防止过渡期出现双 Leader 的核心。
快照压缩——季度总结归档
纪要从第 0 条堆到第 50000 条,新入职的小雅根本不可能逐条读一遍才能上岗。于是每季度末,行政把截至目前的全部决策浓缩成一份《季度状态摘要》:公司现在的成员名单、资金余额、项目进度就一张纸。摘要上写着“覆盖至第 48000 条纪要”,之前的原始纪要就可以送进仓库封存,甚至销毁。
etcd 中的同样机制叫快照(snapshot)。当 appliedIndex 与日志首条索引的差距超过阈值(etcd 由 --snapshot-count 控制,默认 10 万条),节点就把状态机当前状态序列化成一个快照,连同“最后包含的日志索引和任期”一起持久化,然后截断此前的日志。Follower 如果落后太多,Leader 不再逐条补日志,而是直接发送 InstallSnapshot 消息,Follower 收到后加载快照并跳到对应状态。
快照是一个自包含的数据包:
快照让 etcd 的日志“只增不减”变成了“增删有度”。存储占用被控制在合理范围,节点重启后也只需加载最新快照再回放少量日志,几秒钟就能回到服务状态。对于 AI 场景动辄海量配置下发的集群,没有快照,etcd 早被日志撑爆了。
- 状态机状态 S前 i_last 条日志全部应用后的最终结果,例如所有 key-value。
- 最后包含索引摘要覆盖到第几条纪要,用于日志截断后的对齐。
- 最后包含任期第 i_last 条纪要属于第几届,配合日志匹配特性校验。
etcd工程实现——从论文到工业级系统
星辰科技的纪要制度要真正工业化,光有白板和纸不够:纪要必须写进保险柜(WAL 预写日志)、每页纪要要有目录索引(MVCC 多版本)、查阅不能打断正在进行的记录(ReadIndex 线性读)。这三件套,正是 etcd 落地 Raft 时的工程三支柱。
etcd 的存储分四层:WAL(预写日志)保证任何已确认的日志在断电后不丢失;Snapshot 存储(boltdb)保存最新的状态机快照;内存中 raft 日志在提交后逐步截断。写入路径是:客户端请求 → Leader 追加日志 → 广播 → 多数确认 → 标记 committed → 通过 Ready 结构体交给应用层 → 应用进入 MVCC 存储并响应客户端。读出方面,etcd 用 ReadIndex 方案实现线性一致读:Leader 先确认自己仍是 Leader,记录 commitIndex,等到应用水位追上它才返回结果,避免读到旧数据。
整个写入流水线由三个水位刻度管理:
一句话总结 etcd 的工程哲学:WAL 保证“纪要进了保险柜才算数”,MVCC 让“查纪要不排队”,ReadIndex 让“读到的永远是最新已归档的一页”。保险柜、目录、快速借阅三者配合,星辰科技的纪要制度才真正扛得住生产环境。
- 应用水位 applied已真正执行、写进 MVCC 存储的日志位置。
- 提交水位 commit已被多数派确认、允许应用的日志位置。
- 匹配水位 matched集群中复制进度最慢的 Follower 所达到的位置。
故障与实验——机房断电那一夜
某个凌晨,机房意外断电,星辰科技 5 位核心成员中有 2 位彻底离线。因为白板上的规矩是“多数派开会就有效”,剩下的 3 人继续维持公司运转:大飞继续发纪要、大家照常确认。等两位同事连上网,大飞把断电期间新写的 3 页纪要补发给它们,几分钟后 5 人状态完全一致。整个过程,公司业务没有中断。
这就是 Raft 的少数派容忍能力。理论上,只要超过半数的节点存活且彼此连通,集群就能继续选举和提交。可用性可以用“多数派同时存活的概率”来量化;网络分区时,只有包含多数派的那一侧能选出 Leader,少数派一侧即使内部完全正常也无法提交,从机制上杜绝了脑裂双主。etcd 官方性能实验中,3 节点集群在 kill 掉 leader 后,切换时间通常在 1-2 秒内。
设每个节点独立存活的概率为 q,5 节点集群的可用性为所有“存活节点数 ≥ 3”的概率之和:
故障实验告诉我们:Raft 不是永不故障,而是“多数派在,服务就在”。etcd 生产部署至少要 3 节点、建议 5 节点,正是这份概率账算出来的结论。分布式世界没有银弹,但有了 Raft,星辰科技的纪要在最黑的夜晚也没有乱过一页。
- 单节点存活率 q单个成员在故障窗口内不宕机的概率。
- 组合计数n 个节点中恰好存活 k 个的选法数量。
- 累加多数派存活数从过半下界到全员上界逐项求和。
AI 应用场景
模型版本发布的一致性保障
阶段:模型部署与版本管理
解决的问题:AI 推理服务通常多副本部署,发布新模型版本时,所有副本必须读取同一个“当前生效版本号”。传统方式用 MySQL 主从或定时任务下发,主库故障时切换逻辑复杂且可能丢了最后一次更新;etcd 借助 Raft 日志复制,让版本号的写入在多个节点间强一致,任何节点故障都不会丢确认过的版本记录。
具体案例:某短视频推荐团队把 model_version 键存在 3 节点 etcd 中。发布系统通过 etcdctl put model_version 42 触发一次 Raft 共识:Leader 写日志、复制到多数派、提交后返回成功。推理服务 watch 该键,收到版本变更事件后平滑加载新模型。实验中模拟 kill 掉一个 etcd 节点,剩余 2 节点能继续读改写,客户端无感知。
分布式训练的选主调度
阶段:训练任务调度与容错
解决的问题:多机多卡训练中,需要一个"chief worker"负责全局 checkpoint 和梯度聚合决策。没有强一致选主,多个 worker 可能同时认为自己是主节点,导致 checkpoint 互相覆盖。etcd 的 Raft 领导者选举提供租约机制(lease),worker 竞争 leader 身份并定期续约。
具体案例:某大模型训练框架用 etcd 实现 chief 选举:每个 worker 启动时创建带租约的 key,先成功创建的成为 chief;chief 每 5 秒续约,故障后租约过期,其他 worker 在等锁时自动触发新一轮竞争。实测 8 节点训练集群拔掉 chief 所在机器后,约 2 秒内选出新 chief,训练从最近 checkpoint 继续,仅损失一次保存间隔的计算量。
Python 演示
# -*- coding: utf-8 -*-"""Raft 共识最小模拟:一次选举 + 两次日志复制""" import random def last_log_info(log): """返回最后一条日志的 (任期, 索引),空日志返回 (0, -1)""" if not log: return (0, -1) return (log[-1][0], len(log) - 1) class RaftNode: """单个 Raft 节点""" def __init__(self, nid, peers): self.nid = nid self.peers = peers # 集群内其他节点 self.term = 0 self.state = 'follower' # follower / candidate / leader self.voted_for = None self.vote_count = 0 self.leader_id = None self.log = [] # 日志条目 [(term, command)] self.commit_idx = 0 self.clock = 0 self.election_deadline = 0 # 超过该时间点触发选举 # ---------- 选举 ---------- def start_election(self, now): """发起选举:任期+1、投自己、请求其他节点投票""" if self.state != 'follower': return self.term += 1 self.state = 'candidate' self.voted_for = self.nid self.vote_count = 1 for peer in self.peers: if peer.grant_vote(self, self.term): self.vote_count += 1 if self.vote_count * 2 > len(self.peers) + 1: self.state = 'leader' self.leader_id = self.nid else: self.state = 'follower' def grant_vote(self, candidate, cand_term): """投票规则:任期更新,且候选人的日志不旧于自己""" if cand_term < self.term: return False if cand_term > self.term: self.term = cand_term self.voted_for = None cand_log = last_log_info(candidate.log) my_log = last_log_info(self.log) if self.voted_for is None and cand_log >= my_log: self.voted_for = candidate.nid self.state = 'follower' return True return False # ---------- 日志复制 ---------- def append_entries(self, leader_term, prev_idx, prev_term, entries, now): """一致性检查通过后,把新条目追加到本地日志""" if leader_term < self.term: return False if prev_idx >= 0: if prev_idx >= len(self.log) or self.log[prev_idx][0] != prev_term: return False # 一致性检查失败 self.log = self.log[:prev_idx + 1] + entries self.term = leader_term self.state = 'follower' self.leader_id = self.nid self.clock = now self.election_deadline = now + 5 + random.random() * 5 return True def replicate(self, command, now): """领导者写入本地日志并广播,多数派确认后提交""" if self.state != 'leader': return False self.log.append((self.term, command)) peers_ok = 1 for peer in self.peers: prev_idx = len(self.log) - 2 prev_term = self.log[prev_idx][0] if prev_idx >= 0 else 0 ok = peer.append_entries(self.term, prev_idx, prev_term, self.log[prev_idx + 1:], now) if ok: peers_ok += 1 if peers_ok * 2 > len(self.peers) + 1: self.commit_idx = len(self.log) return True return False def run_simulation(): """3 节点模拟:自动选出 leader,再写入两条命令""" random.seed(7) nodes = [RaftNode(i, []) for i in range(3)] for node in nodes: node.peers = [p for p in nodes if p is not node] # 让初始超时彼此错开,所有人都立刻进入竞选窗口 for i, node in enumerate(nodes): node.election_deadline = i * 2 clock = 0 leader = None for _ in range(100): clock += 1 for node in nodes: if node.state == 'leader': for peer in nodes: if peer is not node: peer.clock = clock # 模拟心跳刷新 elif clock >= node.election_deadline: node.start_election(clock) leader = next((n for n in nodes if n.state == 'leader'), None) if leader: break if leader: leader.replicate('set alpha=1', clock) leader.replicate('set beta=2', clock) print('选举出的领导者: 节点', leader.nid if leader else None) for node in sorted(nodes, key=lambda n: n.nid): cmds = [cmd for _, cmd in node.log] print('节点%d 日志: %s,提交到 %d' % (node.nid, cmds, node.commit_idx)) if __name__ == '__main__': run_simulation()
cand_log = last_log_info(candidate.log)取出候选人最后一条日志的 (任期,索引),用于日志新旧比较,这是选举限制的落地。if self.voted_for is None and cand_log >= my_log:同一任期一个节点只能投一票,且只投给日志不比自己旧的候选人。if self.vote_count * 2 > len(self.peers) + 1:严格过半判断,等价于 quorum = ⌊n/2⌋ + 1。self.log = self.log[:prev_idx + 1] + entries一致性检查通过后追加;若检查失败,leader 会在下一轮用更小的 prev_idx 重试(后退匹配)。peer.clock = clock模拟 leader 的心跳广播,让 follower 不断推迟选举超时。if clock >= node.election_deadline:只有超时才会触发选举,且超时是随机的,避免多个节点同时竞选。
常见误区
误区我以为 Raft 选举就是简单拉票,谁先发起、谁能拿到过半选票谁就是 leader,跟日志内容没关系。
正解选举还附加了“日志新旧”限制——候选人的最后一条日志必须不旧于大多数节点。否则一个日志落后的节点当选后,可能把已确认的数据覆盖掉。这就像公司选拔负责人,不是谁人缘好就行,必须是最了解最近决策的人,否则他一上任就会把公司方向带偏。
为何常见:很多人先学的是“过半投票”这个简化描述,忽略了 Raft 论文中安全性章节的选举限制。
误区日志复制时,leader 把日志发给所有 follower,只要多数确认收到了,就立刻 commit 并告诉客户端成功。
正解必须是“当前任期”的日志才能凭多数直接提交。上一任期的日志即使已经有副本备份,也不能立即提交,而要等当前任期有新日志被提交时连带提交。这是为了防止旧 leader 留下的日志在信息不足时被错误提交,导致已确认数据被推翻。就像上届负责人留下的决议,新班子要先形成自己任期的新决议,才能连带确认旧决议。
为何常见:博客常把流程简化成“多数确认即提交”,把“当前任期”这个关键前提漏掉了。
误区心跳间隔越小,系统发现故障越快,响应也越快,所以心跳时间越小越好。
正解心跳太小会让网络抖动被误判为 leader 失联,频繁触发选举,反而降低可用性。etcd 默认心跳 100ms,选举超时 1000ms 左右,并且加入随机化,就是为了给网络抖动留出足够余量。就像你给朋友发消息,不能 1 秒没回就当他死了开始选新组长。
为何常见:把“延迟越低越好”的直觉套用到分布式环境,忽略了网络不确定性和选举风暴的风险。
误区etcd 是 CP 系统,发生网络分区时必须拒绝所有写请求,保证所有节点数据一致,所以它随时可能不可用。
正解Raft/etcd 的 CP 是“牺牲少数派可用性换一致性”,但仅发生在少数派分区。多数派所在的区可以正常写,表现为持续可用;只有少数派分区内的节点无法选主、拒绝写。所以准确说法是“少数派不可写,多数派持续可用”,而不是“永远不可写”。
为何常见:把 CAP 定理中 P 情况下的“放弃可用性”泛化成“任何时候都可能不可用”,忽略了 Raft 的多数派存活机制。
误区Raft 的日志是权威数据源,必须一直完整保存,保证任何时刻都能回放重建状态机,所以日志不能删。
正解Raft 用快照压缩解决日志无限增长。当 appliedIndex 超过阈值,节点生成快照(状态机拷贝 + 最后包含索引/任期),截断此前日志。新节点通过 InstallSnapshot 直接加载快照,无需完整日志。就像公司的季度总结归档后,原始会议记录就可以进仓库,不用天天翻。
为何常见:教材先讲“日志是权威”,容易忽略快照机制,直到生产环境日志把磁盘撑爆才意识到。
面试题
Q1. 一个 5 节点 Raft 集群,领导者选举成功至少需要几张选票?
- A.得到 2 张选票
- B.得到 3 张选票← 正确答案
- C.得到 4 张选票
- D.得到 5 张选票
答案:B。quorum = ⌊n/2⌋ + 1 = ⌊5/2⌋ + 1 = 3。这和星辰科技“举手数必须超过一半”的规矩完全一致,2 票只是少数派,不够拍板。
Q2. Raft 中领导者收到客户端写请求后,第一步应该做什么?
- A.立即执行并返回结果
- B.追加日志后广播复制← 正确答案
- C.等待多数节点空闲
- D.先询问所有跟随者
答案:B。Leader 必须先把命令封装成日志条目追加到自己的日志末尾,然后通过 AppendEntries 广播给 Follower,多数确认后再提交并返回。这是“先记纪要、后执行”的 Raft 铁律,绝不能边执行边通知。
Q3. Raft 中“任期(term)”的核心作用是什么?
- A.表示日志的执行时长
- B.标记节点的启动时间
- C.区分信息的新旧程度← 正确答案
- D.控制心跳发送频率
答案:C。任期是单调递增的届数编号,所有消息都携带任期。接收方一旦发现对方任期更大,就承认自己过时并转为 Follower。任期如同“第几届决策期”,是 Raft 判断消息新旧、抵御陈旧消息的第一手段。
Q4. 领导者发现某个 Follower 的日志与自己不一致时,正确处理是什么?
- A.强制覆盖全部日志
- B.让跟随者重新选举
- C.逐条回退重新比对← 正确答案
- D.忽略不一致的节点
答案:C。Leader 采用“后退匹配”:把 nextIndex 减一,携带更早的 prevLogIndex 和 prevLogTerm 重新发送 AppendEntries,直到一致性检查通过。像核对纪要时一页页往前翻,找到双方最后一页相同的地方再继续钉新纪要。
Q5. Raft 选举限制要求候选人的日志满足什么条件才能获得选票?
- A.候选人的日志必须最新
- B.候选人不旧于多数节点← 正确答案
- C.候选人必须连续任职
- D.候选人任期必须最大
答案:B。候选人最后一条日志 (term, index) 必须不旧于投票节点自己的日志,这样才能保证胜出的 Leader 包含所有已提交日志。注意不是“必须最新”,而是“不旧于多数”,就像新负责人至少要了解最近的决策,不一定事事都最清楚。
Q6. 为什么 Raft 只允许“当前任期”的日志通过数副本直接提交?
- A.为了降低日志存储开销
- B.为了方便批量提交
- C.防止旧任期日志提交后被推翻← 正确答案
- D.让读请求可以绕过日志
答案:C。旧 leader 可能在某条旧任期日志是否真正达成一致的状态未知时就崩溃了。新 leader 若直接凭副本数提交旧日志,可能误提交一条本应被覆盖的日志,导致已确认记录被推翻。因此旧日志必须等当前任期有日志被提交时连带提交。
Q7. 日志条目 A 为 (term=8, index=5),日志条目 B 为 (term=7, index=9),按 Raft 规则哪个更新?
- A.任期大的 A 更新← 正确答案
- B.索引大的 B 更新
- C.两者一样新
- D.无法比较
答案:A。Raft 比较日志新旧先比任期再比索引:A 的 term=8 大于 B 的 term=7,所以 A 更新。这就像“第 8 届的第 5 条决议”必然新于“第 7 届的第 9 条决议”,届数优先级更高。
Q8. 快照压缩机制带来哪些好处?
- A.降低日志存储占用← 正确答案
- B.加快新节点的同步← 正确答案
- C.提高状态机性能
- D.减少客户端写延迟
答案:AB。快照截断已应用日志,直接降低磁盘占用(A);新节点通过 InstallSnapshot 一步加载快照,不必逐条回放海量日志(B)。快照不改变状态机本身性能(C),也不影响写路径延迟(D),它们不是快照的目的。
Q9. etcd 中一条 Raft 日志从写入到应用到状态机,经历哪些环节?
- A.客户端请求到达领导者← 正确答案
- B.日志复制到多数节点← 正确答案
- C.提交后立即删除日志
- D.应用日志到状态机← 正确答案
答案:ABD。完整链路是:客户端请求 → Leader 追加日志(A)→ 复制给多数派并标记提交(B)→ 应用到状态机并响应客户端(D)。C 错误,提交后日志仍会保留,直到快照压缩才被截断,不能“立即删除”。
Q10. 关于 Raft 联合共识(joint consensus)的说法,正确的有哪些?
- A.需要新旧配置各自多数← 正确答案
- B.配置变更也走日志复制← 正确答案
- C.一步完成旧到新切换
- D.允许联合期间出现双领导者
答案:AB。联合共识要求 C_old 和 C_new 两个配置的多数派同时批准(A),且配置变更本身就是一条日志条目,走标准复制流程(B)。C 错误——联合共识是“旧 → 新旧联合 → 新”的两步切换,不是一步到位;D 错误——联合共识的初衷恰恰是避免过渡期出现两个 Leader。