# Mit 6.824 Raft实验 2A 2B
Mit 6.824 Raft实验 2A 2B
Author: Minghao Zhou
这个项目写了好久,从一点也看不懂开始,到最后debug就和回家一样自然,成就感还是很足的哈哈。看大佬的架构和代码感觉学到了很多,故在此记录一下。
\src\raft> go
test -run 2A
Test (2A): initial election ...
... Passed -- 3.1 3 96 24384 0
Test (2A): election after network failure ...
... Passed -- 4.5 3 210 40306 0
Test (2A): multiple elections ...
... Passed -- 5.5 7 924 168376 0
PASS
ok 6.5840/raft 13.351s
\src\raft> go test -run 2B
Test (2B): basic agreement ...
... Passed -- 0.6 3 16 4058 3
Test (2B): RPC byte count ...
... Passed -- 1.6 3 48 112838 11
Test (2B): test progressive failure of followers ...
... Passed -- 4.9 3 202 41134 3
Test (2B): test failure of leaders ...
... Passed -- 4.9 3 298 64009 3
Test (2B): agreement after follower reconnects ...
labgob warning: Decoding into a non-default variable/field Term may not work
... Passed -- 4.2 3 139 33129 7
Test (2B): no agreement if too many followers disconnect ...
... Passed -- 3.5 5 322 61842 4
Test (2B): concurrent Start()s ...
... Passed -- 0.7 3 20 5070 6
Test (2B): rejoin of partitioned leader ...
... Passed -- 4.0 3 223 50502 4
Test (2B): leader backs up quickly over incorrect follower logs ...
... Passed -- 17.4 5 2500 1956440 102
Test (2B): RPC counts aren't too high ...
... Passed -- 2.3 3 70 17938 12
PASS
ok 6.5840/raft 44.311s
0. 参数介绍
论文中给出了很多必要的参数,每一个参数都需要使用。这里我给出对论文中说明的一些代码细节的补充
State
变量 | 说明 |
---|---|
currentTerm | 目前轮次 |
votedFor | 给谁投票,主要用于记录当前伦次是否已经投过票了 |
log | Entry结构的数组,用于存放当前Server的所有指令;log[0]存放nil |
commitIndex | 目前已提交的最新Entry的index,对于Follower来说,这个变量将和Leader的同步;对于Leader来说,将在大部分机器都同意后成功commit,更新至len(rf.log) - 1 |
lastApplied | 目前提交到状态机的最新Entry的index |
nextIndex[] | 一个只有当Leader的时候才会启用的数组,记录了每个机器的下一个指令放置的位置 |
matchIndex[] | 同上,记录了每个机器已经和leader同步了的最新的指令的坐标。 |
AppendEntries RPC
这个RPC用于同步指令,随时钟启动。其中的Entries[]变量给出了这次需要同步的指令,如果这个变量为空(nil),则这个包被称作心跳包,用于维持leader的身份,将阻止peer开始选举。
需要注意的是,RPC中所有的变量需要大写字母开头,和论文给出的变量名不同。
Arg
变量 | 说明 |
---|---|
Term | Leader的term,用于接收信号的peer识别这是不是一个靠谱的leader |
LeaderId | Leader的ID |
PrevLogIndex | Leader已有的最新的指令的位置(非本次传输的指令) |
PrevLogTerm | Leader已有的最新指令的Term,如果这个Term不匹配,说明本机和Leader的指令不是一个时期的。 |
Entries[] | Entry结构体的数组,存放若干个Entry,为本次需要同步的指令。 |
LeaderCommit | Leader已经认可的最新可以commit的位置,如果比自己的新就同步。 |
Reply
变量 | 说明 |
---|---|
Term | 返回给leader自己的term,如果比leader的新就说明leader该下台了 |
Success | 本次是否同步成功。如果不成功可能是日志冲突之类的 |
RequestVote RPC
这个RPC用于candidate向别的peer请求投票。里面的内容用于说明candidate的身份和水平(日志是否够新)
Arg
变量 | 说明 |
---|---|
Term | Candidate的term |
CandidateID | candidate的身份识别码 |
LastLogIndex | Candidate最新日志的index |
LastLogTerm | Candidate最新日志的Term |
Result (我觉得这个应该叫reply)
变量 | 说明 |
---|---|
Term | 收到request的peer的term |
VoteGranted | 是否投票给Candidate |
1. 流程介绍
- 这个流程图是简化后的、没有任何差错的情况下会发生的,实际上在各种情况下,server可能会在三种状态间互相转换。具体转换规则如下图:
- 所有服务器在任何情况都要遵守的规则如下
- (ApplyLoop)如果commitIndex大于lastApplied,就说明有一部分Leader已经认可的指令还没有apply,应用至状态机
- 任何时候,如果任何一个RPC中,对方的Term如果高于自己,就直接设置term为对方的term并转为Follower。
2. Ticker
在我写这个代码的过程中,我最迷茫的是这个ticker该怎么写,一开始写了一份很烂的代码就是源于一个不合理的ticker。后来网上看别人的代码,最后选了这个结构:
func (rf *Raft) ticker() {
for !rf.killed() {
// Your code here (2A)
// Check if a leader election should be started.
// pause for a random amount of time between 50 and 350
// milliseconds.
select {
case <-rf.heartbeatTimer.C:
DPrintf("server %d [%s] receive heartbeat timer\n", rf.me, rf.role)
if rf.role == Leader {
DPrintf("server %d [Leader] receive heartbeat timer, start heartbeat\n", rf.me)
rf.HeartBeat()
rf.heartbeatTimer.Reset(rf.heartBeatInterval)
}
case <-rf.electionTimer.C:
rf.startElection()
}
}
}
- 其中每次触发完heartbeat后就重置一下heartbeat计时器,达到心跳的效果;
- 每次变成candidate或者follower就重置一下election计时器,超时了就会开始选举。
3. Make
func Make(peers []*labrpc.ClientEnd, me int,
persister *Persister, applyCh chan ApplyMsg) *Raft {
rf := &Raft{}
rf.peers = peers
rf.persister = persister
rf.me = me
// Your initialization code here (2A, 2B, 2C).
rf.role = Follower
rf.term = 0
rf.voteFor = -1
rf.heartBeatInterval = 50 * time.Millisecond
rf.electionTimer = time.NewTimer(rf.randomTimeout())
rf.heartbeatTimer = time.NewTimer(rf.heartBeatInterval)
// 2B
rf.commitIndex = 0
rf.lastApplied = 0
rf.applyCh = applyCh
rf.log = make([]Entry, 1)
// initialize from state persisted before a crash
rf.readPersist(persister.ReadRaftState())
// start ticker goroutine to start elections
go rf.ticker()
go rf.applyLoop()
return rf
}
- Make函数中包含了初始化,和开启循环的过程。我的代码选择让ticker只负责前两个循环,applyloop手动再开一个循环。
- 一个比较困惑我的地方是这个lab的说明中提示最好不要使用timer,使用sleep来进行计时,但是每次转换成candidate或follower的时候都需要重置这个计时,想半天想不明白咋搞,所以我最后还是选择了timer哈哈。
4. 小结
- 感觉有了以上内容应该不难写出整个项目,加油吧!
5. 参考网址
https://www.zhihu.com/tardis/zm/art/103849249?source_id=1005
https://thesquareplanet.com/blog/students-guide-to-raft/
https://pdos.csail.mit.edu/6.824/labs/lab-raft.html
http://thesecretlivesofdata.com/raft/#replication