etcd系列-----raft协议:消息处理(一)

消息处理

正如前面分析newRaft()函数时所看到的,当节点的raft实例创建完成之后,当前节点处于Follower状态。为了方便介绍,这里假设集群为新建集群,且集群启动后所有节点都会处于Follower状态。另外,集群中所有节点的WAL日志文件等也都是空的, 所以WAL日志重放也不会得到任何信息,节点的初始Term 值也都是0。另外,我们还假设当前集群开启了Pre Vote模式以及CheckQuorum模式。


1、MsgHup消息

集群启动一段时间之后,会有一个Follower节点的选举计时器超时, 此时就会创建MsgHup消息(其中Tenn为0)井调用raft.Step()方法。raft.Step()方法是raft模块处理各类消息的入口。raft.Step()方法主要分为两部分: 第一部分是根据Term值对消息进行分类处理,第二部分是根据消息的类型进行分类处理。raft.Step()方法中相关的代码片段如下所示。

func (r *raft) Step(m pb.Message) error {
	// Handle the message term, which may result in our stepping down to a follower.
	switch {//首先根据消息的Term值进行分类处理
	case m.Term == 0://对本地消息并没有做什么处理,这里介绍的MsgHup消息Term为0,就是本地消息的一种;后面介绍的MsgProp消息和MsgReadindex消息也是本地消息,其Term也是o
		// local message
	case m.Term > r.Term:

	case m.Term < r.Term:
		
	}

	switch m.Type {//根据Message的Type进行分类处理
	case pb.MsgHup:
		if r.state != StateLeader {//只有非Leader状态的节点才会处理MsgHup消息
		//获取raftLog中已捉交但未应用( lip applied~committed) 的Entry记录
			ents, err := r.raftLog.slice(r.raftLog.applied+1, r.raftLog.committed+1, noLimit)
			if err != nil {
				r.logger.Panicf("unexpected error getting unapplied entries (%v)", err)
			}
			//检测是否有未应用的EntryConfChange记录,如果有就放弃发起选举的机会
			if n := numOfPendingConf(ents); n != 0 && r.raftLog.committed > r.raftLog.applied {
				r.logger.Warningf("%x cannot campaign at term %d since there are still %d pending configuration changes to apply", r.id, r.Term, n)
				return nil
			}
           //检测当前集群是否开启了PreVote模式,如采开启了,则允切换到调用raft.campaign()方法切换当前节点的角色, 发起PreVote
			r.logger.Infof("%x is starting a new election at term %d", r.id, r.Term)
			if r.preVote {
				r.campaign(campaignPreElection)
			} else {
				r.campaign(campaignElection)
			}
		} else {
			r.logger.Debugf("%x ignoring MsgHup because already leader", r.id)
		}

	case pb.MsgVote, pb.MsgPreVote://对MsgVote和MsgPreVote消息的处理(
		
	default://对于其他类型消息的处理
		r.step(r, m)
	}
	return nil
}

在raft.campaign()方法中除了完成状态切换还会向集群中的其他节点发送相应类型的消息,例如,如果当前Follower节点要切换成PreCandidate状态,则会发送MsgPreVote消息。

Follower 节点在选举计时器超时的行为: 首先它会通过tickElection()创建MsgHup消息并将其交给raft.Step()方法进行处理; raft.Step()方法会将当前Follower节点切换成PreCandidate状态, 然后创建MsgPreVote类型的消息, 最后将该消息追加到raft.msgs字段中, 等待上层模块将其发迭出去。

2、MsgPreVote消息

