MIT 6.824 分布式课程Lab2 2B 日志追加实现

这部分主要实现附加日志部分,即一致性操作。主要涉及到完善Start()函数,完善附加日志请求AppendEntries RPC和回复AppendEntriesReply RPC结构,并实现附加日志过程函数。

一、AppendEntries和AppendEntriesReply结构

根据论文来完善AppendEntries结构:
MIT 6.824 分布式课程Lab2 2B 日志追加实现_第1张图片
附加日志请求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()函数实现

由于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的请求处理实现:

  • 如果 term < currentTerm 就返回 false。
  • 如果日志在 prevLogIndex 位置处的日志条目的任期号和 prevLogTerm 不匹配,则返回 false 。
  • 如果已经已经存在的日志条目和新的产生冲突(相同偏移量但是任期号不同),删除这一条和之后所有的 ,附加任何在已有的日志中不存在的条目。
  • 如果 leaderCommit > commitIndex,令 commitIndex 等于 leaderCommit 和 新日志条目索引值中较小的一个。

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
}

你可能感兴趣的:(分布式系统)