这部分主要实现附加日志部分,即一致性操作。主要涉及到完善Start()函数,完善附加日志请求AppendEntries RPC和回复AppendEntriesReply RPC结构,并实现附加日志过程函数。
根据论文来完善AppendEntries结构:
附加日志请求AppendEntries RPC:
由领导人负责用来复制日志指令;也会用作heartbeat
参数 | 含义 |
---|---|
term | 领导人的任期 |
leaderId | 领导人的Id,以便于跟随者重定向请求 |
prevLogIndex | 新的日志条目紧随之前的索引值 |
prevLogTerm | prevLogIndex条目的任期号 |
entries[] | 准备存储的日志条目(表示心跳时为空;一次性发送多个是为了提高效率) |
leaderCommit | 领导人已经提交的日志索引值 |
type AppendEntries struct {
Term int
LeaderId int
PrevLogIndex int
PrevLogTerm int
Entries []LogEntries
LeaderCommit int
}
附加日志请求回复AppendEntriesReply RPC:
参数 | |
---|---|
term | 当前的任期号,用于领导人去更新自己 |
success | 跟随者包含了匹配上prevLogIndex和prevLogTerm的日志时为真 |
type AppendEntriesReply struct {
Term int
VoteGranted bool
}
由于Start()的功能是将接收到的客户端命令追加到自己的本地log,然后给其他所有peers并行发送AppendEntries RPC来迫使其他peer也同意领导者日志的内容,在收到大多数peers的已追加该命令到log的肯定回复后,若该entry的任期等于leader的当前任期,则leader将该entry标记为已提交的(committed),提升(adavance)commitIndex到该entry所在的index,并发送ApplyMsg消息到ApplyCh,相当于应用该entry的命令到状态机。
func (rf *Raft) Start(command interface{}) (int, int, bool) {
index := -1
term := -1
isLeader := true
//leader将客户端command作为新的entry追加到本地log
term, isLeader = rf.GetState()
rf.mu.Lock()
defer rf.mu.Unlock()
if isLeader {
index = rf.getLastIndex() + 1
entry := LogEntries{
LogTerm: term,
LogCommand: command,
LogIndex: index,
}
rf.log = append(rf.log, entry)
rf.persist()
}
return index, term, isLeader
}
3.1、多数接收者追加日志成功后的状态指定
对于startElection(),只有为Candidate状态且获得大多数投票,才能变为leader。
对于Start(),只有为Leader状态且已将entry复制到了大多数peers,才能提升commitIndex。
因为是为每个peer创建一个goroutine发送RPC并进行RPC回复的处理,根据回复实时统计得到肯定回复的数量。可能出现在给其中一个peer发送RPC时,因为该peer的任期比leader更高,它拒绝了candidate或leder的RPC请求,candidate或leader被拒绝后,切换到Follower状态。而与此同时,或者在此之后,该过时的candidaet或leader(已经切换到follwer),收到了其他peers的大多数的肯定回复,如果这时不对candidate或leader的状态加以判断,那么该过时的candidate或leader因为满足了多数者条件,采取进一步的动作(对于过时的candidate是变为leader,对于过时的leader来说是提升commitIndex),这显然是错误的!所以必须在达到多数者条件时检查下是否仍处于指定状态,如果是,才能进一步执行相关动作。
3.2、 一致性检查冲突的解决
AppendEntries RPC请求处理的一个重要内容就是进行一致性检查,如果一致性检查失败,就会将AppenEntriesReply中的参数success置为false,以便leader递减nextIndex并重试。最终一致性检查通过,如果存在冲突的条目,则会删除冲突的条目并替换为AppendEntriesArgs中的entries。
在Raft论文中指出:
许多人的另一个问题(通常在解决了上面那个问题后马上遇到)是当收到心跳后,它们会在prevLogIndex之后(following prevLogIndex)截断(truncate)跟随者的日志,然后追加AppendEntries参数中包含的任何条目。这也是不正确的。我们可以再次转向图2:
If an existing entry conflicts with a new one(same index but different terms), delete the existing entry and all that follow it.
这里的If至关重要。如果跟随者拥有领导者发送的所有条目,则跟随者一定不能(MUST NOT)截断其日志。领导者发送的条目之后的任何内容(any elements following the entries sent by the leader)必须(MUST)保留。
这是因为我们可能从领导者那里收到过时的(outdated)AppendEntries RPC,截断日志意味着“收回(taking back)”这些我们可能已经告诉领导者它们在我们的日志中的条目。
所以判断folower日志是否和leader的log存在冲突的方法就是检查AppendEntriesArgs的entries参数中包含的条目是否都已经存在于follower的log中,如果都存在,则不存在冲突。如果在追加的过程中追加的前半部分存在,后半部分不同,则follower后追加的后半部分截断追加日志内容。
3.3、 减少被拒绝的AppendEntries RPC的次数
如果需要的话,算法可以通过减少被拒绝的追加条目(AppendEntries) RPC的次数来优化。例如,当追加条目(AppendEntries) RPC的请求被拒绝时,跟随者可以包含冲突条目的任期号和它自己存储的那个任期的第一个索引值。借助这个信息,领导者可以减少nextIndex来越过该任期内的所有冲突的日志条目;这样就变为每个任期需要一条追加条目(AppendEntries) RPC而不是每个条目一条。
这么做之所以有效的原因在于AppendEntriesArgs的entrires携带的日志条目可以在冲突点之前,但不能在冲突点之后。也就是说,如果任期2的某个条目是冲突点,但该条目不是任期2的第一个条目,按照论文中给出的优化处理,entries中将包含从任期2的第一个条目到该冲突点之前的所有条目,而这些条目本身是和leader的log中对应位置的条目是匹配的,但是截断这些条目并替换为leader中一样的条目,仍然是正确的。
而对于leader的AppendEntries RPC回复处理来说,得到了AppendEntriesReply的conflictTerm和conflictIndex参数,需要进行进一步的处理。
首先,conflictIndex一定小于nextIndex,因为一致性检查是从prevLogIndex(nextIndex-1)处查看的,所以conflictTerm至多是prevLogIndex对应entry的任期,而conflictIndex作为conflictTerm的第一次出现的索引,至多等于prevLogIndex,所以必然小于nextIndex。
接着判断leader的log中conflictIndex处entry的任期是否等于conflictTerm,如果等于,说明在该索引处,leader与该peer的日志已经是匹配的,可以直接将nextIndex置为conflictIndex+1,否则leader应该也采取上面的优化手段,递减conflictIndex,直到其为该任期的第一个条目的索引,接着也是将nextIndex置为conflictIndex+1,再次发送AppendEntries RPC进行重试。并且前一种情况下,接下来的这次重置将通过一致性检查,而第二种情况则不一定,而且还有可能出现“活锁”。
当节点选举成为leader之后,需要调用broadcastHeartbeat()用来广播发送心跳或者追加日志。
func (rf *Raft) broadcastHeartbeat() {
for i := 0; i < len(rf.peers); i++ {
if i == rf.me {
continue
}
go func(index int) {
for {
rf.mu.Lock()
if rf.state != Leader {
rf.mu.Unlock()
return
}
nextIndex := rf.nextIndex[index]
entries := make([]LogEntries, 0)
entries = append(entries, rf.log[nextIndex:]...)
args := AppendEntries{
Term: rf.currentTerm,
LeaderId: rf.me,
PrevLogIndex: rf.getPrevLogIndex(index),
PrevLogTerm: rf.getPrevLogTerm(index),
Entries: entries,
LeaderCommit: rf.commitIndex,
}
rf.mu.Unlock()
reply := &AppendEntriesReply{}
ok := rf.sendAppendEntries(index, args, reply)
if !ok {
DPrintf("sendAppendEntries fail,request args Term:%d, LeaderId:%d ", args.Term, args.LeaderId)
return
}
rf.mu.Lock()
if rf.state != Leader || rf.currentTerm != args.Term {
rf.mu.Unlock()
return
}
rf.mu.Unlock()
if reply.VoteGranted {
rf.mu.Lock()
rf.matchIndx[index] = args.PrevLogIndex + len(args.Entries)
rf.nextIndex[index] = rf.matchIndx[index] + 1
rf.mu.Unlock()
/*
当前任期内的日志大多数人表决成功,则领导人节点提交
*/
rf.advanceCommitIndex()
return
} else {
if reply.Term > rf.currentTerm {
rf.coverToFollow(reply.Term)
return
} else {
nIndex := rf.getNextIndex(*reply, nextIndex)
//更新leader为该peer保存的nextIndex
rf.nextIndex[index] = nIndex
}
}
}
}(i)
}
}
AppendEntries RPC的请求处理实现:
func (rf *Raft) AppendEntries(args AppendEntries, reply *AppendEntriesReply) {
defer rf.persist()
success := false
conflictTerm := 0
conflictIndex := 0
/*
如何来自leader的Term大于自身的currentTerm时,表明目前存在leader且自己的任期是过时的,需要切换成Follow状态。
*/
if args.Term > rf.currentTerm {
rf.coverToFollow(args.Term)
}
rf.mu.Lock()
defer rf.mu.Unlock()
if args.Term == rf.currentTerm {
/*
收到AppendEntries RPC,说明leader存在,切换成Follow状态
*/
rf.state = Follow
chanSet(rf.appendEntriesCh)
if args.PrevLogIndex > rf.getLastIndex() {
conflictIndex = len(rf.log)
} else {
prevLogTerm := rf.log[args.PrevLogIndex].LogTerm
if prevLogTerm != args.PrevLogTerm {
conflictTerm = rf.log[args.PrevLogIndex].LogTerm
for i := 1; i < len(rf.log); i++ {
if rf.log[i].LogTerm == conflictTerm {
conflictIndex = i
break
}
}
}
if args.PrevLogIndex == 0 || (args.PrevLogTerm == prevLogTerm) {
success = true
index := args.PrevLogIndex
for i := 0; i < len(args.Entries); i++ {
index += 1
if index > rf.getLastIndex() {
rf.log = append(rf.log, args.Entries[i:]...)
break
}
if rf.log[index].LogTerm != args.Entries[i].LogTerm {
entries := make([]LogEntries, 0)
copy(entries, args.Entries)
rf.log = append(rf.log[:index], entries...)
}
}
DPrintf("Server(%v=%v) term:%v, AppendEntries Success", args.LeaderId, rf.me, rf.currentTerm)
if args.LeaderCommit > rf.commitIndex {
rf.commitIndex = intMin(args.LeaderCommit,rf.getLastIndex())
}
}
}
}
rf.applyLogs()
reply.Term = rf.currentTerm
reply.VoteGranted = success
reply.ConflictIndex = conflictIndex
reply.conflictTerm = conflictTerm
return
}