当集群中其他节点(此时集群中其他节点都处于Follower状态〉收到MsgPreVote(其Term字段值为1 )消息之后, 经过网络层处理及相关验证之后, 最终也会调用raft.Step()方法进行处理。下面我们回到raft.Step()方法中,分析其中关于MsgPreVote消息处理的相关代码:

func (r *raft) Step(m pb.Message) error {
	// Handle the message term, which may result in our stepping down to a follower.
	switch {
	case m.Term == 0:
	case m.Term > r.Term://在上这场景中,当收到MsgPreVote消息(Term字段为1)时,集群中的其他Follower节点的Term位都为0
		if m.Type == pb.MsgVote || m.Type == pb.MsgPreVote {//这里只对MsgVote和MsgPreVote两种类型消息进行处理
		//下面通过一系列条件判断当前节点是否参与此次选举(或预选),其中主妥检测集群是否开启了CheckQuorum模式、当前节点是否有已知的Lead节点,以及其选举计时器的时间
			force := bytes.Equal(m.Context, []byte(campaignTransfer))
			inLease := r.checkQuorum && r.lead != None && r.electionElapsed < r.electionTimeout
			if !force && inLease {
				// If a server receives a RequestVote request within the minimum election timeout
				// of hearing from a current leader, it does not update its term or grant its vote
				r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] ignored %s from %x [logterm: %d, index: %d] at term %d: lease is not expired (remaining ticks: %d)",
					r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), r.Vote, m.Type, m.From, m.LogTerm, m.Index, r.Term, r.electionTimeout-r.electionElapsed)
				return nil
			}
		}
		switch {
		case m.Type == pb.MsgPreVote:
			// Never change our term in response to a PreVote
		case m.Type == pb.MsgPreVoteResp && !m.Reject:
			// We send pre-vote requests with a term in our future. If the
			// pre-vote is granted, we will increment our term when we get a
			// quorum. If it is not, the term comes from the node that
			// rejected our vote so we should become a follower at the new
			// term.
		default:
			r.logger.Infof("%x [term: %d] received a %s message with higher term from %x [term: %d]",
				r.id, r.Term, m.Type, m.From, m.Term)
			if m.Type == pb.MsgApp || m.Type == pb.MsgHeartbeat || m.Type == pb.MsgSnap {
				r.becomeFollower(m.Term, m.From)
			} else {
				r.becomeFollower(m.Term, None)
			}
		}
	switch m.Type {
	case pb.MsgHup:
	
	case pb.MsgVote, pb.MsgPreVote:
		if r.isLearner {
			// TODO: learner may need to vote, in case of node down when confchange.
			r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] ignored %s from %x [logterm: %d, index: %d] at term %d: learner can not vote",
				r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), r.Vote, m.Type, m.From, m.LogTerm, m.Index, r.Term)
			return nil
		}
		//当前节点在参与预选时,会综合下面几个条件决定是否投票(在Raft协议的介绍中也捉到过).
        //1、当前节点是否已经投过桑
        //2、 MsgPreVote消息发送者的任期号是否更大
        //3、当前节点投票给了对方节点
        //4、 MsgPreVote消息发送者的raftLog中是否包含当前节点的全部Entry记录, isUpToDate()方法在前面介绍过了,这里不再赞这
		if (r.Vote == None || m.Term > r.Term || r.Vote == m.From) && r.raftLog.isUpToDate(m.Index, m.LogTerm) {
			r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] cast %s for %x [logterm: %d, index: %d] at term %d",
				r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), r.Vote, m.Type, m.From, m.LogTerm, m.Index, r.Term)
			// 将票才是给MsgPreVote消息的发送节点
			r.send(pb.Message{To: m.From, Term: m.Term, Type: voteRespMsgType(m.Type)})
			if m.Type == pb.MsgVote {
				// Only record real votes.
				r.electionElapsed = 0
				r.Vote = m.From
			}
		} else {
		//不满足上述投赞同票条件时,当前节点会返回拒绝票(响应消息中的Reject字段会设立成true)
			r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] rejected %s from %x [logterm: %d, index: %d] at term %d",
				r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), r.Vote, m.Type, m.From, m.LogTerm, m.Index, r.Term)
			r.send(pb.Message{To: m.From, Term: r.Term, Type: voteRespMsgType(m.Type), Reject: true})
		}

	default:
		r.step(r, m)
	}

 MsgPreVote消息的处理过程:raft.Step()方法首先检测该MsgPreVote消息是否为Leader节点迁移时发出的消息及其他合法性检测,决定当前节点是否参与此次选举;之后当前节点会根据自身的状态决定是否将其选票投给MsgPreVote消息的发送节点。

3、MsgPre VoteResp消息

