代码 github 链接
强烈建议先自己做。
先放上测试全部通过的结果:
文章目录
流程逻辑
论文中的图 2 非常重要!经常去看相关的参数和流程。
这部分主要实现 Leader 选举和心跳。
主要关⼼发送和接收 RequestVote RPC、与选举相关的服务器规则以及与领导者选举相关的状态。
- 2A 部分启动测试程序只需在
raft.go
所在目录命令行输入:
go test -run 2A -race
这里加了 -race
用于检测并发冲突,建议加上。
- 测试程序首先会调用多次
rf = Make(...)
创建多个 Raft 节点。 - 每个节点在
Make()
中初始化一些参数后就会go rf.ticker()
启动 ticker 协程。 ticker
用来循环检测是否选举超时,是不是要变为 candidate 开始选举。这里就需要写选举逻辑,请求投票 RPC,循环需要一个随机睡眠时间。- 选举成为 leader 后就要开启 leader 的工作,2A 部分只有心跳,也就是空的 追加日志 RPC。
- 需要处理发送接收 RPC 的逻辑,看论文图 2 和 5.2 节,下面也挑出重点总结了。
- 在 Raft 节点转变身份、处理 RPC 时建议打印日志,方便后续 Debug(
util.go
中有封装好的打印日志方法,可以直接用)。
其他要求
仔细查看实验手册,很多细节需要注意。
- 测试要求leader每秒发送⼼跳RPC不超过⼗次,也就是最多 100ms 一次。
- 要求 Raft 在旧领导者失败后的 5 秒内选举新领导者,即使需要多轮选举,需要自己选择合适的检查选举超时时间。
- 检查选举超时需要随机睡眠时间,原因论文里有,防止节点只给自己投票一直选举失败。
- 在死循环时加上
rf.killed()
判断,测试杀死 Raft 后直接退出。 - RPC 所有结构首字母都要大写,不然无法外部访问。
- 这里面使用 RPC 都是调用的 labrpc 包,位置
../labrpc/labrpc.go
,便于模拟有损网络,其中服务器可能无法访问,请求和回复可能会丢失。但是使用和 Lab 1里的一样,直接用就行。 - 注意并发安全,共享变量需要加锁。
设置参数
- 首先在
Raft struct
中补充节点在选举中的状态参数,这里还需定义一个 Log 条目的结构体(每⼀个条⽬包含⼀个⽤户状态机执⾏的指令,和收到时的任期号)。 - 填写
RequestVoteArgs
和RequestVoteReply
结构。注意字段名首字母都要大写才能外部访问。 - 要实现⼼跳,需要定义
AppendEntriesArgs
和AppendEntriesReply
结构(虽然此时还不需要所有参数)。
论文中需要关心的部分
摘自我的论文笔记,只摘录与 2a 部分有关的。
AppendEntries RPC(追加待同步⽇志 RPC)
由 Leader 负责调⽤来复制⽇志(5.3);也会⽤作心跳(5.2)
传入参数:
term | 领导⼈的任期号 |
leaderId | 领导⼈的 Id,以便于跟随者重定向请求 |
返回值:
term | 当前的任期号,⽤于领导⼈去更新⾃⼰ |
---|
接收者实现:
- 如果
term < currentTerm
就返回false
(5.1 节) - 如果⽇志在
prevLogIndex
位置处的⽇志条⽬的任期号和prevLogTerm
不匹配,则返回false
(5.3 节) - 如果现有的⽇志条⽬和新的产⽣冲突(索引值相同但是任期号不同),删除现有的和之后所有的条目 (5.3 节)
- 追加⽇志中尚未存在的任何新条⽬
- 如果
leaderCommit
>commitIndex
,令commitIndex = min(leaderCommit, 新日志条目索引)
RequestVote RPC(请求投票 RPC)
由候选⼈调⽤⽤来征集选票(5.2 节)
传入参数:
term | 候选⼈的任期号 |
---|---|
candidateId | 请求选票的候选⼈的 Id |
返回值:
term | 当前任期号,以便于候选⼈去更新⾃⼰的任期号 |
---|---|
voteGranted | 候选⼈赢得了此张选票时为 true |
接收者实现:
- 如果
term < currentTerm
返回false
(5.2 节) - 如果
votedFor
为null
或者为candidateId
,并且候选人的日志至少和接受者一样新,那么就给它投票(5.2 节,5.4 节)
Rules for Servers(服务器的规则)
所有服务器:
- 如果接收到的 RPC 请求或响应中,任期号
T > currentTerm
,那么就令currentTerm
等于T
,并切换状态为 Follower(5.1 节)
Followers(跟随者)(5.2 节):
- 响应来自候选人和领导者的 RPC 请求
- 如果选举超时,都没有收到现任 Leader 的AppendEntries RPC,也没有给候选人投票:自己转变成候选人。
Candidates(候选人)(5.2 节):
- 在转变成候选人后就立即开始选举过程
- 自增当前的任期号(
currentTerm
) - 给自己投票
- 重置选举超时计时器
- 发送 RequestVote RPC 给其他所有服务器
- 自增当前的任期号(
- 如果接收到大多数服务器的选票,那么就变成 Leader
- 如果接收到来自新的 Leader 的 AppendEntries RPC,转变成 follower
- 如果选举过程超时,再次发起一轮选举
Leader(领导人):
- 一旦成为领导人:发送空 AppendEntries RPCs(心跳)给每个服务器;在空闲期间重复发送,防止选举超时(5.2 节)
Raft 集群的服务器都处于三个状态之一:
- Leader:只有一个,响应所有客户端请求
- Follower:其余都是,不发送只响应 Leader 或 Candidate 的请求。若客户向其请求,会重定向到 Leader。
- Candidate:选举新 Leader 时使用(5.2)
图 4:服务器状态。Follower 只响应来自其他服务器的请求。如果 Follower 接收不到消息,那么他就会变成 Candidate 并发起一次选举。获得集群中大多数选票的 Candidate 将成为 Leader。在一个任期内,Leader 保持身份直到自己宕机。
任期编号在 Raft 算法中充当逻辑时钟,每个节点都储存当前任期号,节点之间通信会交换任期号,当一个节点:
- 当前任期号比其他节点小,更新自己的任期号
- Leader 或 Candidate 发现自己任期号过期,立即转为 Follower
- 收到过期的任期号请求,拒绝请求。
节点之间通信使用远程过程调用(RPCs),包含两种(第7节还增加了第三种传送快照的):
- **请求投票(RequestVote) RPCs:**Candidate 在选举期间发起(5.2)
- 追加条目(AppendEntries)RPCs:Leader 发起,用于复制日志和心跳(5.3)
节点未及时收到 RPC 响应会重试,能并行发起 RPCs。
5.2 Leader 选举(Leader election)
- 刚开始所有节点都是 Follower。
- Follwer 一段时间没接收到消息即选举超时,发起新选举。
- Leader 周期性发送**心跳包(不含日志的 AE RPC)**给所有 Follower 来维持自己地位。
- Follwer 只要能收到 Leader 或 Candidate 的 RPC 就保持当前状态。
- 开始选举。Follower 自增
term
(任期号)并转为 Candidate,并行向其他节点发送 RV RPC 等待给自己投票。- 等待时收到 Leader 的心跳,且心跳中的任期不小于自己的当前任期,则自己变为 Follower。若小于自己的任期,则拒绝并保持 Candidate。
- 如果同时出现多个 Candidate,选票可能被瓜分,没有人得到多数选票。则等待超时后重新选举。
- Raft 使用随机选举超时时间(例如 150-300 毫秒)防止多次无人上任。每个节点开始选举时重制超时时间。可以让多数情况只有一个节点超时,进入下一轮赢得选举。
- 获得多数选票的 Candidate 变为 Leader。
- 每个节点在一个任期内,按先来先服务(5.4节还有额外限制)最多为一个 Candidate 投票。
- 成为 Leader 后向其他节点发送心跳建立权威。
我的代码逻辑
代码逻辑框架仅供参考,完整代码可以到 我的 github 查看。
非常建议自己写,收获会最大!
// 创建节点
func Make(peers []*labrpc.ClientEnd, me int, persister *Persister, applyCh chan ApplyMsg) *Raft {
// ...
go rf.ticker()
}
// 判断选举超时
func (rf *Raft) ticker() {
for rf.killed() == false{
// 随机睡眠
// 如果成为了 leader,循环结束
// if 选举超时
go rf.candidateDo()
// 未超时
//...
}
}
// candidate 要实现的请求投票
func (rf *Raft) candidateDo() {
// 并发请求投票 RPC,计算票数
ok := rf.sendRequestVote(server, rvArgs, rvReply)
// 票数过半,启动 leader
go rf.leaderDo()
}
// 发送 RequestVote RPC
func (rf *Raft) sendRequestVote(server int, args *RequestVoteArgs, reply *RequestVoteReply) bool {
ok := rf.peers[server].Call("Raft.RequestVote", args, reply)
return ok
}
// RequestVote RPC 处理程序.
func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
// 处理逻辑看论文
}
// leader 要实现的功能
func (rf *Raft) leaderDo() {
for rf.killed() == false {
// 失去 leader 身份,结束循环
// 发追加日志 RPC,实现心跳
ok := rf.sendAppendEntries(server, aeArgs, aeReply)
// 心跳间隔睡眠
}
}
// 发送 AppendEntries RPC
func (rf *Raft) sendAppendEntries(server int, args *AppendEntriesArgs, reply *AppendEntriesReply) bool {
ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)
return ok
}
// AppendEntries RPC 处理程序.
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
// 处理逻辑看论文
}