在一个与世隔绝的小山村,村民们每年都要共同决定庙会的日期、地点和预算。没有电话也没有互联网,大家只能靠挨家挨户传话和定期集会商量事情。更麻烦的是,老王生病无法参会、村口的石桥被洪水冲断,两拨村民一时联系不上。在这种条件下,全村还能不能达成一个统一且大家都认账的决定?这就是分布式系统最核心的“共识问题”。 Raft共识算法就像一套精心设计的“村庄治理手册”。它把复杂的一致性难题拆解成几个彼此独立的子问题:先选出一位权威的“村长”(Leader选举),再让全村统一维护一本“决定日记”(日志复制),最后设定严格的规矩保证账本不会记错(安全性约束)。有了这套手册,即使部分村民缺席、道路中断,村庄依然能正常运转。这正是让多台服务器像一个整体一样工作的核心机制。

岗位关联

学懂 Raft,你就握住了分布式系统一致性这块“压舱石”——它是深入理解 etcd、TiKV 等现代分布式系统的关键入场券。

目录
  1. 可视化演示
  2. 一、共识问题背景——村庄的难题
  3. 二、核心术语体系——村里的三种身份
  4. 三、Leader选举——如何选出村长
  5. 四、日志复制机制——全村同步一本日记
  6. 五、安全性约束——保证账本不串行
  7. 六、故障与分区处理——村民缺席与山路中断
  8. 七、集群变更与优化——村庄扩建与精简
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Raft三节点集群Leader选举超时机制、日志复制流水线及安全性约束,支持选举超时调节。

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

Leader选举过程 橙色候选者自增Term发起RequestVote RPC 蓝色获得多数票后成为Leader,发送心跳 term = max(term) + 1; vote(self) 随机超时机制避免多候选者同时竞选

一、共识问题背景——村庄的难题

假设你生活在一个与世隔绝的村庄,全村几百口人要每年举办一次庙会。问题来了:庙会的日期、地点、预算由谁说了算?没有手机、没有网络,只有挨家挨户的传话和定期集会。更麻烦的是,村西头的老王生病卧床无法参与讨论,村口的石桥被洪水冲断,两边的村民一时联系不上。在这种情况下,全村还能不能达成一个统一的、大家都认账的决定?

在分布式系统中,这个问题被称为“共识问题”(Consensus Problem):一群各自独立的计算机(村民)需要对某个状态达成一致,同时必须容忍部分节点故障(有人缺席)和网络断裂(路被冲断)。如果每个节点各执一词,系统就会陷入混乱,客户端从不同服务器读到的数据会互相矛盾。

Raft 用“状态机复制”(State Machine Replication)来解决这个问题:让所有节点从同一个初始状态出发,按相同的顺序执行相同的操作,那么最终状态自然一致。关键在于,整个系统不需要所有人点头,而是围绕一个简单的“过半多数”原则运作:

有了这个“过半”原则,村庄的每次决策都要求至少过半数村民点头。任何超过半数的决定都不会与另一个超过半数的决定同时存在——这保证了任何时刻都不会出现两个互相矛盾的有效决定,这就是Raft一切安全性的基石。

quorum=⌊N/2⌋+1\displaystyle quorum = \left\lfloor N / 2 \right\rfloor + 1
这个公式计算的是“过半多数”:一个集群中有 N 个节点,至少需要 ⌊N/2⌋+1 个节点同意,才能做出决策。
  1. N 的含义N 是集群中节点的总数,例如一个 3 节点集群,N 就是 3。
    N=3\displaystyle N = 3
  2. 向下取整⌊N/2⌋ 表示 N 除以 2 后向下取整,小数部分直接丢弃。
    ⌊3/2⌋=1\displaystyle \left\lfloor 3 / 2 \right\rfloor = 1
  3. 加一构成多数在向下取整的基础上加 1,得到不可拆分的过半数阈值。
    quorum=1+1=2\displaystyle quorum = 1 + 1 = 2

二、核心术语体系——村里的三种身份

为了让偌大的村庄高效运转,村民们约定俗成形成了三种角色:有人负责拍板,有人负责执行,还有人等待被选。村里把一段时间(比如一年)称为一个“任期”,任期内由一位村长全权负责,任期结束时重新换届。

在 Raft 中,集群里的每个节点在任意时刻都处于三种角色之一。Leader(领导者/村长)是唯一负责接收客户端请求、分发日志、决定提交的角色;Follower(跟随者/村民)被动响应 Leader 的日志复制和投票请求,绝大多数节点平时都是 Follower;Candidate(候选者/竞选者)是当 Follower 在一定时间内没有收到 Leader 的心跳(村长失联)时转换而来,它发起新一届选举。

