MIT 6.824 Lab 3学习笔记
2026/4/16大约 8 分钟
Lab 3
理论部分
选举流程和实现要求
- 节选自译文对论文图二的解释
Raft 是一种用来管理章节 2 中描述的复制日志的算法。图 2 为了参考之用,总结这个算法的简略版本,图 3 列举了这个算法的一些关键特性。图中的这些元素会在剩下的章节逐一介绍。
Raft 通过选举一个杰出的领导人,然后给予他全部的管理复制日志的责任来实现一致性。领导人从客户端接收日志条目(log entries),把日志条目复制到其他服务器上,并告诉其他的服务器什么时候可以安全地将日志条目应用到他们的状态机中。拥有一个领导人大大简化了对复制日志的管理。例如,领导人可以决定新的日志条目需要放在日志中的什么位置而不需要和其他服务器商议,并且数据都从领导人流向其他服务器。一个领导人可能会发生故障,或者和其他服务器失去连接,在这种情况下一个新的领导人会被选举出来。
通过领导人的方式,Raft 将一致性问题分解成了三个相对独立的子问题,这些问题会在接下来的子章节中进行讨论:
- 领导选举:当现存的领导人发生故障的时候, 一个新的领导人需要被选举出来(章节 5.2)
- 日志复制:领导人必须从客户端接收日志条目(log entries)然后复制到集群中的其他节点,并强制要求其他节点的日志和自己保持一致。
- 安全性:在 Raft 中安全性的关键是在图 3 中展示的状态机安全:如果有任何的服务器节点已经应用了一个确定的日志条目到它的状态机中,那么其他服务器节点不能在同一个日志索引位置应用一个不同的指令。章节 5.4 阐述了 Raft 算法是如何保证这个特性的;这个解决方案涉及到选举机制(5.2 节)上的一个额外限制。
在展示一致性算法之后,这一章节会讨论一些可用性的问题和计时在系统中的作用。
状态:
所有服务器上的持久性状态 (在响应 RPC 请求之前,已经更新到了稳定的存储设备)
| 参数 | 解释 |
|---|---|
| currentTerm | 服务器已知最新的任期(在服务器首次启动时初始化为0,单调递增) |
| votedFor | 当前任期内收到选票的 candidateId,如果没有投给任何候选人 则为空 |
| log[] | 日志条目;每个条目包含了用于状态机的命令,以及领导人接收到该条目时的任期(初始索引为1) |
所有服务器上的易失性状态
| 参数 | 解释 |
|---|---|
| commitIndex | 已知已提交的最高的日志条目的索引(初始值为0,单调递增) |
| lastApplied | 已经被应用到状态机的最高的日志条目的索引(初始值为0,单调递增) |
领导人(服务器)上的易失性状态 (选举后已经重新初始化)
| 参数 | 解释 |
|---|---|
| nextIndex[] | 对于每一台服务器,发送到该服务器的下一个日志条目的索引(初始值为领导人最后的日志条目的索引+1) |
| matchIndex[] | 对于每一台服务器,已知的已经复制到该服务器的最高日志条目的索引(初始值为0,单调递增) |
追加条目(AppendEntries)RPC:
由领导人调用,用于日志条目的复制,同时也被当做心跳使用
| 参数 | 解释 |
|---|---|
| term | 领导人的任期 |
| leaderId | 领导人 ID 因此跟随者可以对客户端进行重定向(译者注:跟随者根据领导人 ID 把客户端的请求重定向到领导人,比如有时客户端把请求发给了跟随者而不是领导人) |
| prevLogIndex | 紧邻新日志条目之前的那个日志条目的索引 |
| prevLogTerm | 紧邻新日志条目之前的那个日志条目的任期 |
| entries[] | 需要被保存的日志条目(被当做心跳使用时,则日志条目内容为空;为了提高效率可能一次性发送多个) |
| leaderCommit | 领导人的已知已提交的最高的日志条目的索引 |
| 返回值 | 解释 |
|---|---|
| term | 当前任期,对于领导人而言 它会更新自己的任期 |
| success | 如果跟随者所含有的条目和 prevLogIndex 以及 prevLogTerm 匹配上了,则为 true |
接收者的实现:
- 返回假 如果领导人的任期小于接收者的当前任期(译者注:这里的接收者是指跟随者或者候选人)(5.1 节)
- 返回假 如果接收者日志中没有包含这样一个条目 即该条目的任期在 prevLogIndex 上能和 prevLogTerm 匹配上 (译者注:在接收者日志中 如果能找到一个和 prevLogIndex 以及 prevLogTerm 一样的索引和任期的日志条目 则继续执行下面的步骤 否则返回假)(5.3 节)
- 如果一个已经存在的条目和新条目(译者注:即刚刚接收到的日志条目)发生了冲突(因为索引相同,任期不同),那么就删除这个已经存在的条目以及它之后的所有条目 (5.3 节)
- 追加日志中尚未存在的任何新条目
- 如果领导人的已知已提交的最高日志条目的索引大于接收者的已知已提交最高日志条目的索引(
leaderCommit > commitIndex),则把接收者的已知已经提交的最高的日志条目的索引commitIndex 重置为 领导人的已知已经提交的最高的日志条目的索引 leaderCommit 或者是 上一个新条目的索引 取两者的最小值
请求投票(RequestVote)RPC:
由候选人负责调用用来征集选票(5.2 节)
| 参数 | 解释 |
|---|---|
| term | 候选人的任期号 |
| candidateId | 请求选票的候选人的 ID |
| lastLogIndex | 候选人的最后日志条目的索引值 |
| lastLogTerm | 候选人最后日志条目的任期号 |
| 返回值 | 解释 |
|---|---|
| term | 当前任期号,以便于候选人去更新自己的任期号 |
| voteGranted | 候选人赢得了此张选票时为真 |
接收者实现:
- 如果
term < currentTerm返回 false (5.2 节) - 如果 votedFor 为空或者为 candidateId,并且候选人的日志至少和自己一样新,那么就投票给他(5.2 节,5.4 节)
所有服务器需遵守的规则:
所有服务器:
- 如果
commitIndex > lastApplied,则 lastApplied 递增,并将log[lastApplied]应用到状态机中(5.3 节) - 如果接收到的 RPC 请求或响应中,任期号
T > currentTerm,则令currentTerm = T,并切换为跟随者状态(5.1 节)
跟随者(5.2 节):
- 响应来自候选人和领导人的请求
- 如果在超过选举超时时间的情况之前没有收到当前领导人(即该领导人的任期需与这个跟随者的当前任期相同)的心跳/附加日志,或者是给某个候选人投了票,就自己变成候选人
候选人(5.2 节):
- 在转变成候选人后就立即开始选举过程
- 自增当前的任期号(currentTerm)
- 给自己投票
- 重置选举超时计时器
- 发送请求投票的 RPC 给其他所有服务器
- 如果接收到大多数服务器的选票,那么就变成领导人
- 如果接收到来自新的领导人的附加日志(AppendEntries)RPC,则转变成跟随者
- 如果选举过程超时,则再次发起一轮选举
领导人:
- 一旦成为领导人:发送空的附加日志(AppendEntries)RPC(心跳)给其他所有的服务器;在一定的空余时间之后不停的重复发送,以防止跟随者超时(5.2 节)
- 如果接收到来自客户端的请求:附加条目到本地日志中,在条目被应用到状态机后响应客户端(5.3 节)
- 如果对于一个跟随者,最后日志条目的索引值大于等于 nextIndex(
lastLogIndex ≥ nextIndex),则发送从 nextIndex 开始的所有日志条目:- 如果成功:更新相应跟随者的 nextIndex 和 matchIndex
- 如果因为日志不一致而失败,则 nextIndex 递减并重试
- 假设存在 N 满足
N > commitIndex,使得大多数的matchIndex[i] ≥ N以及log[N].term == currentTerm成立,则令commitIndex = N(5.3 和 5.4 节)
实践部分
实现选举机制
问题排查
Lab 3A
Go 1.18遗留问题

非 常量字符串引用??t.Fatalf的第一个参数应该是字符串字面量,而不是变量