PreCandidate节点会收到集群中其他节点返回的MsgPreVoteResp消息,其中的Term字段与PreCandidate节点的Term值相同。在raft.Step()方法中没有对Term值相等的MsgPreVoteResp消息做特殊的处理,而是直接交给了raft.step字段指向的函数进行处理。在前面分析的状态切换方法(become方法〉中,可以看到raft.step宇段会根据节点的状态指向不同的消息处理函数, 在PreCandidate状态的节点中,该字段指向了stepCandidate()方法。stepCandidate()函数对MsgPreVoteResp消息的处理逻辑:

func stepCandidate(r *raft, m pb.Message) {
	// Only handle vote responses corresponding to our candidacy (while in
	// StateCandidate, we may get stale MsgPreVoteResp messages in this term from
	// our pre-candidate state).
	var myVoteRespType pb.MessageType
	//根据当前节点的状态决定其能够处理的选举响应消息的类型
	if r.state == StatePreCandidate {
		myVoteRespType = pb.MsgPreVoteResp
	} else {
		myVoteRespType = pb.MsgVoteResp
	}
	switch m.Type {
	case pb.MsgProp:
		r.logger.Infof("%x no leader at term %d; dropping proposal", r.id, r.Term)
		return
	case pb.MsgApp:
		r.becomeFollower(r.Term, m.From)
		r.handleAppendEntries(m)
	case pb.MsgHeartbeat:
		r.becomeFollower(r.Term, m.From)
		r.handleHeartbeat(m)
	case pb.MsgSnap:
		r.becomeFollower(m.Term, m.From)
		r.handleSnapshot(m)
	case myVoteRespType://处理收到的选举响应消息,当前示例中处理的是MsgPreVoteResp消息
		gr := r.poll(m.From, m.Type, !m.Reject)//记录并统计投票结果
		r.logger.Infof("%x [quorum:%d] has received %d %s votes and %d vote rejections", r.id, r.quorum(), gr, m.Type, len(r.votes)-gr)
		switch r.quorum() {//得票是否过半数
		case gr:
			if r.state == StatePreCandidate {
				r.campaign(campaignElection)//当PreCandidate节点在预选中收到半数以上的选票之后,会发起正式的选举
			} else {
				r.becomeLeader()
				r.bcastAppend()
			}
		case len(r.votes) - gr:
			r.becomeFollower(r.Term, None)//赞同与拒绝相等时,无法获取半数以上的票数,当前节点切换成Follower状态,等待下一轮的选举(或预选)
		}
	case pb.MsgTimeoutNow:
		r.logger.Debugf("%x [term %d state %v] ignored MsgTimeoutNow from %x", r.id, r.Term, r.state, m.From)
	}
}

当PreCandidate状态节点收到半数以上的投票时,会通过rcampaign()方法发起正式选举,其中会通过raft.becomeCandidate()方法将当前节点切换成Candidate状态,井向剩余其他节点发送MsgVot巳消息

4、MsgVote消息

PreCandidate 状态节点收到半数以上的投票之后, 会发起新一轮的选举, 即向集群中的其他节点发送MsgVote消息。当集群中其他节点收到MsgVote消息之后,也是交由raft.Step()方法进行处理的, 其中根据Term 值进行分类处理的部分与前面介绍的MsgPreVote处理类似

case m.Term > r.Term:
		if m.Type == pb.MsgVote || m.Type == pb.MsgPreVote {
			force := bytes.Equal(m.Context, []byte(campaignTransfer))
			inLease := r.checkQuorum && r.lead != None && r.electionElapsed < r.electionTimeout
			if !force && inLease {
				// If a server receives a RequestVote request within the minimum election timeout
				// of hearing from a current leader, it does not update its term or grant its vote
				r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] ignored %s from %x [logterm: %d, index: %d] at term %d: lease is not expired (remaining ticks: %d)",
					r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), r.Vote, m.Type, m.From, m.LogTerm, m.Index, r.Term, r.electionTimeout-r.electionElapsed)
				return nil
			}
		}
		switch {
		case m.Type == pb.MsgPreVote:
			// Never change our term in response to a PreVote
		case m.Type == pb.MsgPreVoteResp && !m.Reject:
			// We send pre-vote requests with a term in our future. If the
			// pre-vote is granted, we will increment our term when we get a
			// quorum. If it is not, the term comes from the node that
			// rejected our vote so we should become a follower at the new
			// term.
		default:
			r.logger.Infof("%x [term: %d] received a %s message with higher term from %x [term: %d]",
				r.id, r.Term, m.Type, m.From, m.Term)
			if m.Type == pb.MsgApp || m.Type == pb.MsgHeartbeat || m.Type == pb.MsgSnap {
				r.becomeFollower(m.Term, m.From)
			} else {
				r.becomeFollower(m.Term, None)
			}
		}

raft.Step()方法中根据消息类型进行分类处理的代码片段中,除了检测当前节点是否投票及发送MsgVoteResp消息,还会重置当前节点的选举超时计时器并更新ra位Vote字段

5、MsgVoteResp消息

与对MsgPreVoteResp消息的处理类似,MsgVoteResp消息也是由raft.stepCandidate()方法处理的

