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

6、MsgApp消息

在MsgVote消息的处理过程中,集群中其他节点己经切换成了Follower状态, 并且它们自身记录的Term值与该任期中Leader节点维护的Term值相同。当它们收到当前Leader节点发来的MsgApp消息时,也是交由ra仕Step()方法处理的。

func (r *raft) Step(m pb.Message) error {
	
	switch m.Type {
	case pb.MsgHup:

	case pb.MsgVote, pb.MsgPreVote:

	default:
		r.step(r, m)//当前节点是Follower状态,raft.step字段指向stepFollower( )函数
	}
	return nil
}

func stepFollower(r *raft, m pb.Message) {
	switch m.Type {
	case pb.MsgProp:
		
	case pb.MsgApp:
		r.electionElapsed = 0//重置选举计算器,防止当前Follower发起新一轮选举
		r.lead = m.From      //设立raft.Lead记录,保存当前集群的Leader节点ID
		r.handleAppendEntries(m)//将MsgApp消息中携带的Entry记录追加到raftLog中, 并且向Leader节点发送MsgAppResp消息,响应此次MsgApp消息
	case pb.MsgHeartbeat:
		r.electionElapsed = 0
		r.lead = m.From
		r.handleHeartbeat(m)
	case pb.MsgSnap:
		r.electionElapsed = 0
		r.lead = m.From
		r.handleSnapshot(m)
	case pb.MsgTransferLeader:
		if r.lead == None {
			r.logger.Infof("%x no leader at term %d; dropping leader transfer msg", r.id, r.Term)
			return
		}
		m.To = r.lead
		r.send(m)
	case pb.MsgTimeoutNow:
		
	case pb.MsgReadIndex:
		
	case pb.MsgReadIndexResp:
		
	}
}

raft.handleAppendEntries()方法首先会检测MsgApp消息中携带的Entry记录是否合法,然后将这些Entry记录追加到raftLog中,最后创建相应的MsgAppResp消息。handleAppendEntries()方法

func (r *raft) handleAppendEntries(m pb.Message) {
     //m.Index表示leader发送给follower的上一条日志的索引位置,如采Follower节点在Index位置的Entry记录已经提交过了,
     //则不能进行追加操作,在前面在Raft协议时捉到过,己提交的记录不能被覆盖,
     //所以Follower节点会将其committed位置通过MsgAppResp消息(Index字段)通知Leader节点
	if m.Index < r.raftLog.committed {
		r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: r.raftLog.committed})
		return
	}
     //尝试将消息携带的Entry记录追加到raftLog中
	if mlastIndex, ok := r.raftLog.maybeAppend(m.Index, m.LogTerm, m.Commit, m.Entries...); ok {
	//如采追加成功,则将最后一条记录的索引位通过MsgAppResp消息返回给Leader节点,这样Leader节点就可以根据此值更新其对应的Next和Match值
		r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: mlastIndex})
	} else {
	//如采追加记录失败,则将失败信息返回给Leader节点(即MsgAppResp 消息的Reject字段为true),同时返回的还有一些提示信息(RejectHint字段保存了当前节点raftLog中最后一条记录的索引)
		r.logger.Debugf("%x [logterm: %d, index: %d] rejected msgApp [logterm: %d, index: %d] from %x",
			r.id, r.raftLog.zeroTermOnErrCompacted(r.raftLog.term(m.Index)), m.Index, m.LogTerm, m.Index, m.From)
		r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: m.Index, Reject: true, RejectHint: r.raftLog.lastIndex()})
	}
}

7、MsgAppResp消息

再回到Leader节点,当其收到集群中其他Follower节点发迭的MsgAppResp响应消息之后,也是交由raft.Step()方法进行处理的,其中会调用raft.step 字段指向的StepLeader() 函数进行处理的。下面开始分析raft.StepLeader()方法中处理MsgAppResp消息的代码

