测试文件测试内容分析
func TestReElection2A(t *testing.T) {
servers := 3
cfg := make_config(t, servers, false)
defer cfg.cleanup()
cfg.begin("Test (2A): election after network failure")
leader1 := cfg.checkOneLeader()
// if the leader disconnects, a new one should be elected.
cfg.disconnect(leader1)
cfg.checkOneLeader()
// if the old leader rejoins, that shouldn't
// disturb the new leader.
cfg.connect(leader1)
leader2 := cfg.checkOneLeader()
// if there's no quorum, no leader should
// be elected.
cfg.disconnect(leader2)
cfg.disconnect((leader2 + 1) % servers)
time.Sleep(2 * RaftElectionTimeout)
cfg.checkNoLeader()
// if a quorum arises, it should elect a leader.
DPrintf("Check two server can choose a leader")
cfg.connect((leader2 + 1) % servers)
cfg.checkOneLeader()
// re-join of last node shouldn't prevent leader from existing.
cfg.connect(leader2)
cfg.checkOneLeader()
cfg.end()
}
整体测试流程如下:
1、开启3个服务器,检查能否选举出一个leader
2、一个leader掉线,看能否选出一个新的leader
3、旧的leader回归,不应该影响新的leader的状态(当然旧leader迅速被重新选举为leader也没问题)
4、下线两个服务器,此时不应该有leader
5、恢复一个机器,此时有两个机器。应该选举出一个leader
6、恢复下线机器,三台服务器同时可用,此时原有leader的状态不应该被影响
需要推敲的问题
1、断线的旧leader的回来尚未接收到发送的心跳就直接开始新的选举是否有问题
答:没问题,是运行的正常逻辑
2、lastActiveTime在哪些时候进行更新比较好?
答:应该严格遵守figure2的实现流程
If election timeout elapses without receiving AppendEntries RPC from current leader or granting vote to candidate:
只有在接收到当前leader的AppendEntries请求或者批准投票时才会对lastActiveTime进行更新
3、第五步通常会出现新来和回归的机器同时开始选举,怎么办?
答:此时第一次选举应该失败,随后因为二者的election Timeout不同,会自动开启新一轮选举得到最终结果。
4、选举的leader应该将广播时间设置比较早,以便迅速出发发送心跳的服务
核心代码示例
func (rf *Raft) startElection() {
rf.mu.Lock()
defer rf.mu.Unlock()
now := time.Now()
elapses := now.Sub(rf.lastActiveTime)
// 超时的话会从Follower转为Candidate
if rf.role == Follower {
if elapses >= rf.electionTimeOut {
rf.role = Candidate
DPrintf("RaftNode [%d] changed from follower to candidate at term %d", rf.me, rf.currentTerm)
}
}
if rf.role == Candidate && elapses >= rf.electionTimeOut {
rf.lastActiveTime = now
rf.currentTerm += 1
rf.votedFor = rf.me
rf.persist()
// 准备开始发起投票,构造投票流程
args := RequestVoteArgs{
Term: rf.currentTerm,
CandidateId: rf.me,
LastLogIndex: len(rf.logs),
// go不支持三元表达式,所以term赋值写在最后
}
lastLogTerm := 0
if len(rf.logs) != 0 {
lastLogTerm = rf.logs[len(rf.logs)-1].Term
}
args.LastLogTerm = lastLogTerm
// 参数赋予流程初步结束,为了避免RPC请求停顿影响效率,先放掉锁
rf.mu.Unlock()
// 开始for循环不断发起响应通知进行处理
cond := sync.NewCond(&rf.mu)
// 初始化时全部peer里自己就已经给自己投了一票,因此初始化为1
finished := 1
count := 1
for index, _ := range rf.peers {
if index == rf.me {
continue
}
// 发送请求并处理返回结果
go func(server int, args *RequestVoteArgs) {
reply := RequestVoteReply{}
ok := rf.sendRequestVote(server, args, &reply)
rf.mu.Lock()
finished += 1
DPrintf("RaftNode [%d] send Rpc to RaftNode [%d] and get response,%v,now finish %d", rf.me, server, ok, finished)
if ok {
// 进行逻辑运行,除了降级逻辑外,还是尽量将逻辑推迟到下面
if reply.VoteGranted == true {
count += 1
} else {
if reply.Term > rf.currentTerm {
// 状态进行改变清空
rf.currentTerm = reply.Term
rf.role = Follower
rf.votedFor = -1
rf.leaderId = -1
rf.persist()
}
}
DPrintf("RaftNode [%d] get result %d,vote %d", rf.me, finished, count)
}
cond.Broadcast()
rf.mu.Unlock()
}(index, &args)
}
rf.mu.Lock()
// 想要直接往后走:1、投票完毕 2、得到超过半数票数无需继续等待 3、已经不再是candidate状态
// 注意小于等于与大于等于的使用,一开始在这里卡死便是因为小于等于直接满足
for count <= len(rf.peers)/2 && finished != len(rf.peers) && rf.role == Candidate {
DPrintf("finish %d,count %d", finished, count)
cond.Wait()
}
// 不是candidate的话直接结束
if rf.role != Candidate {
return
}
DPrintf("boolean %v,count %d,total %d", count > len(rf.peers)/2, count, len(rf.peers))
// 否则对状态进行判断
if count > len(rf.peers)/2 {
rf.role = Leader
rf.leaderId = rf.me
rf.lastBroadcastTime = time.Unix(0, 0) // 设置为原始时间,保证立刻发送心跳
rf.persist()
DPrintf("RaftNode [%d] become a leader", rf.me)
}
}
return
}
在写的时候本人遇到的错误点如下:
1、在写逻辑表达式、if判断条件时一定要注意大于等于与大于之类的区别。
开始写成count<len(peers)/2,导致一开始直接满足条件跳过cond.wait所处的for循环,永远输出错误结果。实际上应该为count<=len(peers)/2
2、第一遍完成后遇到了split-brain问题,两个都获得了一票,总票数不等于len(peers)且没有人得票超过一半陷入死锁状态
解决方法:将rf.mu.lock从if ok块里提出来,放到rpc结果后。如果机器下线,RPC结果ok为false,也可以进行统计
编程技巧
1、尽可能使用cond.wait等方法来避免for循环空转浪费CPU资源
2、RPC调用前不要加锁,等返回结果后再加锁处理结果,以避免因为网络因素长时间无法得到结果程序无法释放出锁,影响整体执行效率