任期(Term)是 Raft 中的一个全局单调递增的数字,类似村庄的“届数”。每次选举开始,任期加一。日志条目(Log Entry)是集群要执行的操作记录,每条日志由“任期号 + 索引 + 操作命令”组成:

在村庄的类比中,任期号就像“第几届村委”,日志条目就像“村务记录本上的每一条决定”。任何一条决定上都盖着“第几届”的章,大家只看章就能分辨记录的新旧归属,永远不会把两届的账混在一起。

log[i]=(termi, commandi)\displaystyle log[i] = (term_i,\ command_i)
日志是一个按索引编号的数组,每个位置保存一条记录:它是在哪个任期提出的,以及要执行什么命令。
  1. 索引 i日志条目的位置编号,从 1 开始连续递增,代表账本上的第几行记录。
    i∈{1,2,3,… }\displaystyle i \in \{1, 2, 3, \dots\}
  2. 任期号 term_i记录这条日志是在哪个任期提出的,用于判断新旧和匹配关系。
    termi=该日志提出时所属的任期\displaystyle term_i = \text{该日志提出时所属的任期}
  3. 操作命令 command_i具体要执行的操作,例如“把变量 x 设为 1”。
    commandi=SET x=1\displaystyle command_i = \text{SET}\ x = 1

三、Leader选举——如何选出村长

村里每隔一段时间就要换届。如果村民发现村长很久没发通知(心跳超时),就会进入“竞选状态”:自己举手报名当候选者,同时给自己投一票,然后向其他村民喊话“请大家支持我”。

具体流程如下:第一步,每个 Follower 等待一个随机超时时间,若超时仍未收到 Leader 心跳,就任期加一并转为候选者;第二步,候选者给自己投票,并并行向所有节点发送 RequestVote 请求;第三步,每个节点在同一任期内只投一票(先到先得),并且只投给日志足够新的候选者;第四步,如果候选者收到超过半数的选票,就当选为 Leader,并立刻开始向所有节点发送心跳来确立权威。

为什么需要随机超时?这就像村民们各喊各的——如果大家都同时举手竞选,票数就会分散,谁也过不了半数。随机等待能让每个人在不同的时刻“醒来”,大概率只有一个人在竞选窗口内发起选举,从而减少选票分裂。如果出现多个候选者同时竞选(选票分裂),这一届无人胜出,所有人超时后进入下一轮重新开始。

村长的当选,意味着村里有了唯一的“话事人”。接下来,所有决策都由这个话事人统一发出,村民们只需执行和确认,不再需要反复争论。

votesReceived≥⌊N/2⌋+1\displaystyle votesReceived \ge \left\lfloor N / 2 \right\rfloor + 1
选举获胜的条件:候选者获得的选票数必须达到“过半多数”。
  1. 票数累计候选者每获得一张赞成票,votesReceived 就增加 1,自己也投自己一票。
    votesReceived=1+∑n=1N−1voten\displaystyle votesReceived = 1 + \sum_{n=1}^{N-1} vote_n
  2. 过半判定把票数与集群一半加一的值比较,只有大于等于它才能胜选。
    votesReceived≥⌊N/2⌋+1\displaystyle votesReceived \ge \left\lfloor N/2 \right\rfloor + 1

四、日志复制机制——全村同步一本日记

村长上任后的第一件事,就是把决策记入统一的“村务日记”。当客户端(外来的游客)提出一个诉求(比如“庙会定在 2 月 15 日”),Leader 不会立刻执行,而是先把这条诉求记为一条日志,然后发给所有 Follower,要求他们也在自己的日记里记下这一条。等到过半数 Follower 都回复“记好了”,Leader 才标记这条日志为已提交(committed),并真正执行它,同时在下一轮心跳中告诉 Follower 可以提交。

这个过程中最关键的是日志匹配(Log Matching)检查:Leader 在发送新日志条目时,会带上前一条日志的索引和任期号。Follower 收到后先检查自己的日志里是否有索引相同、任期也相同的条目,只有匹配时才允许追加:

如果日志不一致怎么办?Leader 会像“对账”一样,从后往前逐步回退尝试,把不一致的 Follower 日志强制对齐到自己这边。这就像村里会计重新核对账本:先找一条双方都认可的记录,把后面的全部替换掉。

这种“先记日记、统一对账、过半数再生效”的流程,保证了全村任何时刻都只有一份公认的历史账本。客户端只信任已提交的决定,绝不能在账本还没对上之前就对外宣布结果。

