前言:
虽然mit6.824这门课说不让大家把代码公布出来,因为它们觉得自己写学习到的才是最多的。我不否认它们这个观点,但当一个go实践并不多的学生开始写的时候,它会发现毫无思路。因此,对于一些想学raft并实践的学生(比如说我),是非常需要一份详细到不能再详细的代码解析的。然而,现状是网上的raft 各个part解析都是只讲重点而不注重细节,因此我认为很有必要把细节缕清楚!
参考资料:
我这里的代码是参考thu大神的,但这位小哥哥也没有详细地解释清楚,我就借花献佛,将它的代码逻辑讲明白
在实现之前,可以先参考我之前写的一些博客,包括raft动图的介绍以及论文翻译:
详见我的论文学习模块
mit 6.824 lab2 raft
内容概括:
本文主要从以下几个方面展开对parta实验的源码解析:
1.raft结构体
2.make初始化
2.5 changestate()函数的实现
3.requestvoterpc的实现
4.appendentries的实现(仅heartbeat)
5.ticker的逻辑
6.测试样例的讲解与测试结果的解析(正常选取,再选举,多节点宕机然后再进入)
preparation
一些常数的设定:
const (FOLLOWER = 0CANDIDATE = 1LEADER = 2TO_FOLLOWER = 0TO_CANDIDATE = 1TO_LEADER = 2ELECTION_TIMEOUT_MAX = 600ELECTION_TIMEOUT_MIN = 400HEARTBEAT_TIMEOUT = 100
)
设置follower、candidate、leader三个状态对应的int,以及心跳的timeout为100ms(raft实验要求1s不超过十次心跳),这样我们就要适当增加论文中150-300ms的electiontimeout的限度了,这里设置为400-600ms
raft结构体
先看看论文中的结构体描述:
type Raft struct {mu sync.Mutex peers []*labrpc.ClientEnd persister *Persister me int dead int32 currentTerm int votedFor int log []Entry getVoteNum int commitIndex int lastApplied int state int lastResetElectionTime time.Time nextIndex []int matchIndex []int applyCh chan ApplyMsg lastSSPointIndex intlastSSPointTerm int}
详细的描述可以看注释,在parta中我们不会用到后面的这几个index和term相关的属性,都是后面几个part用到的。persister也不用管,也是后面的partd才用到(用来存快照的)
我们着重看一下这个peers
peers数组我们只需要关注它们的index,其中该raft节点本身的index就是me;然后,每个raft节点中存储的peers的index所指向的实际raft节点都是一致的(他这里没有明显指出来,但我们需要知道这一点才好理解)
然后,相比论文中的state表,我们新增了state变量表示当前该raft节点的实际状态,以及它上一次重置时间的时间戳lastResetElectionTime(用来判断是否超时)
make初始化
我们在raft.go中需要提供make接口来方面tester调用make_config构建raft集群,主要的作用就是初始化:
func Make(peers []*labrpc.ClientEnd, me int,persister *Persister, applyCh chan ApplyMsg) *Raft {rf := &Raft{}rf.peers = peersrf.persister = persisterrf.me = merf.mu.Lock()rf.state = FOLLOWERrf.currentTerm = 0rf.getVoteNum = 0rf.votedFor = -1rf.lastApplied = 0rf.commitIndex = 0rf.log = []Entry{}rf.log = append(rf.log, Entry{}) rf.lastSSPointIndex = 0rf.lastSSPointTerm = 0rf.applyCh = applyChrf.mu.Unlock()rf.readPersist(persister.ReadRaftState())go rf.candidateElectionTicker()go rf.leaderAppendEntriesTicker()return rf}
很显然,前面就是各种状态的初始化。然后最后用go关键字拉起了两个协程,分别是用来控制进行定期的election选举的检查以及进行定期的leader发送心跳提醒。
changestate()函数的实现
在介绍changestate函数之前,我们先归纳一下在parta中所有状态变换的可能以及它们是否需要重置时间
- server x收到reqeustvote rpc, 且rpc中的term比当前的currentTerm要大,也就是说当前server x手上的信息不是最新的,那么server x -> follower(server x 之前可能是leader或者candidate哦),然后是不重置时间的(因为可能它就是一个follower)
- candidate自己的getvoteNum过半,选举成功,candidate -> leader, 重置时间
- candidate发现传rpc的term比reply的小, candidate -> follower, 不重置时间
- follower -> candidate 重置时间
- server x 给 candidate 投票后,x重置时间
- server x 收到leader心跳,重置时间
- leader看到更大的term,降级为follower, 重置时间
接下来介绍一下changestate函数
func (rf *Raft) changeState(howtochange int, resetTime bool) {if howtochange == TO_FOLLOWER {rf.state = FOLLOWERrf.votedFor = -1rf.getVoteNum = 0if resetTime {rf.lastResetElectionTime = time.Now()}}if howtochange == TO_CANDIDATE {rf.state = CANDIDATErf.votedFor = rf.merf.getVoteNum = 1rf.currentTerm += 1rf.lastResetElectionTime = time.Now()rf.candidateJoinElection()}if howtochange == TO_LEADER {rf.state = LEADERrf.votedFor = -1rf.getVoteNum = 0rf.nextIndex = make([]int, len(rf.peers))rf.matchIndex = make([]int, len(rf.peers))rf.lastResetElectionTime = time.Now()}
}
这个函数两个参数,一个是需要变成的state,一个是是否需要重置时间。我们可以发现只有follower才可以选择是否需要变更时间,其他的candidate和leader都是变了之后时间默认重置的,然后这主要涉及到状态的再初始化,这就不需要赘述了。然后就是candidate中涉及到的一个candidateJoinElection函数,因为candidate只可能是follower变得,因此变成candidate的同时就可以发起requestvoterpc了
candidatejoinelection函数:
这里面涉及到rpc的处理我们后面再一起讲
模拟rpc
我们先看看两个调用rpc接口的函数哈(requestvoterpc和appendentriesrpc)
func (rf *Raft) sendRequestVote(server int, args *RequestVoteArgs, reply *RequestVoteReply) bool {ok := rf.peers[server].Call("Raft.RequestVote", args, reply)return ok
}func (rf *Raft) sendAppendEntries(server int, args *AppendEntriesArgs, reply *AppendEntriesReply) bool {ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)return ok
}
这里面都把调用rpc定义成raft绑定的方法了,这里是rf发rpc,然后peers[server]收到rpc,然后args是rf发出去的参数,通过我们编写的rpc接口处理器处理后,也就是call的第一个参数,server就会根据具体情况返回对应的reply结果,然后ok如果为true就说明网络是ok的,完成了一次有效的通信。
因此,我们为了实现rpc,要定义输入输出参数的格式,以及定义rpc处理函数;然后通过ticker来调用一个协程负责向其他节点发送rpc(这里又要拉起新的协程),然后再完成后续的判断
未完待续。。。