labA的内容
labA的内容就是实现raft的选举leader部分,最重要的是处理三个状态之间的关系,以及超时选举和心跳模块
ticker背景部分
通过select case阻塞来判断执行哪个逻辑,阻塞channel和rpc请求部分不要加锁,不如容易死锁,或者超时等待不符合预期。非阻塞执行完逻辑后,就重启该case的定时器,如果是选举的话,就使用
func (rf *Raft) ticker() {
for !rf.killed() {
select {
case <-rf.ElectionTimeOut.C:
{
rf.mu.Lock()
// 开始新的选举
log.Println("state is ", rf.Raft_Status)
log.Println(rf.me, " start to try to get votes")
rf.CandidateAction()
// 重设置计时器
rf.ResetElection()
rf.mu.Unlock()
}
case <-rf.HeartBeatTimeOut.C:
{
rf.mu.Lock()
if rf.Raft_Status == Leader {
log.Println(rf.me, " start heart beat")
// 开始心跳监测
rf.StartHeart()
rf.HeartBeatTimeOut.Reset(time.Duration(rf.HeartTime) * time.Millisecond)
}
rf.mu.Unlock()
}
}
// Your code here (2A)
// Check if a leader election should be started.
// TODO:
// pause for a random amount of time between 50 and 350
// milliseconds.
// ms := 50 + (rand.Int63() % 300)
// time.Sleep(time.Duration(ms) * time.Millisecond)
}
}
然后是选举超时部分
变成候选人,把自己的currentTerm自增,然后投票给自己,并行发送(注意是并行非阻塞,不需要等待所以go协程完成后再开始,这会影响我们timer)requestvote,因为我们的current_term可能在发现更大term时发生变化,所以,我们要把args放在全局区,对比当前raft状态也是用args来进行,注意要判断是不是raft变成了follwer,如果变成了follower,我们就要停止发送rpc请求,如果得到了大多数选票,我们就变成了leader,要马上发送空日志心跳来通知别的我已经是leader了,或者通过心跳发现term更大的话,我们就变成了follower(把选票清空,这个地方也是为数不多可以来重置选票为-1的地方了),同时将当前任期改为term.
func (rf *Raft) CandidateAction() {
log.Println("start candidate")
rf.Raft_Status = Candidate
rf.CurrentTerm++
log.Println(rf.CurrentTerm)
rf.VoteFor = rf.me
log.Println("candidate start get vote and vote itself")
vote_cnt := 1
// var wg sync.WaitGroup
// wg.Add(len(rf.peers) - 1)
args := RequestVoteArgs{
Candidate_Curr_Term: rf.CurrentTerm,
Candidate_Id: rf.me,
// Last_Log_Index: last_idx,
// Last_Log_Term: rf.Log_Array[last_idx].Log_Term,
}
for i := range rf.peers {
if i != rf.me {
//last_idx := len(rf.Log_Array) - 1
go func(idx int) {
rf.mu.Lock()
if rf.Raft_Status != Candidate {
rf.mu.Unlock()
return
}
rf.mu.Unlock()
reply := RequestVoteReply{}
if rf.sendRequestVote(idx, &args, &reply) {
rf.mu.Lock()
defer rf.mu.Unlock()
log.Println(rf.me, "send the rpc to the ", idx)
// to do
if rf.CurrentTerm == args.Candidate_Curr_Term && rf.Raft_Status == Candidate {
log.Println("pos here")
if reply.Vote_Granted {
vote_cnt++
log.Println(rf.me, "get vote numver is", vote_cnt)
if vote_cnt > len(rf.peers)/2 {
rf.ToLeader()
rf.StartHeart()
log.Println("heart sync finish, change ", rf.me, " to leader")
}
} else if reply.Current_Term > rf.CurrentTerm {
log.Println("the leader to follower ", rf.me)
rf.ToFollower()
rf.CurrentTerm, rf.VoteFor = reply.Current_Term, -1
}
}
} else {
log.Println("rpc fail vote")
}
// wg.Done()
}(i)
// 并行发送到其他所以机器上投票
}
}
}
RequestVote
如果当前term大于请求方的term,时失败了,并要求发送方变成follower,或者当前term < 请求term (term相当于逻辑时钟,拥有强大的统治力)成功,当前变成follower, 或者term = 请求term,如果票被选并且不是请求方获得的话,也是失败的。当然变成follower时把他们选举时间重置一下
func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
// Your code here (2A, 2B).
log.Println("Rpc requestVote start ", rf.me)
rf.mu.Lock()
log.Println("Rpc get lock", rf.me)
defer rf.mu.Unlock()
reply.Vote_Granted = false
if rf.CurrentTerm > args.Candidate_Curr_Term || (rf.CurrentTerm == args.Candidate_Curr_Term && rf.VoteFor != -1 && rf.VoteFor != args.Candidate_Id) {
reply.Current_Term = rf.CurrentTerm
reply.Vote_Granted = false
log.Println(args.Candidate_Id, " vote grand fail", rf.me)
return
}
if args.Candidate_Curr_Term > rf.CurrentTerm {
log.Println("request is higer!!!!")
rf.ToFollower()
rf.VoteFor = -1
rf.CurrentTerm = args.Candidate_Curr_Term
}
// -1 stand for null
rf.VoteFor = args.Candidate_Id
reply.Current_Term = rf.CurrentTerm
log.Println(args.Candidate_Id, " vote grand ", rf.me)
rf.ResetElection()
reply.Vote_Granted = true
// TODO: term is used for the candidate to update itself
}
HeatBeat
心跳就是重置定时器,防止follwer超时用的,还有一个就是要发现大的term,因为获得多数选票这个限制导致了一个term只能有一个leader,但是当term变大了,就可能有新的leader,然后我们把小的leader变成follwer,因为一旦有大的存在,小的就不可能在后续当leader了。这里为了保障leader的timeout不超时,我采用了心跳得到回应来重置timeout。
unc (rf *Raft) StartHeart() {
args := AppendArgs{
Leader_Term: rf.CurrentTerm,
Leader_Id: rf.me,
// PrevLogTerm: rf.Log_Array[rf.Next_Idx[idx]].Log_Term,
// PrevLogIndex: rf.Next_Idx[idx],
Entries: rf.Log_Array,
Leader_Commit: rf.Committed_Idx,
}
for i := range rf.peers {
if i != rf.me {
// pre_idx := rf.Next_Idx[i]
go func(idx int) {
rf.mu.Lock()
if rf.Raft_Status != Leader {
rf.mu.Unlock()
return
}
rf.mu.Unlock()
reply := AppendReply{}
if rf.SendAppendEntries(idx, &args, &reply) {
rf.mu.Lock()
defer rf.mu.Unlock()
if !reply.Success {
log.Println("find the leadr and change state to follower")
rf.ToFollower()
rf.CurrentTerm, rf.VoteFor = reply.Term, -1
} else {
rf.ResetElection()
}
// if reply.Success {
// log.Println("append success", idx)
// rf.Next_Idx[idx] = len(rf.Log_Array) - 1
// return
// }
// // pre_idx == 0的情况
// //变化为term中最后一个
// if pre_idx > 0 && rf.Log_Array[pre_idx-1].Log_Term == rf.Log_Array[pre_idx].Log_Term {
// for pre_idx > 0 && rf.Log_Array[pre_idx-1].Log_Term == rf.Log_Array[pre_idx].Log_Term {
// pre_idx--
// }
// } else {
// //已经是term最后一个直接,进入下一个
// pre_idx--
// }
// rf.Next_Idx[idx] = pre_idx
} else {
log.Println("rpc append failed")
}
}(i)
}
}
}
AppendEntries
这和上面部分要讲的差不多,就不重复说明了
func (rf *Raft) AppendEntries(args *AppendArgs, reply *AppendReply) {
log.Println("append ", rf.me)
rf.mu.Lock()
defer rf.mu.Unlock()
if args.Leader_Term < rf.CurrentTerm {
reply.Term = rf.CurrentTerm
reply.Success = false
return
}
if args.Leader_Term > rf.CurrentTerm {
rf.CurrentTerm, rf.VoteFor = args.Leader_Term, -1
}
// flag := false
// for idx, entry := range rf.Log_Array {
// if entry.Log_Term == args.PrevLogTerm && idx == args.PrevLogIndex {
// flag = true
// break
// }
// }
// if !flag {
// reply.Success = false
// return
// }
// // 以上是为了参与者和自己的日志相同,如果不是则拒绝
// // 确认一致的最后一条日志,然后删除这条之后的,再添加
// if len(rf.Log_Array) > args.PrevLogIndex && rf.Log_Array[args.PrevLogIndex].Log_Term == args.PrevLogTerm {
// rf.Log_Array = rf.Log_Array[:args.PrevLogIndex+1]
// }
// // to d
// rf.Log_Array = append(rf.Log_Array, args.Entries[args.PrevLogIndex+1:]...)
// if args.Leader_Commit > rf.Committed_Idx {
// rf.Committed_Idx = int(math.Min(float64(args.Leader_Commit), float64(len(rf.Log_Array))))
// }
rf.ToFollower()
rf.ResetElection()
rf.CurrentTerm = args.Leader_Term
reply.Success = true
//TODO:2,3,4,5 in the paper
}
基于分布式或者高竞争状况下使用锁和结构的思考
锁
通过阅读官网给出的tips,如果term++和转变state如果不应该让别人看见的话,我们就要把这整个段上锁,这有点像事务一致性,不暴露中间状态。同时rpc和channel请求时要解锁,防止互相等待对方的锁,或者阻塞过长,导致timer时间不符合预期等情况。
结构
go可以通过锁和共享数据,或者channel来传递信息,我们尽量分开任务在协程里面,这样减少了超时时间的花费。不要用ticker(最简单的方法是使用 time.Sleep() 和一个小常量参数来驱动定期检查。不要使用 time.Ticker 和 time.Timer;它们很难正确使用。)
同时我们要注意到 网络可以延迟 RPC 和 RPC 回复,并且当 您发送并发 RPC 时,网络可以重新排序请求和 回复。可能后面的实验会遇到这些问题
正确性
测试了300次,也可能还是有bug:( 。。。。。更新:把ticker改成了time来循环监测,同时把heart和elect分成两个部分go协程,降低了延迟。这样符合了hint要求,对架构更放心,并且测试了600次。不要用ticker(最简单的方法是使用 time.Sleep() 和一个小常量参数来驱动定期检查。不要使用 time.Ticker 和 time.Timer;它们很难正确使用。)
参考资料:https://pdos.csail.mit.edu/6.824/labs/raft-locking.txt
https://pdos.csail.mit.edu/6.824/labs/raft-structure.txt
Students' Guide to Raft :: Jon Gjengset
MIT6.824-2021/lab2.md at master · OneSizeFitsQuorum/MIT6.824-2021 · GitHub