func stepLeader(r *raft, m pb.Message) {
	// These message types do not require any progress for m.From.
	switch m.Type {
	case pb.MsgBeat:
		
	case pb.MsgCheckQuorum:
		
	case pb.MsgProp:
		
	case pb.MsgReadIndex:
     //根据消息的From字段获取对应的Progress实例,为后面的消息处理做准备
	// All other message types require a progress for m.From (pr).
	pr := r.getProgress(m.From)
	if pr == nil {
		r.logger.Debugf("%x no progress available for %x", r.id, m.From)
		return
	}
	switch m.Type {
	case pb.MsgAppResp:
	   //更新对应Progress实例的RecentActive字段,从Leader节点的角度来看,MsgAppResp消息的发送节点还是存活的
		pr.RecentActive = true

		if m.Reject {//MsgApp 消息被拒绝
			r.logger.Debugf("%x received msgApp rejection(lastindex: %d) from %x for index %d",
				r.id, m.RejectHint, m.From, m.Index)
				//通过MsgAppResp消息携带的信息及对应的Progress状态,重新设立其Next
			if pr.maybeDecrTo(m.Index, m.RejectHint) {
			//如果对应的Progress处于ProgressStateReplicate状态,则切换成
I           //ProgressStateProbe状态,试探Follower的匹配位置(这里的试探是指发送一条消息并等待其相应之后,再发送后续的消息)
				r.logger.Debugf("%x decreased progress of %x to [%s]", r.id, m.From, pr)
				if pr.State == ProgressStateReplicate {
					pr.becomeProbe()
				}
				//再次向对应Follower节点发送MsgApp消息,在sendAppend()方法中会将对应的Progress.pause字段设立为true,从而暂停后续消息的发送,从而实现前面说的“试探”的效果
				r.sendAppend(m.From)
			}
		} else {//之前发送的MsgApp消息已经被对反的Follower节点接收(Entry记录被成功追加)
			oldPaused := pr.IsPaused()
			//MsgAppResp消息的Index字段是对应Follower节点raftLog中最后一条Entry记录的索引,这里会根据该值更新其对应Progress实例的Match和Next,Progress. maybeUpdate () 
            //方法在前面已经介绍过了
			if pr.maybeUpdate(m.Index) {
				switch {
				case pr.State == ProgressStateProbe:
				//一旦MsgApp被Follower节点接收,则表示已经找到其正确的Next和Match,不必再进行“试探”,这里将对应的Progress.state切换成ProgressStateReplicate
					pr.becomeReplicate()
				case pr.State == ProgressStateSnapshot && pr.needSnapshotAbort():
					r.logger.Debugf("%x snapshot aborted, resumed sending replication messages to %x [%s]", r.id, m.From, pr)
					//之前由于某些原因,Leader节点通过发送快照的方式恢复Follower节点,但在发送MsgSnap消息的过程中,Follower节点恢复,并正常接收了Leader节点的MsgApp消
                    //息,此时会丢弃MsgSnap消息,并开始“试探”该Follower节点正确的Match和Next值
					pr.becomeProbe()
				case pr.State == ProgressStateReplicate:
				//之前向某个Follower节点发送MsgApp消息时,会将其相关信息保存到对应的
                //Progress.ins中,在这里收到相应的MsgAppResp响应之后,会将其从ins中删除,
                 //这样可以实现了限流的效采,避免网络出现延迟时,继续发送消息,从而导致网络更加拥堵
					pr.ins.freeTo(m.Index)
				}
                //收到一个Follower节点的MsgAppResp消息之后,除了修改相应的Match和Next,还会尝试更新raftLog.committed,因为有些Entry记录可能在此次复制中被保存到了
                //半数以上的节点中,raft.maybeCommit()方法在前面已经分析过了
				if r.maybeCommit() {
				//向所有节点发送MsgApp消息,注意,此次MsgApp消息的Commit字段与上次MsgApp消息已经不同,raft.bcastAppend()方法前面已经讲过
					r.bcastAppend()
				} else if oldPaused {//之前是pause状态,现在可以任性地发消息了
					// update() reset the wait state on this node. If we had delayed sending
					// an update before, send it now.
					//之前Leader节点暂停向该Follower节点发送消息,收到MsgAppResp消息后,在上述代码中已经重立了相应状态,所以可以继续发送MsgApp消息
					r.sendAppend(m.From)
				}
				// Transfer leadership is in progress.
				if m.From == r.leadTransferee && pr.Match == r.raftLog.lastIndex() {
					r.logger.Infof("%x sent MsgTimeoutNow to %x after received MsgAppResp", r.id, m.From)
					r.sendTimeoutNow(m.From)
				}
			}
		}
	case pb.MsgHeartbeatResp:
	
	case pb.MsgSnapStatus:
	
	case pb.MsgUnreachable:
		// During optimistic replication, if the remote becomes unreachable,
		// there is huge probability that a MsgApp is lost.
		if pr.State == ProgressStateReplicate {
			pr.becomeProbe()
		}
		r.logger.Debugf("%x failed to send message to %x because it is unreachable [%s]", r.id, m.From, pr)
	case pb.MsgTransferLeader:
		
	}
}

stepLeader()方法关于MsgAppResp消息处理的代码片段中,介绍一下两个第一次遇到的方法。首先是Progress.maybeDecrTo()方法,它会根据对应Progress的状态和MsgAppResp消息携带的提示信息,完成Progress.Next的更新

//maybeDecrTo()方法的两个参数都是MsgAppResp消息携带的信息:
//reject是被拒绝MsgApp消息的Index字段佳,
//last是被拒绝MsgAppResp消息的RejectHint字段佳(即对应Follower节点raftLog中最后一条Entry记录的索引)
func (pr *Progress) maybeDecrTo(rejected, last uint64) bool {
	if pr.State == ProgressStateReplicate {
		// the rejection must be stale if the progress has matched and "rejected"
		// is smaller than "match".
		if rejected <= pr.Match {
			return false
		}
		// directly decrease next to match + 1
		    //根据前面对MsgApp消息发送过程的分析,处于ProgressStateReplicate状态时,发送MsgApp
    //消息的同时会直接调用Progress.optimisticUpdate()方法增加Next,这就使得Next可能会
    //比Match大很多,这里回退Next至Match位置,并在后面重新发送MsgApp消息进行尝试
		pr.Next = pr.Match + 1
		return true
	}

	// the rejection must be stale if "rejected" does not match next - 1
	if pr.Next-1 != rejected {//出现过时的MsgAppResp消息直接忽略
		return false
	}
    //根据MsgAppResp携带的信息重直Next
	if pr.Next = min(rejected, last+1); pr.Next < 1 {
		pr.Next = 1//将Next重直为l
	}
	//Next重置完成,恢复消息发送,并在后面重新发送MsgApp消息
	pr.resume()
	return true
}

另一个需要介绍的是inflights结构体,Progress.ins字段就指向了一个inflights实例,inflights的主要功能是记录当前节点己发出但未收到响应的MsgApp消息
在前面介绍MsgApp消息的发送过程时,提到过inflights.add()方法是用来记录发送出去的MsgApp消息
通过前面分析的内容可知,当Leader节点收到MsgAppResp消息时,会通过inflights.freeTo()方法将指定消息及其之前的消息全部清空,释放inflights 空间,让后面的消息继续发送。

7、MsgBeat消息和MsgCheckQuorum消息

Leader节点除了向集群中其他Follower节点发送MsgApp消息,还会向这些Follower节点发送MsgBeat消息。MsgBeat消息的主要作用是探活, 当Follower节点收到MsgBeat消息时会重置其选举计时器,从而防止Follower节点发起新一轮选举。

8、MsgHeartbeat消息和MsgHeartbeatResp消息

当集群中的Follower 节点收到Leader 节点发来的MsgHeartbeat 消息之后,也是通过raft.Step()方法调用raft.stepFollower()方法进行处理的,处理完成后会向Leader节点返回相应的MsgHearbeatResp消息作为响应

9、MsgProp消息

raft模块中,客户端发往到集群的写请求是通过MsgProp消息表示的。 在前面介绍Raft 协议时提到,Raft 集群中只有Leader节点能够响应客户端的写入请求。Leader节点对于MsgProp消息的主要处理是在raft.stepLeader()方法中实现的,相关的代码片段如下:

func stepLeader(r *raft, m pb.Message) {
	// These message types do not require any progress for m.From.
	switch m.Type {
	case pb.MsgBeat:

	case pb.MsgCheckQuorum:
		
	case pb.MsgProp:
	//这里重点介绍MsgProp消息相关代码片段,其他类型消息的处理片段暂时省略检测MsgProp消息是否携带了Entry记录,如果未携带,则输出异常日志并终止程序(略)
    //检测当前节点是否被移出集群,如果当前节点以Leader状态被移出集群,则不再处理MsgProp消息(咯)检测当前是否正在进行Leader节点的转移,不再处理MsgProp消息(略)
		if len(m.Entries) == 0 {
			r.logger.Panicf("%x stepped empty MsgProp", r.id)
		}
		if _, ok := r.prs[r.id]; !ok {
			// If we are not currently a member of the range (i.e. this node
			// was removed from the configuration while serving as leader),
			// drop any new proposals.
			return
		}
		if r.leadTransferee != None {
			r.logger.Debugf("%x [term %d] transfer leadership to %x is in progress; dropping proposal", r.id, r.Term, r.leadTransferee)
			return
		}

		for i, e := range m.Entries {//边历MsgProp消息携带的全部Entry记录
			if e.Type == pb.EntryConfChange {//如存在EntryConfChange类型的Entry记录,则将raft.pendingConf设立为true
				if r.pendingConf {//如存在多条EntryConfChange类型的记录,则只保留第一条
					r.logger.Infof("propose conf %s ignored since pending unapplied configuration", e.String())
					m.Entries[i] = pb.Entry{Type: pb.EntryNormal}
				}
				r.pendingConf = true
			}
		}
		//将上述Entry记录追加到当前节点的raftLog中通过MsgApp消息向集群中其他节点复制Entry记录,bcastAppend( )方法在前面介绍过了
		r.appendEntry(m.Entries...)
		r.bcastAppend()
		return
	case pb.MsgReadIndex:
		return
	}

	// All other message types require a progress for m.From (pr).
	pr := r.getProgress(m.From)
	if pr == nil {
		r.logger.Debugf("%x no progress available for %x", r.id, m.From)
		return
	}
	switch m.Type {
	case pb.MsgAppResp:
		
	case pb.MsgHeartbeatResp:
		
	case pb.MsgSnapStatus:
		
	case pb.MsgUnreachable:
		
	case pb.MsgTransferLeader:
		
	}
}

当集群中的Candidate 节点收到客户端发来的MsgProp 消息时, 会直接忽略该消息。当Follower 节点收到MsgProp 消息时,会将该MsgProp 消息转发给当前集群的Leader节点,stepFollower()方法中相关代码片段如下:

func stepFollower(r *raft, m pb.Message) {
	switch m.Type {
	case pb.MsgProp:
		if r.lead == None {//当前集群中没有Leader节点, 则忽略该MsgProp消息
			r.logger.Infof("%x no leader at term %d; dropping proposal", r.id, r.Term)
			return
		} else if r.disableProposalForwarding {
			r.logger.Infof("%x not forwarding to leader %x at term %d; dropping proposal", r.id, r.lead, r.Term)
			return
		}
		m.To = r.lead//将消息的To字段设立为当前Leader节点的id
		r.send(m)//将MsgProp消息发送到当前的Leader节点,send()方法前面介绍过
		
	}
}

10、MsgReadlndex消息和MsgReadlndexResp消息

介绍Raft协议时提到, 客户端的读请求需要读到集群中最新的、己提交的数据(即linearizability语义), 而不能读到老数据。在现实场景中也经常会遇到读多写少的情况,如果每次读请求都涉及多个节点的磁盘操作,则性能必然较差。Leader 节点保存了整个集群中最新的数据,如果只读请求只访问Leader节点,则Leader节点可以直接将结果返回给客户端,但是在网络分区的场景下,一个旧的Leader节点就可能返回旧数据。为此,raft模块使用MsgReadlndex消息来解决上述问题:当Leader节点收到客户端的只读请求时,会将当前请求的编号记录下来,在返回数据给客户端之前,Leader 节点需要先确定自己是否依然是当前集群的Leader 节点(通过心跳的方式〉,在确定其依然是Leader节点之后,就可以说明该节点可以响应该请求,只需要等待当前Leader 节点的提交位置(即raftLog.committed)到达或是超过只读请求的编号即可向客户端返回响应。在raft模块中,客户端发往集群的只读请求使用MsgReadlndex消息表示,其中只读请求有两种模式,分别是ReadOnlySafe和ReadOnly LeaseBased。 Leader节点对于MsgReadlndex消息的主要处理也是在raft.stepLeader()方法中实现的。集群中,Follower节点不能直接响应客户端的只读请求,而是转发给Leader节点进行处理,等待Leader节点响应之后,Follower节点才能响应Client.

11、MsgSnap消息

前面介绍的ra位sendAppend()方法可知,在Leader节点尝试向集群中的Follower节点发送MsgApp消息时,如果查找不到待发送的Entry记录(即该Follow节点对应的Progress.Next指定的Entry记录),则会尝试通过MsgSnap消息将快照数据发送到Follower节点, Follower节点之后会通过快照数据恢复其自身状态,从而可以与Leader节点进行正常的Entry记录复制。例如, 当Follower节点右机时间比较长, 就可能出现上述发送MsgSnap消息的场景。

func (r *raft) sendAppend(to uint64) {
	pr := r.getProgress(to)
	if pr.IsPaused() {
		return
	}
	m := pb.Message{}
	m.To = to

	term, errt := r.raftLog.term(pr.Next - 1)
	ents, erre := r.raftLog.entries(pr.Next, r.maxMsgSize)

	if errt != nil || erre != nil { // send snapshot if we failed to get term or entries
		if !pr.RecentActive {
			r.logger.Debugf("ignore sending snapshot to %x since it is not recently active", to)
			return
		}

		m.Type = pb.MsgSnap//将消息类型设直成MsgSnap,为后续发送快照数据做准备
		snapshot, err := r.raftLog.snapshot()//获取快照数据
		if err != nil {
			if err == ErrSnapshotTemporarilyUnavailable {
				r.logger.Debugf("%x failed to send snapshot to %x because snapshot is temporarily unavailable", r.id, to)
				return
			}
			panic(err) // TODO(bdarnell)
		}
		if IsEmptySnap(snapshot) {
			panic("need non-empty snapshot")
		}
		m.Snapshot = snapshot//设置MsgSnap消息的Snapshot字段
		sindex, sterm := snapshot.Metadata.Index, snapshot.Metadata.Term//获取快照的相关信息
		r.logger.Debugf("%x [firstindex: %d, commit: %d] sent snapshot[index: %d, term: %d] to %x [%s]",
			r.id, r.raftLog.firstIndex(), r.raftLog.committed, sindex, sterm, to, pr)
	//将目标Follower节点对应的Progress切换成ProgressStateSnapshot状态,其中会用Progress.PendingSnapshot字段记录快照数据的信息
		pr.becomeSnapshot(sindex)
		r.logger.Debugf("%x paused sending replication messages to %x [%s]", r.id, to, pr)
	} else {
		
	}
	r.send(m)
}

下面转到Follower 节点,介绍其对MsgSnap 消息的处理,其相关的代码片段位于raftstepFollower()方法

func stepFollower(r *raft, m pb.Message) {
	switch m.Type {
	case pb.MsgProp:
	case pb.MsgApp:
		r.electionElapsed = 0
		r.lead = m.From
		r.handleAppendEntries(m)
	case pb.MsgHeartbeat:
		r.electionElapsed = 0
		r.lead = m.From
		r.handleHeartbeat(m)
	case pb.MsgSnap:
		r.electionElapsed = 0//重置raft.electionElapsed,防止发生选举
		r.lead = m.From   //设置Leader的id
		r.handleSnapshot(m)//通过MsgSnap消息中的快照数据,重建当前节点的raftLog
}

在raft.handleSnapshot()方法中,会读取MsgSnap 消息中的快照数据,并重建当前节点的raftLog

func (r *raft) handleSnapshot(m pb.Message) {
    //获取快照数据的元数据
	sindex, sterm := m.Snapshot.Metadata.Index, m.Snapshot.Metadata.Term
	if r.restore(m.Snapshot) {//返回值表示是否通过快照数据进行了重建
	//向Leader节点返回MsgAppResp消息(Reject始终为false) 。 该MsgAppResp消息作为MsgSnap消息的响应,与前面介绍的MsgApp消息并无差别
		r.logger.Infof("%x [commit: %d] restored snapshot [index: %d, term: %d]",
			r.id, r.raftLog.committed, sindex, sterm)
		r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: r.raftLog.lastIndex()})
	} else {
		r.logger.Infof("%x [commit: %d] ignored snapshot [index: %d, term: %d]",
			r.id, r.raftLog.committed, sindex, sterm)
		r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: r.raftLog.committed})
	}
}

