在一个与世隔绝的小山村,村民们每年都要共同决定庙会的日期、地点和预算。没有电话也没有互联网,大家只能靠挨家挨户传话和定期集会商量事情。更麻烦的是,老王生病无法参会、村口的石桥被洪水冲断,两拨村民一时联系不上。在这种条件下,全村还能不能达成一个统一且大家都认账的决定?这就是分布式系统最核心的“共识问题”。 Raft共识算法就像一套精心设计的“村庄治理手册”。它把复杂的一致性难题拆解成几个彼此独立的子问题:先选出一位权威的“村长”(Leader选举),再让全村统一维护一本“决定日记”(日志复制),最后设定严格的规矩保证账本不会记错(安全性约束)。有了这套手册,即使部分村民缺席、道路中断,村庄依然能正常运转。这正是让多台服务器像一个整体一样工作的核心机制。
学懂 Raft,你就握住了分布式系统一致性这块“压舱石”——它是深入理解 etcd、TiKV 等现代分布式系统的关键入场券。
目录
可视化演示
展示Raft三节点集群Leader选举超时机制、日志复制流水线及安全性约束,支持选举超时调节。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Leader选举过程 橙色候选者自增Term发起RequestVote RPC 蓝色获得多数票后成为Leader,发送心跳 term = max(term) + 1; vote(self) 随机超时机制避免多候选者同时竞选
一、共识问题背景——村庄的难题
假设你生活在一个与世隔绝的村庄,全村几百口人要每年举办一次庙会。问题来了:庙会的日期、地点、预算由谁说了算?没有手机、没有网络,只有挨家挨户的传话和定期集会。更麻烦的是,村西头的老王生病卧床无法参与讨论,村口的石桥被洪水冲断,两边的村民一时联系不上。在这种情况下,全村还能不能达成一个统一的、大家都认账的决定?
在分布式系统中,这个问题被称为“共识问题”(Consensus Problem):一群各自独立的计算机(村民)需要对某个状态达成一致,同时必须容忍部分节点故障(有人缺席)和网络断裂(路被冲断)。如果每个节点各执一词,系统就会陷入混乱,客户端从不同服务器读到的数据会互相矛盾。
Raft 用“状态机复制”(State Machine Replication)来解决这个问题:让所有节点从同一个初始状态出发,按相同的顺序执行相同的操作,那么最终状态自然一致。关键在于,整个系统不需要所有人点头,而是围绕一个简单的“过半多数”原则运作:
有了这个“过半”原则,村庄的每次决策都要求至少过半数村民点头。任何超过半数的决定都不会与另一个超过半数的决定同时存在——这保证了任何时刻都不会出现两个互相矛盾的有效决定,这就是Raft一切安全性的基石。
- N 的含义N 是集群中节点的总数,例如一个 3 节点集群,N 就是 3。
- 向下取整⌊N/2⌋ 表示 N 除以 2 后向下取整,小数部分直接丢弃。
- 加一构成多数在向下取整的基础上加 1,得到不可拆分的过半数阈值。
二、核心术语体系——村里的三种身份
为了让偌大的村庄高效运转,村民们约定俗成形成了三种角色:有人负责拍板,有人负责执行,还有人等待被选。村里把一段时间(比如一年)称为一个“任期”,任期内由一位村长全权负责,任期结束时重新换届。
在 Raft 中,集群里的每个节点在任意时刻都处于三种角色之一。Leader(领导者/村长)是唯一负责接收客户端请求、分发日志、决定提交的角色;Follower(跟随者/村民)被动响应 Leader 的日志复制和投票请求,绝大多数节点平时都是 Follower;Candidate(候选者/竞选者)是当 Follower 在一定时间内没有收到 Leader 的心跳(村长失联)时转换而来,它发起新一届选举。
任期(Term)是 Raft 中的一个全局单调递增的数字,类似村庄的“届数”。每次选举开始,任期加一。日志条目(Log Entry)是集群要执行的操作记录,每条日志由“任期号 + 索引 + 操作命令”组成:
在村庄的类比中,任期号就像“第几届村委”,日志条目就像“村务记录本上的每一条决定”。任何一条决定上都盖着“第几届”的章,大家只看章就能分辨记录的新旧归属,永远不会把两届的账混在一起。
- 索引 i日志条目的位置编号,从 1 开始连续递增,代表账本上的第几行记录。
- 任期号 term_i记录这条日志是在哪个任期提出的,用于判断新旧和匹配关系。
- 操作命令 command_i具体要执行的操作,例如“把变量 x 设为 1”。
三、Leader选举——如何选出村长
村里每隔一段时间就要换届。如果村民发现村长很久没发通知(心跳超时),就会进入“竞选状态”:自己举手报名当候选者,同时给自己投一票,然后向其他村民喊话“请大家支持我”。
具体流程如下:第一步,每个 Follower 等待一个随机超时时间,若超时仍未收到 Leader 心跳,就任期加一并转为候选者;第二步,候选者给自己投票,并并行向所有节点发送 RequestVote 请求;第三步,每个节点在同一任期内只投一票(先到先得),并且只投给日志足够新的候选者;第四步,如果候选者收到超过半数的选票,就当选为 Leader,并立刻开始向所有节点发送心跳来确立权威。
为什么需要随机超时?这就像村民们各喊各的——如果大家都同时举手竞选,票数就会分散,谁也过不了半数。随机等待能让每个人在不同的时刻“醒来”,大概率只有一个人在竞选窗口内发起选举,从而减少选票分裂。如果出现多个候选者同时竞选(选票分裂),这一届无人胜出,所有人超时后进入下一轮重新开始。
村长的当选,意味着村里有了唯一的“话事人”。接下来,所有决策都由这个话事人统一发出,村民们只需执行和确认,不再需要反复争论。
- 票数累计候选者每获得一张赞成票,votesReceived 就增加 1,自己也投自己一票。
- 过半判定把票数与集群一半加一的值比较,只有大于等于它才能胜选。
四、日志复制机制——全村同步一本日记
村长上任后的第一件事,就是把决策记入统一的“村务日记”。当客户端(外来的游客)提出一个诉求(比如“庙会定在 2 月 15 日”),Leader 不会立刻执行,而是先把这条诉求记为一条日志,然后发给所有 Follower,要求他们也在自己的日记里记下这一条。等到过半数 Follower 都回复“记好了”,Leader 才标记这条日志为已提交(committed),并真正执行它,同时在下一轮心跳中告诉 Follower 可以提交。
这个过程中最关键的是日志匹配(Log Matching)检查:Leader 在发送新日志条目时,会带上前一条日志的索引和任期号。Follower 收到后先检查自己的日志里是否有索引相同、任期也相同的条目,只有匹配时才允许追加:
如果日志不一致怎么办?Leader 会像“对账”一样,从后往前逐步回退尝试,把不一致的 Follower 日志强制对齐到自己这边。这就像村里会计重新核对账本:先找一条双方都认可的记录,把后面的全部替换掉。
这种“先记日记、统一对账、过半数再生效”的流程,保证了全村任何时刻都只有一份公认的历史账本。客户端只信任已提交的决定,绝不能在账本还没对上之前就对外宣布结果。
- 检查 prevLogIndex确认前一条日志的索引在自己的日志范围内,防止越界。
- 检查 prevLogTerm确认前一条日志的任期号与 Leader 声称的一致。
- 追加新条目两个条件都满足后,Follower 将新条目追加到自己的日志末尾。
五、安全性约束——保证账本不串行
当选村长的人如果记性不如别人,或者手里拿的是一本残缺的账本,那全村就会出大乱子。Raft 为此设计了选举限制(Election Restriction):不是谁举手都能选上,只有日志“足够新”的候选者才有资格获得选票。
所谓“更新”的比较规则是:先比较最后一条日志的任期号,任期大的更新;任期相同则比较索引,索引大的更新:
有了选举限制,再配合日志复制中的强制对齐机制,Raft 就能推导出核心安全性质——Leader 完整性(Leader Completeness):一旦某条日志在任期 T 中被提交,那么所有后续任期的 Leader 都一定包含这条日志;Leader 永远不会删除或覆盖已提交的日志条目。
这就像村规里写死了一条:凡是“过半数村民确认过的决定”,后面无论谁来当村长,都必须认账,绝不能翻案。正因为有这样的铁规矩,客户端才敢放心地把数据交给分布式系统——一旦写入成功,就永久生效。
- 先比任期如果 a 的最后一条日志任期比 b 大,则认为 a 的日志更新。
- 任期相同比索引任期相等时,日志索引越大,表示日志越多越新。
六、故障与分区处理——村民缺席与山路中断
庙会临近时,可能有村民生病卧床(节点崩溃),也可能山体滑坡封路(网络分区)。Raft 必须保证这两种情况下系统仍然保持正确。
节点崩溃:如果某个 Follower 崩溃了,Leader 会持续给其他节点发心跳;崩溃节点恢复后,Leader 会通过在 AppendEntries 中携带更早的 prevLogIndex 逐步回退,把缺失的日志补上。
网络分区:假设 5 个节点中的 3 个与 Leader 在同一分区,另外 2 个被隔离。被隔离的 2 个节点由于收不到心跳,会发起新的选举——但因为它们只有 2 票,达不到 quorum 的 3 票,永远无法选出 Leader。而包含原 Leader 的分区有 3 个节点,可以正常提交新日志并继续服务。
回到村庄比喻:山路被冲断后,被隔离的村民人数不足以开成“有效的村民大会”,他们再怎么商量,决定也不算数。而另一边人数过半,会议照常进行,决定依然合法。公路恢复后,被隔离的村民会收到完整的新账本,一切重回正轨。这正是“多数派”设计的神奇之处——少数派无论怎么折腾,都改变不了大局。
- 统计 matchedCountLeader 维护每个 Follower 的日志匹配索引,统计已复制该日志的节点数量。
- 过半判定当 matchedCount 达到 quorum 时,该日志条目的提交才被允许。
七、集群变更与优化——村庄扩建与精简
村庄发展到一定规模,可能有新村民加入,也有旧村民迁出。Raft 支持成员变更(Membership Change):新增或移除节点不能一步到位,而需要经过一个特殊的“联合共识(Joint Consensus)”阶段。
联合共识的核心思想:在变更期间,集群中的新旧两组节点同时参与决策,任何决策需要新旧两个 quorum 同时同意,这保证成员切换过程中不会出现“两个 Leader”并存的窗口。实际工程中更常用的是单节点变更(如 etcd):每次只增删一个节点,避免复杂的两阶段切换。
另外一个重要的工程优化是日志压缩(Log Compaction):日志无限增长会耗尽磁盘,Raft 会定期把已提交的状态做成“快照(Snapshot)”,并丢弃快照点之前的旧日志:
村庄同样如此:几十年的“村务日记”堆积如山,村委会每年都会把过往决定汇总成一份“村规总则”,以后只需翻看总则即可,不用再翻旧账。此外,Raft 还支持只读查询优化(ReadIndex):读操作不需要写日志,但为了保证读到的是最新的已提交状态,Leader 需要先与半数节点确认自己仍是 Leader,然后立即执行查询,避免读到过期数据。
- 状态函数 FF 表示按一条日志命令更新状态的转移函数。
- 累计执行从初始状态出发,把前 snapshotIndex 条日志全部执行完毕。
AI 应用场景
场景一:etcd——Kubernetes 的“村务账本”
阶段:分布式系统核心理论 → 数据一致性与高可用存储
解决的问题:Kubernetes 集群的所有配置、服务发现信息、集群状态都存储在 etcd 中。传统单机存储一旦宕机,整个集群就无法读取配置;多副本之间若不同步,又会出现“两个数据版本”互相打架。Raft 让 etcd 的多个副本通过选举和日志复制自动保持一致,即使某个 etcd 节点宕机,只要不超过半数故障,Kubernetes 依然能正确读写配置。
具体案例:生产环境中的 Kubernetes 集群通常部署 3 或 5 个 etcd 节点。某个 etcd 节点所在机器宕机后,Kubernetes 控制面不会感知到任何中断——因为 Leader 仍然活着,日志依然在过半节点上复制。运维人员需要做的只是在机器恢复后让节点自动重新加入集群,同步回最新日志。
场景二:TiKV——分布式事务数据库的复制引擎
阶段:分布式存储与数据库 → 多副本强一致事务
解决的问题:跨数据中心部署时,数据库的每个数据分片都要有多个副本,否则单点宕机就会丢数据;但如果多个副本各自接受写入,不同副本的数据就会分叉。Raft 为 TiKV 提供了“写日志到多数副本才算成功”的强一致复制机制。
具体案例:TiKV 将数据拆分成多个 Region,每个 Region 的副本默认有 3 个,其中一个作为 Region Leader。客户端写入请求到达 Region Leader 后,Leader 把日志复制到至少 2 个副本节点才返回成功。这样即使一个副本所在机房断电,数据仍然存在且一致,TiDB 的分布式事务才能安全地跨节点提交。
场景三:Consul——微服务中的分布式锁与发现
阶段:微服务架构 → 服务注册发现与分布式协调
解决的问题:微服务集群中多个实例需要竞选“主任务执行者”,或者获取一把全局互斥锁,避免多个实例同时执行定时任务造成重复。Consul 使用 Raft 在服务器节点间同步 KV 数据,从而提供可靠的分布式锁和领导选举(Leader Election)能力。
具体案例:在一个订单处理微服务中,多个节点同时启动后,通过 Consul 的 acquire 请求争夺一把锁。只有获得锁的节点才执行定时对账任务;节点宕机后锁自动释放,另一个节点通过 Raft 同步到最新状态后接任任务执行者。整个过程对业务代码透明。
Python 演示
"""Raft共识简化模拟:演示Leader选举与日志复制的核心逻辑"""import random class RaftNode: """一个Raft节点:维护任期、角色与日志""" def __init__(self, nid): self.nid = nid self.term = 0 self.role = "follower" self.voted_for = None self.log = [] # 元素为 (term, command) def request_vote(self, term, candidate_id, last_log_index, last_log_term): """投票请求:只投给日志足够新、且本任期未投过票的候选者""" if term < self.term: return False if term > self.term: self.term = term self.role = "follower" self.voted_for = None if self.voted_for is not None: return False own_last_index = len(self.log) - 1 own_last_term = self.log[-1][0] if self.log else 0 if last_log_term > own_last_term or ( last_log_term == own_last_term and last_log_index >= own_last_index ): self.voted_for = candidate_id return True return False def append_entries(self, term, prev_log_index, prev_log_term, entry): """日志复制:校验前一条日志后追加新条目""" if term < self.term: return False self.term = term self.role = "follower" if prev_log_index >= len(self.log): return False if prev_log_index >= 0 and self.log[prev_log_index][0] != prev_log_term: return False self.log.append(entry) return True def elect_leader(nodes, term): """执行一轮选举,返回(leader, 新任期)""" term += 1 candidate = random.choice(nodes) candidate.term = term candidate.role = "candidate" candidate.voted_for = candidate.nid votes = 1 last_index = len(candidate.log) - 1 last_term = candidate.log[-1][0] if candidate.log else 0 for node in nodes: if node.nid != candidate.nid and node.request_vote( term, candidate.nid, last_index, last_term ): votes += 1 if votes > len(nodes) // 2: candidate.role = "leader" return candidate, term return None, term def replicate(leader, nodes, entry): """Leader将一条日志复制到所有Follower""" idx = len(leader.log) - 1 prev_idx = idx - 1 prev_term = leader.log[prev_idx][0] if prev_idx >= 0 else 0 for node in nodes: if node.nid != leader.nid: ok = node.append_entries(leader.term, prev_idx, prev_term, entry) print(f" 节点{node.nid} 复制{'成功' if ok else '失败'}") def main(): nodes = [RaftNode(1), RaftNode(2), RaftNode(3)] term = 0 leader = None for _ in range(5): leader, term = elect_leader(nodes, term) if leader: print(f"节点{leader.nid}在任期{term}当选为Leader") break if leader is None: print("5轮内未选出Leader(选票分裂)") return entry = (term, "SET x = 1") leader.log.append(entry) replicate(leader, nodes, entry) for node in nodes: print(f"节点{node.nid}日志: {node.log}") if __name__ == "__main__": main()
def request_vote(...)实现投票请求处理:任期检查、日志新旧比较、一任期内只投一票。if last_log_term > own_last_term or (...)投票限制条件:候选者最后一条日志任期更大,或任期相同但日志更长,才能争取到选票。def append_entries(...)实现日志复制:先检查前一索引、前一任期是否吻合,再决定是否追加。if prev_log_index >= len(self.log): return False日志匹配检查:如果前一条索引超出自己日志长度,说明日志有缺口,拒绝复制。votes > len(nodes) // 2过半判定:3 节点集群需要 2 票才能当选 Leader。leader.log.append(entry)Leader 先把自己的日志追加一条,再复制给其他节点,随后通过心跳广播 commitIndex。
常见误区
误区我听说 Paxos 的数学证明特别难,Raft 作为同类的共识算法肯定也一样,没有数学基础根本学不会。
正解Raft 的发明者专门把共识问题拆解成选举、日志复制、安全性三个独立子问题,每个都对应直观的生活逻辑(选村长、记日记、守规矩)。Raft 论文刻意降低了抽象度,工程实现复杂度远低于 Paxos。哪怕没有数学背景,也能理解其核心流程。
为何常见:很多人从 Paxos 的数学描述入门,先入为主地觉得共识算法都必须靠大量公式才能理解;实际上 Raft 的设计目标就是“可理解性优先”。
误区Raft 只有 Leader 能写,Leader 挂了怎么办?那不就是单点故障吗,数据怎么能一致?
正解Raft 采用的是“多数派复制”:只有日志复制到过半节点才算成功,Leader 宕机后由包含全部已提交日志的新 Leader 接替。Leader 不是一个固定机器,而是一个随时可以无缝交接的职位。只要不超过半数节点故障,系统仍能提供强一致服务。
为何常见:看到“强 Leader”就以为和传统“主从复制”一样只靠主节点写入,忽略了过半复制与选举机制带来的容错能力。
误区集群分成两半后,两边各选各的 Leader,各自处理写请求,最后把数据合并起来就行。
正解少数派分区永远凑不齐过半票数,选不出 Leader,更无法提交新日志;只有包含过半节点的分区能继续服务。多数派分区的日志在恢复后会强制同步给少数派,不是“合并”,而是单方向对齐。
为何常见:人们对“双活”和“多副本”的直觉是把分区当成两个独立系统,忽略了共识算法里“多数”才是唯一合法的存在。
误区Leader 把日志发给所有 Follower,只要大家都复制了,这条日志就算提交成功了。
正解提交的定义是日志被复制到超过半数的节点(quorum),而不是“所有节点”。如果所有 Follower 都复制了,但 Leader 还未推进 commitIndex,这条日志仍不算提交。Leader 还需在下一轮心跳中把 commitIndex 广播给所有节点,它们才能真正执行这条命令。
为何常见:人们把“复制到所有节点”与“多数确认”混为一谈,而且没有意识到 commitIndex 需要显式推进和广播。
误区任期号就是选举的时候加一加,选举完就没用了,不参与日志复制和日志比较。
正解任期贯穿 Raft 的所有核心机制:日志条目里带着任期,日志新旧比较先比任期;AppendEntries 校验前一条日志的任期;选票唯一性也依赖任期。任期相当于 Raft 的全局逻辑时钟,任何跨节点的比较都离不开它。
为何常见:初学者只盯着选举流程看,没注意到任期在日志条目、投票约束等场景中的贯穿作用。
面试题
Q1. Raft 中,Follower 在选举超时内未收到 Leader 心跳时会变成什么角色?
- A.直接成为新的 Leader
- B.转换为候选者并发起选举← 正确答案
- C.切换为只读模式等待恢复
- D.重新启动并清空所有日志
答案:B。Follower 超时未收到心跳,就认为村长失联,于是把自己变成候选者(Candidate)并发起新一届选举。它不会直接当上 Leader——还需要获得多数票。C 和 D 都不是 Raft 中定义的正常状态转移。
Q2. Raft 中一条日志条目被“提交(commit)”意味着什么?
- A.Leader 已经把它发送给了所有的 Follower
- B.集群中超过半数的节点已经复制了它← 正确答案
- C.集群中所有节点都已经执行了它
- D.Leader 已经把它写进自己的日志中
答案:B。提交的定义是日志被复制到过半节点(quorum)。就像村庄里的决定必须过半村民点头才生效,仅仅村长自己写在账本上、或者发给所有人但没人确认,都不算真正通过。C 是提交之后执行的结果,不是提交本身。
Q3. Raft 使用哪种机制来避免选票分裂导致选举长期无法完成?
- A.所有节点使用固定的选举超时时间
- B.随机化的选举超时时间← 正确答案
- C.节点 ID 最大的候选者优先当选
- D.任期号越大的节点越先发起竞选
答案:B。随机超时让每个节点在不同的时刻醒来发起选举,大概率只有一个人成为候选者。如果所有节点同时醒来,票数就会分散,谁也过不了半数,选举陷入死循环。这是 Raft 工程上的核心技巧之一。
Q4. Raft 选举限制(Election Restriction)的核心目的是什么?
- A.让所有节点轮流担任 Leader 角色
- B.保证当选 Leader 的日志足够新← 正确答案
- C.减少日志复制的网络开销
- D.让集群中的所有节点保持同步
答案:B。选举限制确保只有日志“不落后于多数投票者”的候选者才能当选。村长换届时,谁掌握最完整的历史账本,谁才有资格执笔,防止“糊涂账”被带到新一届。这直接保证了 Leader 完整性安全性质。
Q5. 关于 Raft 的日志匹配特性,以下哪些说法是正确的?
- A.索引与任期都相同的日志条目,内容必定相同← 正确答案
- B.任期相同的日志条目,其内容一定相同
- C.Leader 发日志时会附带前一条目的任期供校验← 正确答案
- D.所有节点的日志从集群启动起就完全一致
答案:AC。日志匹配性质说的是“索引+任期共同决定内容”,两者相同内容才相同,所以 A 对、B 错。Raft 的 AppendEntries 会携带 prevLogTerm 让 Follower 校验,C 对。D 错——Follower 的日志可能暂时落后,需要 Leader 强制对齐。
Q6. 一个由 5 个节点组成的 Raft 集群,提交一条日志最少需要多少个节点确认?
- A.2 个节点确认
- B.3 个节点确认← 正确答案
- C.4 个节点确认
- D.5 个节点确认
答案:B。根据过半原则,5 节点集群需要 ⌊5/2⌋+1 = 3 个节点确认才能提交。2 票无法构成多数,4 票和 5 票虽然也可以提交,但题目问的是“最少”需要多少。过半多数是 Raft 一切决策的最低门槛。
Q7. 当 5 节点 Raft 集群发生 3:2 网络分区,包含 2 个节点的少数分区会发生什么?
- A.无法选出 Leader,也不能提交日志← 正确答案
- B.可以选出 Leader 并继续提交日志
- C.与多数分区各自为政同时提交
- D.集群停机并等待人工介入
答案:A。少数分区只有 2 个节点,永远凑不够 3 票的 quorum,选不出 Leader,也无法提交新日志。而包含 3 个节点的多数分区可以正常工作。网络恢复后,少数分区的节点会从 Leader 处同步完整日志,不会出现“双主”。
Q8. 以下哪些属于 Raft 保证的核心安全性质?
- A.每个任期内至多产生一个 Leader← 正确答案
- B.Leader 永远不会修改已提交的日志条目← 正确答案
- C.日志一旦发送给 Follower 就立即生效并执行
- D.索引与任期相同则对应命令必定相同← 正确答案
答案:ABD。Raft 的核心安全性包括“选举安全”(每任期至多一个 Leader)、“Leader 不覆盖已提交日志”和“日志匹配”(索引+任期决定内容),所以 A、B、D 正确。C 是错的——日志发送给 Follower 后,必须等 Leader 推进 commitIndex 才会真正生效。
Q9. Raft 中 Leader 发现某个 Follower 日志与自己不一致时,会如何处理?
- A.拒绝该 Follower 的所有投票请求
- B.逐步回退索引,强制 Follower 对齐自己的日志← 正确答案
- C.让 Follower 继续独立运行不再同步
- D.强制重启该 Follower 节点来清空日志
答案:B。Leader 会像对账一样,从后往前逐步减小 prevLogIndex 重试,不断向 Follower 发送更早位置的日志条目,直到找到双方一致的位置,然后把 Follower 的日志强制对齐为自己的版本。拒绝投票、放任不管、暴力重启都不是 Raft 的处理方式。
Q10. 关于 Raft 成员变更与优化机制,下列说法正确的是?
- A.联合共识要求新旧两组配置都达成多数← 正确答案
- B.ReadIndex 读操作不写入日志条目← 正确答案
- C.快照会丢弃快照点之前的旧日志← 正确答案
- D.快照完成后必须保留全部历史日志
答案:ABC。联合共识(Joint Consensus)在变更期间要求新旧配置各自形成 quorum,A 正确。ReadIndex 让 Leader 通过一次多数确认来提供一致性读,不需要写日志,B 正确。快照的本质是把快照点之前的状态固化并丢弃旧日志,C 正确、D 错误——保留全部历史日志正是快照想解决的问题。