Lab2
这个lab是要实现一个Raft一致性协议。
lab地址:http://nil.csail.mit.edu/6.82...
raft论文地址:http://nil.csail.mit.edu/6.82...
测试方式:
$ cd src/raft
$ go test
Test (2A): initial election ...
--- FAIL: TestInitialElection2A (5.09s)
config.go:333: expected one leader, got none
Test (2A): election after network failure ...
--- FAIL: TestReElection2A (4.99s)
config.go:333: expected one leader, got none
Test (2B): basic agreement ...
--- FAIL: TestBasicAgree2B (10.05s)
config.go:478: one(100) failed to reach agreement
Test (2B): RPC byte count ...
--- FAIL: TestRPCBytes2B (10.05s)
config.go:478: one(99) failed to reach agreement
Test (2B): agreement despite follower disconnection ...
--- FAIL: TestFailAgree2B (10.00s)
config.go:478: one(101) failed to reach agreement
Test (2B): no agreement if too many followers disconnect ...
lab说明: 通过在raft/raft.go
中添加代码来实现Raft协议。在该文件中,您将找到框架代码,以及如何发送和接收rpc的示例。
您的实现必须支持以下接口,测试和最终的kv服务器将使用该接口。您可以在raft.go的评论中找到更多细节。
// create a new Raft server instance:
rf := Make(peers, me, persister, applyCh)
// start agreement on a new log entry:
rf.Start(command interface{}) (index, term, isleader)
// ask a Raft for its current term, and whether it thinks it is leader
rf.GetState() (term, isLeader)
// each time a new entry is committed to the log, each Raft peer
// should send an ApplyMsg to the service (or tester).
type ApplyMsg
服务调用Make(peers,me,…)
来创建Raft节点。peer
参数是Raft对等点(包括这个)的网络标识符数组,用于RPC。
参数me
是对等体数组中这个对等体的索引。Start
要求Raft启动处理,将命令附加到日志中。Start()
应该立即返回,而不需要等待日志追加完成。
该服务希望您的实现为每个新提交的日志条目发送ApplyMsg
到Make()
的applyCh通道参数。
go包含发送RPC(sendRequestVote()
)和处理传入RPC(RequestVote()
)的示例代码。你的Raft对等体应该使用labrpc
包(源文件在src/labrpc
中)来通信。
测试人员可以告诉labrpc
延迟rpc,重新排列它们,并丢弃它们以模拟各种网络故障。当您可以临时修改labrpc时,请确保您的Raft与原始labrpc一起工作,因为我们将使用它来测试和评分。
Raft实例只能与RPC交互;例如,它们不允许使用共享的Go变量或文件进行通信。后续的实验建立在这个实验的基础上,所以给自己足够的时间来编写可靠的代码是很重要的。
Lab2A
实现Raft Leader
选举和心跳(没有日志条目的AppendEntries rpc
)。
第2A部分的目标是选出一个领导者,如果没有失错误,这个领导者仍然是领导者,如果老领导者失败了,或者与老领导者的数据包丢失了,新的领导者会接替他。
运行go test -run 2A 来测试2A代码。
首先我们需要定义清楚Raft以及两种rpc的参数类型:
type Raft struct {
mu sync.Mutex // Lock to protect shared access to this peer's state
peers []*labrpc.ClientEnd // RPC end points of all peers
persister *Persister // Object to hold this peer's persisted state
me int // this peer's index into peers[]
dead int32 // set by Kill()
// 2A
state State // 身份,follower/candidate/leader
currentTerm int // 当前任期
votedFor int // 投票给哪个节点
logs []Entry // 日志
ElectionTimer *time.Timer // 选举超时定时器,对于所有slave节点来说,如果超时,则成为候选人,发起选举
HeartBeatTimer *time.Timer // 心跳定时器,用于同步日志
}
type RequestVoteArgs struct {
// 2A
Term int // 候选人的日志
CandidateId int // 候选人的id
LastLogIndex int // 候选人最后一条日志的index
LastLogTerm int // 候选人最后一条日志的任期
}
type RequestVoteReply struct {
// 2A
Term int // 返回的任期
VoteGranted bool // 是否投票
}
type AppendEntriesArgs struct {
Term int // leader的term
LeaderId int // leader的id
PrevLogIndex int // 之前附加日志的index
PrevLogTerm int // 之前附加日志的term
Entries []Entry // 附加日志(如果只是心跳则是空的)
LeaderCommitIndex int // leader的已提交日志index
}
type AppendEntriesReply struct {
Term int // 任期
Success bool // 心跳/同步日志是否成功
}
我们需要关注这样几个loop:
Raft的超时选举loop和Raft的心跳loop,他们不断循环处理选举定时器超时和心跳定时器超时的事件。
func (rf *Raft) ElectionLoop() {
for {
select {
case <-rf.ElectionTimer.C:
// 选举定时器超时,发起选举
rf.mu.Lock()
// 成为候选者
rf.SwitchState(Candidate)
rf.mu.Unlock()
ch := make(chan bool, len(rf.peers))
lastLogTerm, lastLogIndex := rf.getLastState()
args := RequestVoteArgs{
Term: rf.currentTerm,
CandidateId: rf.me,
LastLogIndex: lastLogIndex,
LastLogTerm: lastLogTerm,
}
// 请求slave投票
for slave, _ := range rf.peers {
if slave == rf.me {
// 不用给自己发送
continue
}
go func(slave int) {
// call
reply := RequestVoteReply{}
flag := rf.sendRequestVote(slave, &args, &reply)
if flag {
if reply.VoteGranted {
ch <- true
}
}
}(slave)
}
// 等待投票并成为leader
go rf.WaitForVote(ch)
}
}
}
// 心跳
func (rf *Raft) HeartBeatLoop() {
for {
select {
case <-rf.HeartBeatTimer.C:
// 同步所有的心跳
rf.sendHeartBeat()
rf.mu.Lock()
rf.ResetHeartBeatTimer()
rf.mu.Unlock()
}
}
}
几个注意点是:
每次心跳发送完之后就要重置选举定时器,避免leader触发选举。
func (rf *Raft) sendHeartBeat() { // 发送心跳 rf.mu.Lock() if rf.state != Leader { rf.mu.Unlock() return } rf.mu.Unlock() heartBeatNum := 0 wg := sync.WaitGroup{} wg.Add(len(rf.peers) - 1) log.Printf("HeartBeat timeout,server=%v\n", rf.me) for slave := 0; slave < len(rf.peers); slave++ { if slave == rf.me { continue } go func(slave int) { defer wg.Done() rf.mu.Lock() // 互斥锁保证一次心跳的信息是某个状态的 args := &AppendEntriesArgs{ Term: rf.currentTerm, LeaderId: rf.me, PrevLogIndex: len(rf.logs) - 1, PrevLogTerm: rf.logs[len(rf.logs)-1].Term, Entries: nil, LeaderCommitIndex: rf.commitIndex, } rf.mu.Unlock() reply := &AppendEntriesReply{} if ok := rf.sendAppendEntries(slave, args, reply); ok { rf.mu.Lock() heartBeatNum++ rf.mu.Unlock() } }(slave) } // 等待所有rpc完成 wg.Wait() log.Printf("[HeartBeat]%v recv all slave reply: %v\n", rf.me, heartBeatNum) rf.mu.Lock() rf.ResetElectionTimer() // 重置选举定时器 rf.mu.Unlock() }
- 心跳定时器和选举定时器超时后都要重置定时器。
- 每次进行rpc通信后要根据term检查自己的状态,若对方term大于自己,则自己变为follower,votefor=-1。
两个RPC(AppendEntries、RequestVote)实现是本lab的重中之重:
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
// log.Printf("[AppendEntries] Before: Server[%v] state[%v],currentTerm[%v],args[%+v]\n", rf.me, rf.state, rf.currentTerm, args)
rf.checkTermOrUpdateState(args.Term)
reply.Term = rf.currentTerm
reply.Success = true
// reply false if term < currentTerm
if args.Term < rf.currentTerm {
reply.Success = false
return
}
// 到这里说明是真实的leader
rf.ResetElectionTimer()
// 如果term相同,但是自己是候选者,则转变为追随者
if args.Term >= rf.currentTerm && rf.state == Candidate {
rf.SwitchState(Follower)
}
// 如果对方term大于自己,则更新term,并且成为追随者
if args.Term > rf.currentTerm {
rf.currentTerm = args.Term
rf.SwitchState(Follower)
}
if args.Entries == nil || len(args.Entries) <= 0 {
// 日志为空,说明这是心跳
return
}
// 如果preLogIndex不存在logs中或者preLogIndex的日志对应term不匹配,则同步日志失败
if args.PrevLogIndex >= len(rf.logs) || rf.logs[args.PrevLogIndex].Term != args.PrevLogTerm {
reply.Success = false
return
}
// 如果一个已经存在日志在新的位置冲突,删除已经存在日志
i := 0
for ; i < len(args.Entries); i++ {
index := i + args.PrevLogIndex + 1
entry := args.Entries[i]
// 先把之前相同的部分同步
if index < len(rf.logs) && rf.logs[index].Term != entry.Term {
rf.logs = rf.logs[:index]
break
}
}
// 补充之前不在logs中的日志
for ; i < len(args.Entries); i++ {
rf.logs = append(rf.logs, args.Entries[i])
}
// 更新leadercommit
if args.LeaderCommitIndex > rf.commitIndex {
// commitIndex不能超出rf.logs的范围
rf.commitIndex = min(args.LeaderCommitIndex, len(rf.logs)-1)
}
}
func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
// Your code here (2A, 2B).
// 暴露给其他raft节点的rpc接口
// 2A:
rf.mu.Lock()
defer rf.mu.Unlock()
// log.Printf("[RequestVote] Start: Server[%v] state[%v],currentTerm[%v],args[%+v]\n", rf.me, rf.state, rf.currentTerm, args)
reply.Term = rf.currentTerm
reply.VoteGranted = false
// 任期大于对方拒绝
rf.checkTermOrUpdateState(args.Term)
if args.Term < rf.currentTerm {
return
}
// 自己已经投票并且没有投票给对方,拒绝
if rf.votedFor != -1 && rf.votedFor != args.CandidateId {
return
}
// 注意,如果两个被请求投票节点是leader节点,但是其实两个节点的term相同,log也完全相同
// 如何保证leader不会投票给请求方,这里需要保证leader的voteFor是leader自己
if rf.currentTerm < args.Term {
rf.state = Follower
rf.votedFor = args.CandidateId
reply.VoteGranted = true
return
}
lastLogTerm, lastLogIndex := rf.getLastState()
if args.LastLogTerm > lastLogTerm ||
args.LastLogTerm == lastLogTerm && args.LastLogIndex >= lastLogIndex {
// 如果最后一条日志的任期号和候选人的任期号相同,则比较已提交日志的index(目的是保证已提交日志不会被覆盖)
reply.VoteGranted = true
// 成为follower
rf.state = Follower
rf.votedFor = args.CandidateId
} else {
return
}
}
这里的实现主要是要仔细阅读论文,然后多debug。
Lab2B
测试命令:go test -run 2B
。
我们先来阅读一下提示:
- 第一个目标应该是先通过TestBasicAgree2B()。首先是实现Start(),然后编写代码通过AppendEntries rpc发送和接收新的日志条目。
- 您将需要执行选举限制(论文的5.4.1节)。
- 在2B实验室早期的测试中,一个无法达成一致的方法是,即使领导人还活着,也要举行多次选举。
寻找选举计时器管理中的漏洞,或者在赢得选举后没有立即发送心跳。您的代码可能有重复检查某些事件的循环。
不要让这些循环连续执行而不暂停,因为这会使您的实现非常慢,从而导致测试失败。
使用Go的条件变量,或者插入时间。Sleep(10 * time.毫秒)在每个循环迭代中。
我们先来看看start
函数的实现,这里注意函数的返回值并且将command生成对应的log添加到日志中即可。返回值index、term、isLeader,
分别表达这个command的序列号(日志中的顺序)、任期、以及对应raft节点是否是leader(只有leader才会将command添加到日志中)。
func (rf *Raft) Start(command interface{}) (int, int, bool) {
index := -1
term := -1
isLeader := true
if term, isLeader = rf.GetState(); isLeader {
rf.mu.Lock()
rf.logs = append(rf.logs, Entry{
Term: rf.currentTerm,
Command: command,
})
rf.matchIndex[rf.me] = rf.nextIndex[rf.me] + len(rf.logs) - 1
index = len(rf.logs) - 1
if Debug > 0 {
log.Printf("[Start] Server[%v] replicate command to log | current term[%d] | current state[%d]\n | log length[%d]",
rf.me, rf.currentTerm, rf.state, len(rf.logs))
}
rf.mu.Unlock()
}
return index, term, isLeader
}
我们注意到第2个提示,这里要实现选举限制,我们先来看看论文中是如何描述的:
在任何基于领导人的一致性算法中,领导人都必须存储所有已经提交的日志条目。
在某些一致性算法中,例如 Viewstamped Replication,某个节点即使是一开始并没有包含所有已经提交的日志条目,
它也能被选为领导者。这些算法都包含一些额外的机制来识别丢失的日志条目并把他们传送给新的领导人,
要么是在选举阶段要么在之后很快进行。不幸的是,这种方法会导致相当大的额外的机制和复杂性。
Raft使用了一种更加简单的方法,它可以保证所有之前的任期号中已经提交的日志条目在选举的时候都会出现在新的领导人中,
不需要传送这些日志条目给领导人。这意味着日志条目的传送是单向的,只从领导人传给跟随者,
并且领导人从不会覆盖自身本地日志中已经存在的条目。
Raft使用投票的方式来阻止一个候选人赢得选举除非这个候选人包含了所有已经提交的日志条目。
候选人为了赢得选举必须联系集群中的大部分节点,这意味着每一个已经提交的日志条目在这些服务器节点中肯定存在于至少一个节点上。
如果候选人的日志至少和大多数的服务器节点一样新(这个新的定义会在下面讨论),那么他一定持有了所有已经提交的日志条目。
请求投票RPC实现了这样的限制:RPC中包含了候选人的日志信息,然后投票人会拒绝掉那些日志没有自己新的投票请求。
Raft通过比较两份日志中最后一条日志条目的索引值和任期号定义谁的日志比较新。
如果两份日志最后的条目的任期号不同,那么任期号大的日志更加新。
如果两份日志最后的条目任期号相同,那么日志比较长的那个就更加新。
用程序语言表示为:
args.LastLogTerm > lastLogTerm ||
args.LastLogTerm == lastLogTerm && args.LastLogIndex >= lastLogIndex
// 如果候选人的最后一个日志的term大于当前节点的lastLogTerm,则说明一定更新
// 如果lastLogTerm相等,则比较lastLogIndex
// 完整代码如下:
func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
// 任期大于对方拒绝
rf.checkTermOrUpdateState(args.Term)
if args.Term < rf.currentTerm {
reply.Term = rf.currentTerm
reply.VoteGranted = false
return
}
// 自己已经投票并且没有投票给对方,拒绝
if rf.votedFor != -1 && rf.votedFor != args.CandidateId {
reply.Term = rf.currentTerm
reply.VoteGranted = false
}
lastLogTerm, lastLogIndex := rf.getLastState()
if args.LastLogTerm > lastLogTerm || args.LastLogTerm == lastLogTerm && args.LastLogIndex >= lastLogIndex {
// 如果最后一条日志的任期号和候选人的任期号相同,则比较已提交日志的index(目的是保证已提交日志不会被覆盖)
reply.VoteGranted = true
// 成为follower
rf.state = Follower
rf.votedFor = args.CandidateId
} else {
return
}
}
提示3告诉我们,即使某些leader还活着的时候仍然可能存在重复选举的问题,
我们最好在一个候选人成为leader时立刻向其他节点发送心跳来避免多次选举。
实现如下:
func (rf *Raft) Election() {
// 选举定时器超时,发起选举
rf.mu.Lock()
// 成为候选者
rf.SwitchState(Candidate)
lastLogTerm, lastLogIndex := rf.getLastState()
args := RequestVoteArgs{
Term: rf.currentTerm,
CandidateId: rf.me,
LastLogIndex: lastLogIndex,
LastLogTerm: lastLogTerm,
}
rf.mu.Unlock()
// 请求slave投票
numberVoted := 1
for slave, _ := range rf.peers {
if slave == rf.me {
continue
}
go func(slave int) {
reply := RequestVoteReply{}
if rf.sendRequestVote(slave, &args, &reply) {
rf.mu.Lock()
if args.Term != rf.currentTerm {
return
}
if reply.VoteGranted {
numberVoted += 1
if numberVoted > len(rf.peers)/2 && rf.state == Candidate {
rf.SwitchState(Leader)
for i := 0; i < len(rf.peers); i++ {
rf.matchIndex[i] = 0
rf.nextIndex[i] = len(rf.logs)
}
rf.mu.Unlock()
// 发送心跳
rf.sendHeartBeat()
rf.mu.Lock()
}
}
rf.mu.Unlock()
}
}(slave)
}
}
发送心跳的代码如下:
func (rf *Raft) sendHeartBeat() {
// 发送心跳
rf.mu.Lock()
if rf.state != Leader {
rf.mu.Unlock()
return
}
rf.ResetElectionTimer()
rf.ResetHeartBeatTimer()
if Debug > 0 {
log.Printf("Server[%d]start send heartbeat\n", rf.me)
}
rf.mu.Unlock()
for slave := 0; slave < len(rf.peers); slave++ {
if slave == rf.me {
continue
}
go func(slave int) {
for !rf.sendHeartBeatTo(slave) {
}
}(slave)
}
}
func (rf *Raft) sendHeartBeatTo(slave int) (res bool) {
res = true
rf.mu.Lock()
// 互斥锁保证一次心跳的信息是某个状态的
args := &AppendEntriesArgs{
Term: rf.currentTerm,
LeaderId: rf.me,
PrevLogIndex: rf.nextIndex[slave] - 1, // 表示下一个要同步给slave的index
PrevLogTerm: rf.logs[rf.nextIndex[slave]-1].Term,
Entries: rf.logs[rf.nextIndex[slave]:],
LeaderCommitIndex: rf.commitIndex,
}
rf.mu.Unlock()
reply := &AppendEntriesReply{}
if ok := rf.sendAppendEntries(slave, args, reply); ok {
rf.mu.Lock()
if rf.state != Leader {
// 注意,不是leader了
rf.mu.Unlock()
return
}
if rf.currentTerm != args.Term {
// 不是同一个任期内的leader
rf.mu.Unlock()
return
}
if reply.Success {
// 发送心跳成功了,然后更改已匹配日志条目和下一次发送日志条目
rf.matchIndex[slave] = args.PrevLogIndex + len(args.Entries)
rf.nextIndex[slave] = rf.matchIndex[slave] + 1
// 检查
rf.checkN()
} else {
if reply.Term > rf.currentTerm {
rf.SwitchState(Follower)
rf.currentTerm = reply.Term
rf.persist()
rf.mu.Unlock()
return false
}
rf.nextIndex[slave] = reply.ConflictIndex
rf.mu.Unlock()
return false
}
rf.mu.Unlock()
}
return
}
几个注意的地方,首先发送心跳前要检查身份是否是leader,只有leader才能发送心跳。
要在临界区内生成心跳的参数,然后发送时也要检查是否是leader。
rpc成功后,如果心跳接受了,要更新leader的matchIndex、nextIndex(日志同步状态),并且调用checkN()
来更新
commitIndex。
如果心跳没有被接受,则根据返回值的conflictIndex,来更新日志同步状态以便下一次心跳。
关于几个测试的注意事项:
TestFailAgree2B:这个测试要多注意的是AppendEntries()函数的处理。
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
rf.checkTermOrUpdateState(args.Term)
reply.Term = rf.currentTerm
reply.Success = true
// reply false if term < currentTerm
if args.Term < rf.currentTerm {
reply.Term = rf.currentTerm
reply.Success = false
return
}
// 到这里说明是真实的leader
rf.ResetElectionTimer()
// 如果自己是候选者,则转变为追随者
if rf.state == Candidate {
rf.SwitchState(Follower)
}
// 如果preLogIndex不存在logs中或者preLogIndex的日志对应term不匹配,则同步日志失败
if args.PrevLogIndex >= len(rf.logs) || rf.logs[args.PrevLogIndex].Term != args.PrevLogTerm {
reply.Success = false
reply.Term = rf.currentTerm
// 找到conflictIndex
if len(rf.logs) <= args.PrevLogIndex {
reply.ConflictIndex = len(rf.logs)
} else {
// 找到某个term的第一个index
for i := args.PrevLogIndex; i >= 1; i-- {
if rf.logs[i].Term != rf.logs[i-1].Term {
reply.ConflictIndex = i
break
}
}
}
return
}
// 如果一个已经存在日志在新的位置冲突,删除已经存在日志
conflicIndex := 0
isMatch := true
firstIndex := args.PrevLogIndex + 1
for i := 0; i < len(args.Entries); i++ {
if firstIndex+i >= len(rf.logs) || rf.logs[firstIndex+i].Term != args.Entries[i].Term {
isMatch = false
conflicIndex = i
break
}
}
if !isMatch {
rf.logs = append(rf.logs[:conflicIndex+firstIndex], args.Entries[conflicIndex:]...)
rf.persist() //持久化
if Debug > 0 {
log.Printf("Server[%d] append entries[conflic=%d,start=%d,len=%d]from server[%d]\n",
rf.me, conflicIndex, conflicIndex+firstIndex, len(args.Entries)-conflicIndex, args.LeaderId)
}
}
// 更新leadercommit
if args.LeaderCommitIndex > rf.commitIndex {
// commitIndex不能超出rf.logs的范围
rf.commitIndex = min(args.LeaderCommitIndex, len(rf.logs)-1)
go rf.Apply()
}
reply.Term = rf.currentTerm
reply.Success = true
}
TestBackup2B:这个测试主要是不要打印日志,日志会影响效率。