prevLogIndex≤len(log)∧log[prevLogIndex].term=prevLogTerm\displaystyle prevLogIndex \le len(log) \land log[prevLogIndex].term = prevLogTerm
日志匹配性质:如果两个节点在某条日志上有相同的索引和任期,那么这条日志之前的所有日志也都相同。
  1. 检查 prevLogIndex确认前一条日志的索引在自己的日志范围内,防止越界。
    prevLogIndex≤len(log)\displaystyle prevLogIndex \le len(log)
  2. 检查 prevLogTerm确认前一条日志的任期号与 Leader 声称的一致。
    log[prevLogIndex].term=prevLogTerm\displaystyle log[prevLogIndex].term = prevLogTerm
  3. 追加新条目两个条件都满足后,Follower 将新条目追加到自己的日志末尾。
    log′=append(log, entry)\displaystyle log' = append(log,\ entry)

五、安全性约束——保证账本不串行

当选村长的人如果记性不如别人,或者手里拿的是一本残缺的账本,那全村就会出大乱子。Raft 为此设计了选举限制(Election Restriction):不是谁举手都能选上,只有日志“足够新”的候选者才有资格获得选票。

所谓“更新”的比较规则是:先比较最后一条日志的任期号,任期大的更新;任期相同则比较索引,索引大的更新:

有了选举限制,再配合日志复制中的强制对齐机制,Raft 就能推导出核心安全性质——Leader 完整性(Leader Completeness):一旦某条日志在任期 T 中被提交,那么所有后续任期的 Leader 都一定包含这条日志;Leader 永远不会删除或覆盖已提交的日志条目。

这就像村规里写死了一条:凡是“过半数村民确认过的决定”,后面无论谁来当村长,都必须认账,绝不能翻案。正因为有这样的铁规矩,客户端才敢放心地把数据交给分布式系统——一旦写入成功,就永久生效。

(lastTerma>lastTermb)∨(lastTerma=lastTermb∧lastIndexa≥lastIndexb)\displaystyle (lastTerm_a > lastTerm_b) \lor (lastTerm_a = lastTerm_b \land lastIndex_a \ge lastIndex_b)
这条规则保证:候选人的日志一定不比投票者的日志旧。
  1. 先比任期如果 a 的最后一条日志任期比 b 大,则认为 a 的日志更新。
    lastTerma>lastTermb\displaystyle lastTerm_a > lastTerm_b
  2. 任期相同比索引任期相等时,日志索引越大,表示日志越多越新。
    lastTerma=lastTermb∧lastIndexa≥lastIndexb\displaystyle lastTerm_a = lastTerm_b \land lastIndex_a \ge lastIndex_b

六、故障与分区处理——村民缺席与山路中断

庙会临近时,可能有村民生病卧床(节点崩溃),也可能山体滑坡封路(网络分区)。Raft 必须保证这两种情况下系统仍然保持正确。

节点崩溃:如果某个 Follower 崩溃了,Leader 会持续给其他节点发心跳;崩溃节点恢复后,Leader 会通过在 AppendEntries 中携带更早的 prevLogIndex 逐步回退,把缺失的日志补上。

网络分区:假设 5 个节点中的 3 个与 Leader 在同一分区,另外 2 个被隔离。被隔离的 2 个节点由于收不到心跳,会发起新的选举——但因为它们只有 2 票,达不到 quorum 的 3 票,永远无法选出 Leader。而包含原 Leader 的分区有 3 个节点,可以正常提交新日志并继续服务。

回到村庄比喻:山路被冲断后,被隔离的村民人数不足以开成“有效的村民大会”,他们再怎么商量,决定也不算数。而另一边人数过半,会议照常进行,决定依然合法。公路恢复后,被隔离的村民会收到完整的新账本,一切重回正轨。这正是“多数派”设计的神奇之处——少数派无论怎么折腾,都改变不了大局。

commitAllowed:matchedCount≥⌊N/2⌋+1\displaystyle commitAllowed: matchedCount \ge \left\lfloor N / 2 \right\rfloor + 1
决定一条日志能否提交的唯一标准:日志已经被复制到“过半”节点。
  1. 统计 matchedCountLeader 维护每个 Follower 的日志匹配索引,统计已复制该日志的节点数量。
    matchedCount=∑j=1N1[matchIndexj≥i]\displaystyle matchedCount = \sum_{j=1}^{N} \mathbb{1}[matchIndex_j \ge i]
  2. 过半判定当 matchedCount 达到 quorum 时,该日志条目的提交才被允许。
    matchedCount≥⌊N/2⌋+1\displaystyle matchedCount \ge \left\lfloor N/2 \right\rfloor + 1