在raft.restore()方法中,会调用raftLog中相应的方法进行检测和重建,其具体实现如下

func (r *raft) restore(s pb.Snapshot) bool {
	if s.Metadata.Index <= r.raftLog.committed {
		return false
	}
	//根据快照数据的元数据查找匹自己的Entry记录,如采存在,则表示当前节点已经拥有了该快照中的全部数据,所以无须进行后续重建操作
	if r.raftLog.matchTerm(s.Metadata.Index, s.Metadata.Term) {
		r.logger.Infof("%x [commit: %d, lastindex: %d, lastterm: %d] fast-forwarded commit to snapshot [index: %d, term: %d]",
			r.id, r.raftLog.committed, r.raftLog.lastIndex(), r.raftLog.lastTerm(), s.Metadata.Index, s.Metadata.Term)
		//快照中全部的Entry记录都已经提交了,所以尝试修改当前节点的raftLog.committed,通过前面的介绍我们知道,raftLog.committed只增不减
		r.raftLog.commitTo(s.Metadata.Index)
		return false
	}
    
	// The normal peer can't become learner.
	if !r.isLearner {
		for _, id := range s.Metadata.ConfState.Learners {
			if id == r.id {
				r.logger.Errorf("%x can't become learner when restores snapshot [index: %d, term: %d]", r.id, s.Metadata.Index, s.Metadata.Term)
				return false
			}
		}
	}

	r.logger.Infof("%x [commit: %d, lastindex: %d, lastterm: %d] starts to restore snapshot [index: %d, term: %d]",
		r.id, r.raftLog.committed, r.raftLog.lastIndex(), r.raftLog.lastTerm(), s.Metadata.Index, s.Metadata.Term)
    
	r.raftLog.restore(s)
	//通过raftLog.unstable记录该快照数据,同时重豆相关字段
	r.prs = make(map[uint64]*Progress)
	r.learnerPrs = make(map[uint64]*Progress)
	r.restoreNode(s.Metadata.ConfState.Nodes, false)
	r.restoreNode(s.Metadata.ConfState.Learners, true)
	return true
}
发布了48 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

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