func stepCandidate(r *raft, m pb.Message) {
	// Only handle vote responses corresponding to our candidacy (while in
	// StateCandidate, we may get stale MsgPreVoteResp messages in this term from
	// our pre-candidate state).
	var myVoteRespType pb.MessageType
	if r.state == StatePreCandidate {
		myVoteRespType = pb.MsgPreVoteResp
	} else {
		myVoteRespType = pb.MsgVoteResp
	}
	switch m.Type {
	case pb.MsgProp:
		r.logger.Infof("%x no leader at term %d; dropping proposal", r.id, r.Term)
		return
	case pb.MsgApp:
		r.becomeFollower(r.Term, m.From)
		r.handleAppendEntries(m)
	case pb.MsgHeartbeat:
		r.becomeFollower(r.Term, m.From)
		r.handleHeartbeat(m)
	case pb.MsgSnap:
		r.becomeFollower(m.Term, m.From)
		r.handleSnapshot(m)
	case myVoteRespType:
		gr := r.poll(m.From, m.Type, !m.Reject)
		r.logger.Infof("%x [quorum:%d] has received %d %s votes and %d vote rejections", r.id, r.quorum(), gr, m.Type, len(r.votes)-gr)
		switch r.quorum() {
		case gr:
			if r.state == StatePreCandidate {
				r.campaign(campaignElection)
			} else {
		    //当前节点切换成为Leader状态, 其中会重置每个节点对应的Next和Match两个索引,
				r.becomeLeader()
				r.bcastAppend()//向集群中其他节点广播MsgApp消息
			}
		case len(r.votes) - gr:
			r.becomeFollower(r.Term, None)
		}
	case pb.MsgTimeoutNow:
		r.logger.Debugf("%x [term %d state %v] ignored MsgTimeoutNow from %x", r.id, r.Term, r.state, m.From)
	}
}

 raft.bcastAppend()方法主要负责向集群中的其他节点发送MsgApp消息(或MsgSnap消息〉,具体实现如下:

func (r *raft) bcastAppend() {
	r.forEachProgress(func(id uint64, _ *Progress) {
		if id == r.id {
			return//过滤当前节点本身,只向集群中其他节点发送消息
		}

		r.sendAppend(id)//向指定节点发送消息
	})
}

raft.sendAppend()方法主要负责向指定的目标节点发送MsgApp消息(或MsgSnap消息〉,在消息发送之前会检测当前节点的状态,然后查找待发迭的Entry记录并封装成MsgApp消息,之后根据对应节点的Progress.State值决定发送消息之后的操作
raft. sendAppend()方法主要负责向指定的目标节点发送MsgApp消息(或MsgSnap消息〉,
在消息发送之前会检测当前节点的状态,然后查找待发迭的Entry记录并封装成MsgApp消息,
之后根据对应节点的Progress.State值决定发送消息之后的操作。 Progress.State
各值的含义:
    (1)、ProgressStateSnapshot状态表示Leader节点正在向目标节点发送快照数据。
    (2)、ProgressStateProbe状态表示Leader节点一次不能向目标节点发送多条消息,只能待一条消息被响应之后,才能发送下一条消息。当刚刚复制完快照数据、上次MsgApp消息被拒绝(或是发送失败)或是Leader节点初始化时,都会导致目标节点的Progress切换到该状态。
    (3)、ProgressStateReplicate状态表示正常的Entry记录复制状态,Leader节点向目标节点发送完消息之后,无须等待响应,即可开始后续消息的发送。
   

func (r *raft) sendAppend(to uint64) {
	pr := r.getProgress(to)
	if pr.IsPaused() {
		return
	}
	m := pb.Message{}//创建待发送的消息
	m.To = to//设置目标节点的ID
    //根据当前Leader节点记录的Next查找发往指定节点的Entry记录(ents)及Next索引对应的记录的Term值(term)
	term, errt := r.raftLog.term(pr.Next - 1)
	ents, erre := r.raftLog.entries(pr.Next, r.maxMsgSize)
    //上述两次raftLog查找出现异常时,就会形成MsgSnap消息,将快照数据发送到指定节点,后面详细介绍MsgSnap消息的发送,这里重点看一下MsgApp消息发送
	if errt != nil || erre != nil { // send snapshot if we failed to get term or entries
		//上述两次raftLog查找出现异常时,就会形成MsgSnap消息,将快照数据发送到指定节点,这部分代码暂时省略, 后面详细介绍MsgSnap消息的发送,这里重点看一下MsgApp消息发送
	} else {
		m.Type = pb.MsgApp //设置消息类型
		m.Index = pr.Next - 1//设置MsgApp消息的Index字段
		m.LogTerm = term//设置MsgApp消息的LogTerm字段
		m.Entries = ents//设置消息携带的Entry记录集合
		m.Commit = r.raftLog.committed//设立消息的Commit字段,即当前节点的raftLog中最后一条已提交的记录索引值
		if n := len(m.Entries); n != 0 {
			switch pr.State {//根据目标节点对应的Progress.State状态决定其发送消息后的行为
			// optimistically increase the next when in ProgressStateReplicate
			case ProgressStateReplicate:
				last := m.Entries[n-1].Index
				pr.optimisticUpdate(last)//更新目标节点对应的Next值(这里不会更新Match)
				pr.ins.add(last)//记录已发送但是未收到响应的消息
			case ProgressStateProbe:
				pr.pause()//消息发送后,就将Progress.Paused字段设置成true,暂停后续消息的发送
			default:
				r.logger.Panicf("%x is sending append in unhandled state %s", r.id, pr.State)
			}
		}
	}
	//发送前面创建的MsgApp消息,raft.send()方法会设置MsgApp消息的Term字段佳,并将其追加到raft.msgs中等待发送。 raft.send()方法前面介绍过了
	r.send(m)
}
发布了48 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/cyq6239075/article/details/105343656