Lab2 Raft
因为以前的版本很多细节出现问题,导致后面的lab4出现很多bug,所以对raft进行重写。
PartA
本部分需要完成Raft的leader选举和发送心跳包(log为空的AE)。
-
实验流程
-
对象设计
本次实验的主要对象有Raft,ApplyMsg,LogEntry。其中Raft代表了一个Raft对等节点,LogEntry则代表日志。
-
Raft
每次进行leader选举,每个对等节点peer需要从自身的Raft对象中读取自身的term,以及最后一条日志的信息,然后将自己的投票结果存在votedFor中。在不同的阶段,peer将自己的角色存储在role中从而执行不同的任务。每个raft对象中通过存储选举的计时器来实现定时选举。
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() // Your data here (2A, 2B, 2C). // Look at the paper's Figure 2 for a description of what // state a Raft server must maintain. //持久化变量 currentTerm int //当前任期 votedFor int //记录当前投票的服务器 logs []LogEntry //日志 commitIndex int //提交的日志编号(优化) //易失变量 lastApplied int //已应用的日志下标 nextIndex []int //follower中的同步起点,初始为lastLogIndex+1,server->Index matchIndex []int //follower已同步的索引,初始为0,server->Index //其他 currentState string //当前状态,leader,follower,candidate grantVotes int //获得的票数 //选举计时器 electionTimer *time.Timer electionTimerLock sync.Mutex //心跳计时器 heartbeatTimer *time.Timer heartbeatTimerLock sync.Mutex applyCh chan ApplyMsg //应用层的提交队列 applyCond *sync.Cond //唤醒应用消息的进程发送信息,在commit时唤醒 }
-
LogEntry
日志标号可以通过快照(2D)存储的编号加上当前日志数组的下标计算得到。type LogEntry struct { Term int //该日志所处的任期 Command interface{ } //该日志包含的命令 }
-
-
流程设计
-
初始化Raft对等节点
Raft节点启动主要需要将初始化term为0,设置为follower,并且为日志分配足够的空间,投票人初始化为未投票状态,以及重启计时器。并且初始化随机种子,用于随机选择选举计时,随机种子在初始化时选择一次就行,不用在每次随机选举时再选。
rf := &Raft{ } rf.peers = peers rf.persister = persister rf.me = me // Your initialization code here (2A, 2B, 2C). rf.currentTerm = 0 rf.votedFor = -1 rf.logs = make([]LogEntry, 0) rf.commitIndex = 0 rf.nextIndex = make([]int, len(peers)) rf.matchIndex = make([]int, len(peers)) rf.currentState = FOLLOWER rf.applyCh = applyCh rf.applyCond = sync.NewCond(&rf.mu) rand.Seed(time.Now().Unix()) rf.electionTimerLock.Lock() rf.electionTimer = time.NewTimer(time.Duration(rand.Int())%electionTimeoutInterval + electionTimeOutStart) rf.electionTimerLock.Unlock() rf.heartbeatTimerLock.Lock() rf.heartbeatTimer = time.NewTimer(heartbeatInterval) rf.heartbeatTimerLock.Unlock() // initialize from state persisted before a crash go rf.electionTicker() go rf.appendEntriesTicker() return rf
-
-
随机数补充
之前出现一个bug选举经常出现碰撞导致不能选举出leader,后来发现是因为在重置选举计时器时又重置了随机种子,go语言的随机是伪随机数,如果随机种子相等则第一个数是相同的,而后面再出现的数会不一样,所以在初始化每个peer的第一轮选举超时时间是一样的,但后面的就不一样了,而如果每次重启计时器就重置随机种子,因为第一轮超时时间相同会发生碰撞且在相同的时间发起第二轮,所以这时的重新生成的随机种子又是一样的,导致又是相同的超时时间,也就导致一直碰撞选不出leader。下面是对rand的测试。package main import ( "fmt" "math/rand" "time" ) func main() { for i := 0; i < 5; i++ { go func(x int) { rand.Seed(1) fmt.Println(x, rand.Int()) fmt.Println(x, rand.Int()) }(i) } time.Sleep(3 * time.Second) } //结果如下,可以看到第一轮是相同的,第二个就不一样了。 4 5577006791947779410 4 8674665223082153551 0 5577006791947779410 0 6129484611666145821 3 5577006791947779410 3 4037200794235010051 1 5577006791947779410 1 3916589616287113937 2 5577006791947779410 2 6334824724549167320
在当初遇到这个bug我考虑在进入选举goroutine之前打印日志查看,结果2A跑通了,而如果不打印就又会选举不出来,后来猜想是因为多个goroutine调用fmt函数导致每个peer执行选举函数有了先后顺序,先打印先执行选举的可以成为leader,即使超时时间相同,定时器几乎同时返回,但是由于在启动选举之前共同调用fmt会发生冲突,就会强制有一个先后的顺序,在后面的等待前面的fmt打印的时候,可能更前一个的peer已经发来了投票请求。为了验证这个想法,我直接把重置的超时时间设为一个定值,然后在超时调用选举函数前加上一个fmt,然后测试2A还是能够成功。
//将重置选举时间改为定时长度 func (rf *Raft) resetElectionTimer() { rf.electionTimer.Reset(electionTimeOutStart) }
//在开始选举前打印一个. func (rf *Raft) electionTicker() { for rf.killed() == false { <-rf.electionTimer.C fmt.Print(".") go rf.startElection() rf.resetElectionTimer() } }
2A测试通过
Test (2A): initial election ... ............ ... Passed -- 3.7 3 62 15244 0 Test (2A): election after network failure ... ............................................. ... Passed -- 7.1 3 186 33678 0 Test (2A): multiple elections ... ......................................................................... ... Passed -- 7.1 7 816 145568 0 PASS ok 6.824/raft 18.139s
-
leader选举
计时器超时后就会进入选举
func (rf *Raft) electionTicker() { for rf.killed() == false { <-rf.electionTimer.C go rf.startElection() rf.resetElectionTimer() } }
如果自身就是leader直接退出选举过程,否则需要进入竞选人candidater。因为需要修改raft的信息所以需要在执行前上锁,防止两个peer在RPC通信时互相持有锁导致死锁。
rf.mu.Lock() //注意RPC调用前要释放锁防止多个拥有锁的peer之间形成死锁 //这里能够使用defer释放是因为这里的rpc都使用的是开辟新的goroutine异步调用,所以主函数可以快速释放 defer rf.mu.Unlock() if rf.currentState != LEADER { //竞选 }
开始选举首先需要将投票给自己,并且获得的票数也加一。然后将任期加一作为竞选Leader的新一轮term,然后准备发起新一轮选举的RPC请求,请求包含了新的term,自己的id,以及自己的最后的一个日志下标和term,这两个信息主要是为了确保竞选成功的peer拥有最新的日志(term最大或者term相同但是日志更长)
rf.currentState = CANDIDATE rf.votedFor = rf.me rf.currentTerm++ rf.persist() //DPrintf("[StartElection] Server:%d In term:%d try to elect", rf.me, rf.currentTerm) rf.grantVotes = 1 args := RequestVoteArgs{ Term: rf.currentTerm, CandidateId: rf.me, LastLogIndex: rf.getLastIndex(), LastLogTerm: rf.getLastTerm(), }
通过RPC发送RequestVote给各个对等点,这里采用开启多个goroutine异步发送,加快效率和减少阻塞。然后根据接收结果进行下一步的处理。
for server := range rf.peers { if server == rf.me { continue } go func(server int