# 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. 流程介绍

image

  • 这个流程图是简化后的、没有任何差错的情况下会发生的,实际上在各种情况下,server可能会在三种状态间互相转换。具体转换规则如下图:

image

  • 所有服务器在任何情况都要遵守的规则如下
    image
  1. (ApplyLoop)如果commitIndex大于lastApplied,就说明有一部分Leader已经认可的指令还没有apply,应用至状态机
  2. 任何时候,如果任何一个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

posted @ 2024-05-10 22:23  AikNr  阅读(6)  评论(0编辑  收藏  举报