七、集群变更与优化——村庄扩建与精简

村庄发展到一定规模,可能有新村民加入,也有旧村民迁出。Raft 支持成员变更(Membership Change):新增或移除节点不能一步到位,而需要经过一个特殊的“联合共识(Joint Consensus)”阶段。

联合共识的核心思想:在变更期间,集群中的新旧两组节点同时参与决策,任何决策需要新旧两个 quorum 同时同意,这保证成员切换过程中不会出现“两个 Leader”并存的窗口。实际工程中更常用的是单节点变更(如 etcd):每次只增删一个节点,避免复杂的两阶段切换。

另外一个重要的工程优化是日志压缩(Log Compaction):日志无限增长会耗尽磁盘,Raft 会定期把已提交的状态做成“快照(Snapshot)”,并丢弃快照点之前的旧日志:

村庄同样如此:几十年的“村务日记”堆积如山,村委会每年都会把过往决定汇总成一份“村规总则”,以后只需翻看总则即可,不用再翻旧账。此外,Raft 还支持只读查询优化(ReadIndex):读操作不需要写日志,但为了保证读到的是最新的已提交状态,Leader 需要先与半数节点确认自己仍是 Leader,然后立即执行查询,避免读到过期数据。

snapshot_state=F(state_before, log[1..snapshotIndex])\displaystyle snapshot\_state = F(state\_before,\ log[1..snapshotIndex])
快照的本质:把 log[1..snapshotIndex] 这些日志执行的最终结果固化成一个状态文件,并记录快照覆盖到的日志索引。
  1. 状态函数 FF 表示按一条日志命令更新状态的转移函数。
    F(s, cmd)=s′\displaystyle F(s,\ cmd) = s'
  2. 累计执行从初始状态出发,把前 snapshotIndex 条日志全部执行完毕。
    snapshot_state=F(F(...F(state0,cmd1)...),cmdsnapshotIndex)\displaystyle snapshot\_state = F(F(...F(state_0, cmd_1)...), cmd_{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 演示

python101 行
"""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()
目的:用约 100 行 Python 实现一个最小的多节点 Raft 模拟,演示随机选举、过半投票、日志复制三条核心逻辑。
  1. def request_vote(...)实现投票请求处理:任期检查、日志新旧比较、一任期内只投一票。
  2. if last_log_term > own_last_term or (...)投票限制条件:候选者最后一条日志任期更大,或任期相同但日志更长,才能争取到选票。
  3. def append_entries(...)实现日志复制:先检查前一索引、前一任期是否吻合,再决定是否追加。
  4. if prev_log_index >= len(self.log): return False日志匹配检查:如果前一条索引超出自己日志长度,说明日志有缺口,拒绝复制。
  5. votes > len(nodes) // 2过半判定:3 节点集群需要 2 票才能当选 Leader。
  6. 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 心跳时会变成什么角色?

单选题 · easy

  • A.直接成为新的 Leader
  • B.转换为候选者并发起选举← 正确答案
  • C.切换为只读模式等待恢复
  • D.重新启动并清空所有日志

答案:B。Follower 超时未收到心跳,就认为村长失联,于是把自己变成候选者(Candidate)并发起新一届选举。它不会直接当上 Leader——还需要获得多数票。C 和 D 都不是 Raft 中定义的正常状态转移。

Q2. Raft 中一条日志条目被“提交(commit)”意味着什么?

单选题 · easy

  • A.Leader 已经把它发送给了所有的 Follower
  • B.集群中超过半数的节点已经复制了它← 正确答案
  • C.集群中所有节点都已经执行了它
  • D.Leader 已经把它写进自己的日志中

答案:B。提交的定义是日志被复制到过半节点(quorum)。就像村庄里的决定必须过半村民点头才生效,仅仅村长自己写在账本上、或者发给所有人但没人确认,都不算真正通过。C 是提交之后执行的结果,不是提交本身。

Q3. Raft 使用哪种机制来避免选票分裂导致选举长期无法完成?

单选题 · mid

  • A.所有节点使用固定的选举超时时间
  • B.随机化的选举超时时间← 正确答案
  • C.节点 ID 最大的候选者优先当选
  • D.任期号越大的节点越先发起竞选

答案:B。随机超时让每个节点在不同的时刻醒来发起选举,大概率只有一个人成为候选者。如果所有节点同时醒来,票数就会分散,谁也过不了半数,选举陷入死循环。这是 Raft 工程上的核心技巧之一。

Q4. Raft 选举限制(Election Restriction)的核心目的是什么?

单选题 · mid

  • A.让所有节点轮流担任 Leader 角色
  • B.保证当选 Leader 的日志足够新← 正确答案
  • C.减少日志复制的网络开销
  • D.让集群中的所有节点保持同步

答案:B。选举限制确保只有日志“不落后于多数投票者”的候选者才能当选。村长换届时,谁掌握最完整的历史账本,谁才有资格执笔,防止“糊涂账”被带到新一届。这直接保证了 Leader 完整性安全性质。

Q5. 关于 Raft 的日志匹配特性,以下哪些说法是正确的?

多选题 · mid

  • A.索引与任期都相同的日志条目,内容必定相同← 正确答案
  • B.任期相同的日志条目,其内容一定相同
  • C.Leader 发日志时会附带前一条目的任期供校验← 正确答案
  • D.所有节点的日志从集群启动起就完全一致

答案:AC。日志匹配性质说的是“索引+任期共同决定内容”,两者相同内容才相同,所以 A 对、B 错。Raft 的 AppendEntries 会携带 prevLogTerm 让 Follower 校验,C 对。D 错——Follower 的日志可能暂时落后,需要 Leader 强制对齐。

Q6. 一个由 5 个节点组成的 Raft 集群,提交一条日志最少需要多少个节点确认?

单选题 · easy

  • A.2 个节点确认
  • B.3 个节点确认← 正确答案
  • C.4 个节点确认
  • D.5 个节点确认

答案:B。根据过半原则,5 节点集群需要 ⌊5/2⌋+1 = 3 个节点确认才能提交。2 票无法构成多数,4 票和 5 票虽然也可以提交,但题目问的是“最少”需要多少。过半多数是 Raft 一切决策的最低门槛。

Q7. 当 5 节点 Raft 集群发生 3:2 网络分区,包含 2 个节点的少数分区会发生什么?

单选题 · mid

  • A.无法选出 Leader,也不能提交日志← 正确答案
  • B.可以选出 Leader 并继续提交日志
  • C.与多数分区各自为政同时提交
  • D.集群停机并等待人工介入

答案:A。少数分区只有 2 个节点,永远凑不够 3 票的 quorum,选不出 Leader,也无法提交新日志。而包含 3 个节点的多数分区可以正常工作。网络恢复后,少数分区的节点会从 Leader 处同步完整日志,不会出现“双主”。

Q8. 以下哪些属于 Raft 保证的核心安全性质?

多选题 · hard

  • A.每个任期内至多产生一个 Leader← 正确答案
  • B.Leader 永远不会修改已提交的日志条目← 正确答案
  • C.日志一旦发送给 Follower 就立即生效并执行
  • D.索引与任期相同则对应命令必定相同← 正确答案

答案:ABD。Raft 的核心安全性包括“选举安全”(每任期至多一个 Leader)、“Leader 不覆盖已提交日志”和“日志匹配”(索引+任期决定内容),所以 A、B、D 正确。C 是错的——日志发送给 Follower 后,必须等 Leader 推进 commitIndex 才会真正生效。

Q9. Raft 中 Leader 发现某个 Follower 日志与自己不一致时,会如何处理?

单选题 · hard

  • A.拒绝该 Follower 的所有投票请求
  • B.逐步回退索引,强制 Follower 对齐自己的日志← 正确答案
  • C.让 Follower 继续独立运行不再同步
  • D.强制重启该 Follower 节点来清空日志

答案:B。Leader 会像对账一样,从后往前逐步减小 prevLogIndex 重试,不断向 Follower 发送更早位置的日志条目,直到找到双方一致的位置,然后把 Follower 的日志强制对齐为自己的版本。拒绝投票、放任不管、暴力重启都不是 Raft 的处理方式。

Q10. 关于 Raft 成员变更与优化机制,下列说法正确的是?

多选题 · extreme

  • A.联合共识要求新旧两组配置都达成多数← 正确答案
  • B.ReadIndex 读操作不写入日志条目← 正确答案
  • C.快照会丢弃快照点之前的旧日志← 正确答案
  • D.快照完成后必须保留全部历史日志

答案:ABC。联合共识(Joint Consensus)在变更期间要求新旧配置各自形成 quorum,A 正确。ReadIndex 让 Leader 通过一次多数确认来提供一致性读,不需要写日志,B 正确。快照的本质是把快照点之前的状态固化并丢弃旧日志,C 正确、D 错误——保留全部历史日志正是快照想解决的问题。