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)
}

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章