- 当其他机器B接受到这个vote投票请求,如果发现Candidate的Term小于自己,则否决,并返回自己的Term。
- 如果机器B发现Candidate的Term大于自己,就更新Term,并且重置投给谁votedFor为A。
- 如果机器B发现当前Term已经给别人投票过,就否决。
- 如果 Candidate的日志不是最新,就否决。
- 赞成
跟随者只响应来自其他服务器的请求。如果跟随者接收不到消息,那么他就会变成候选人并发起一次选举。获得集群中大多数选票的候选人将成为领导者。领导人一直都会是领导人直到自己宕机了。
// create a new Raft server instance:
rf := Make(peers, me, persister, applyCh)
// start agreement on a new log entry:
rf.Start(command interface{}) (index, term, isleader)
// ask a Raft for its current term, and whether it thinks it is leader
rf.GetState() (term, isLeader)
// each time a new entry is committed to the log, each Raft peer
// should send an ApplyMsg to the service (or tester).
type ApplyMsg
一个服务通过调用Make(peers,me,…)创建一个Raft端点。peers参数是通往其他Raft端点处于连接状态下的RPC连接。me参数是自己在端点数组中的索引。
首先根据论文来实现Raft结构体,应该包含如下信息:
所有服务器上持久存在的状态
参数 | 含义 |
---|---|
currentTerm | 服务器最后一次知道的任期号(初始化为 0,持续递增) |
votedFor | 在当前获得选票的候选人的 Id |
log[] | 日志条目集;每一个条目包含一个用户状态机执行的指令和收到时的任期号 |
所有服务器上经常变的状态
字段 | 含义 |
---|---|
commitIndex | 已知的最大的已经被提交的日志条目的索引值 |
lastApplied | 最后被应用到状态机的日志条目索引值(初始化为 0,持续递增 |
在领导人里经常改变的状态 (选举后重新初始化)
字段 | 含义 |
---|---|
nextIndex[] | 对于每一个服务器,需要发送给他的下一个日志条目的索引值(初始化为领导人最后索引值加一) |
matchIndex[] | 对于每一个服务器,已经复制给他的日志的最高索引值 |
Raft结构
type Raft struct {
mu sync.Mutex
peers []*labrpc.ClientEnd
persister *Persister
me int // index into peers[]
// Your data here.
// Look at the paper's Figure 2 for a description of what
// state a Raft server must maintain.
currentTerm int
votedFor int
log []logEntries
commitIndex int
lastApplied int
nextIndex []int
matchIndx []int
state Role
leader int
appendEntriesCh chan bool
voteGrantedCh chan bool
leaderCh chan bool
applyMsgCh chan ApplyMsg
heatbeatTimeout time.Duration
electionTimeout time.Duration
}
通过Make函数初始化创建一个Raft对象
一个服务通过调用Make(peers,me,…)创建一个Raft端点。peers参数是通往其他Raft端点处于连接状态下的RPC连接,me参数是自己在端点数组中的索引。
所有服务器:
如果接收到的 RPC 请求中,任期号T > currentTerm,那么就令 currentTerm 等于 T,并切换状态为跟随者。
跟随者(Follow):
候选人(Candidate):
在转变成候选人后就立即开始选举过程
领导人(Leader):
// create a new Raft server instance:
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.
rf.currentTerm = STARTTERM
rf.votedFor = VOTENULL
rf.log = make([]logEntries,0)
rf.log = append(rf.log,logEntries{0,0})
rf.commitIndex = 0
rf.lastApplied = 0
rf.state = Follow
rf.nextIndex = make([]int,len(rf.peers))
rf.matchIndx = make([]int,len(rf.peers))
rf.appendEntriesCh = make(chan bool,1)
rf.voteGrantedCh = make(chan bool,1)
rf.leaderCh = make(chan bool,1)
rf.applyMsgCh = applyCh
rf.heatbeatTimeout = time.Duration(HEATBEATTIMEOUT) * time.Millisecond
// initialize from state persisted before a crash
rf.readPersist(persister.ReadRaftState())
go func(){
for {
rf.mu.Lock()
state := rf.state
rf.mu.Unlock()
electionTimeout := HEATBEATTIMEOUT * 2 + rand.Intn(HEATBEATTIMEOUT)
rf.mu.Lock()
rf.electionTimeout = time.Duration(electionTimeout) * time.Millisecond
rf.mu.Unlock()
switch state {
case Follow:
select {
/*
如果在超过选举超时时间的情况之前都没有收到领导人的心跳,或者是候选人请求投票的,就自己变成候选人
*/
case <- rf.appendEntriesCh:
case <- rf.voteGrantedCh:
case <- time.After(rf.electionTimeout):
rf.coverToCandidate()
}
case Candidate:
go rf.leaderElection()
select {
case <- rf.appendEntriesCh:
case <- rf.voteGrantedCh:
case <- rf.leaderCh:
case <- time.After(rf.electionTimeout):
rf.coverToCandidate()
}
case Leader:
go rf.broadcastHeartbeat()
time.Sleep(rf.heatbeatTimeout)
}
}
}()
return rf
}
请求投票 RPC和返回参数
要开始一次选举过程,跟随者先要增加自己的当前任期号并且转换到候选人状态。然后他会并行的向集群中的其他服务器节点发送请求投票的 RPCs 来给自己投票。所以先要实现RequestVoteArgs和RequestVoteReply结构体,即请求投票RPC的参数和反馈结果。
请求投票RPC参数RequestVoteArgs
由候选人负责调用用来征集选票
参数 | 含义 |
---|---|
term | 候选人的任期号 |
candidateId | 请求选票的候选人的 Id |
lastLogIndex | 候选人的最后日志条目的索引值 |
lastLogTerm | 候选人最后日志条目的任期号 |
type RequestVoteArgs struct {
// Your data here.
Term int
CandidateId int
LastLogIndex int
LastLogTerm int
}
请求投票RPC参数返回值RequestVoteReply
参数 | 含义 |
---|---|
term | 当前任期号,以便于候选人去更新自己的任期号 |
voteGranted | 候选人赢得了此张选票时为真 |
type RequestVoteReply struct {
// Your data here.
Term int
VoteGranted bool
}
发起投票实现
为了提高性能,需要并行发送RPC。可以迭代peers,为每一个peer单独创建一个goroutine发送RPC。Raft Structure Adivce建议:
在同一个goroutine里进行RPC回复(reply)处理是最简单的,而不是通过(over)channel发送回复消息。
所以,为每个peer创建一个gorotuine同步发送RPC并进行RPC回复处理。另外,为了保证由于RPC发送阻塞而阻塞的goroutine不会阻塞RequestVote RPC的投票统计,需要在每个发送RequestVote RPC的goroutine中实时统计获得的选票数,达到多数后就立即切换为Leader状态,并立即发送一次心跳,阻止其他peer因选举超时而发起新的选举。而不能在等待所有发送goroutine处理结束后再统计票数,这样阻塞的goroutine,会阻塞领导者的产生,且在阻塞的过程中,容易使得达到了选举超时时间,会进入新的一个周期再次进行发出投票请求。
func (rf *Raft) leaderElection() {
rf.mu.Lock()
if rf.state != Candidate {
rf.mu.Unlock()
return
}
rf.mu.Unlock()
rf.mu.Lock()
args := RequestVoteArgs{
Term:rf.currentTerm,
CandidateId:rf.me,
LastLogIndex:rf.getLastIndex(),
LastLogTerm:rf.getLastTerm(),
}
rf.mu.Unlock()
winThreshold := int64(len(rf.peers)/2 + 1)
voteCount := int64(1)
for i := 0; i < len(rf.peers); i++ {
if i == rf.me {
continue
}
go func(index int,args RequestVoteArgs) {
reply := &RequestVoteReply{}
ok := rf.sendRequestVote(index,args,reply)
if !ok {
rf.mu.Lock()
defer rf.mu.Unlock()
DPrintf("sendRequestVote fail,request term:%d,candidate id: %d,",args.Term,args.CandidateId)
return
}
rf.mu.Lock()
if args.Term != rf.currentTerm {
rf.mu.Unlock()
return
}
rf.mu.Unlock()
if reply.VoteGranted == false {
if reply.Term > rf.currentTerm {
rf.coverToFollow(reply.Term)
}
}else{
atomic.AddInt64(&voteCount,1)
if atomic.LoadInt64(&voteCount) >= winThreshold && rf.state == Candidate {
DPrintf("server %d win the vote",rf.me)
rf.coverToLeader()
chanSet(rf.leaderCh)
}
}
}(i,args)
}
}
接收者实现:
如果term < currentTerm返回 false。
如果 votedFor 为空或者就是 candidateId,并且候选人的日志也自己一样新,那么就投票给他。
func (rf *Raft)RequestVote(args RequestVoteArgs, reply *RequestVoteReply) {
// Your code here.
voteGranted := false
/*
如果term < currentTerm返回 false
*/
rf.mu.Lock()
if rf.currentTerm > args.Term {
reply.Term = rf.currentTerm
reply.VoteGranted = voteGranted
rf.mu.Unlock()
return
}
rf.mu.Unlock()
/*
如果接收到的 RPC 请求中,任期号T > currentTerm,那么就令 currentTerm 等于 T,并切换状态为跟随者
T > currentTerm时,当此服务器状态为Candidate时,在发起选举时给自己投票,会将voteFor设置为自己的id,切换到Follow时,重置voteFor。
T > currentTerm时,当次服务器状态为Follow时,则此时服务器没有给其他节点投过票,如果投过票则currentTerm更新为最新的,此时重置voteFor也没问题。
*/
if rf.currentTerm < args.Term {
rf.coverToFollow(args.Term)
}
/*
如果 votedFor 为空或者就是 candidateId,并且候选人的日志也自己一样新,那么就投票给他
*/
rf.mu.Lock()
if (rf.votedFor == VOTENULL || rf.votedFor == args.CandidateId) && ((rf.currentTerm <= args.Term) || ((rf.getLastIndex() <= args.LastLogIndex) && (rf.currentTerm== args.Term)) ) {
voteGranted = true
rf.votedFor = args.CandidateId
rf.state = Follow
rf.leader = args.CandidateId
chanSet(rf.voteGrantedCh)
}
reply.VoteGranted = voteGranted
reply.Term = rf.currentTerm
rf.mu.Unlock()
}