etcd中raft協議的消息(四) —— 心跳相關的消息(MsgBeat、MsgHeartbeat、MsgHeartbeatResp和MsgCheckQuorum)

MsgBeat和MsgHeartbeat消息

      Leader推動心跳計時器(heartbeatElapsed),而Follower推動選舉計時器(electionElapsed),選舉計時器的流程前面已經提到,這裏主要介紹心跳計時器。當上層模塊調用Tick()時,Leader會推動心跳計時器,在tickHeartbeat函數中有詳細介紹。有兩種情況會是Leader向集羣中其他的節點發送心跳消息分別是心跳計時器超時和在ReadOnly模式下Leader接收到客戶端請求的只讀消息,這裏我們主要介紹心跳計時器超時的情況。

 MsgBeat和MsgHeartbeat消息的主要區別是MsgBeat是本地消息,而發給集羣中其他節點的心跳消息是使用MsgHeartbeat消息

Leader調用tickHeartbeat推動心跳計時器

    當心跳計時器超時時會向其他節點發送心跳,如果raft的配置checkQuorum爲true,則也會發送MsgCheckQuorum消息。tickHeartbeat的主要流程:

1.遞增心跳計時器和選舉計時器

2.如果選舉計時器大於等於選舉超時時間

                重置選舉計時器,Leader節點不會主動發起選舉

                 檢測當前節點是否與集羣中的大多數節點連通,如果超過半數以上的節點連通則發送MsgCheckQuorum確認

                 如果有節點正在轉移,則禁止節點轉移

3.如果選舉計時器沒有超時

              如果當前節點不是Leader,則直接返回

              心跳計時器超時,則發送MsgBeat消息,繼續維持向其餘節點發送心跳消息,並重置心跳計時器

func (r *raft) tickHeartbeat() {
	r.heartbeatElapsed++	//遞增心跳計時器
	r.electionElapsed++	//遞增選舉計時器

	if r.electionElapsed >= r.electionTimeout {  //當選舉計時器大於等於選舉超時時間
		r.electionElapsed = 0	//重置選舉計時器,Leader節點不會主動發起選舉
		if r.checkQuorum { //檢測當前節點是否與集羣中的大多數節點連通
			r.Step(pb.Message{From: r.id, Type: pb.MsgCheckQuorum})
		}
		// If current leader cannot transfer leadership in electionTimeout, it becomes leader again.
		if r.state == StateLeader && r.leadTransferee != None { //禁止節點轉移
			r.abortLeaderTransfer()		//清空raft.leadTransferee字段,放棄轉移
		}
	}

	if r.state != StateLeader {	//檢測當前節點是否是Leader,不是Leader直接返回
		return
	}

	if r.heartbeatElapsed >= r.heartbeatTimeout { //心跳計時器超時,則發生MsgBeat消息,繼續維持向其餘節點發送心跳
		r.heartbeatElapsed = 0 //重置心跳計時器
		r.Step(pb.Message{From: r.id, Type: pb.MsgBeat})
	}
}

Leader發送MsgBeat類型的消息流程

     Leader發送MsgBeat類型的消息主要是通過bcastHeartbeat方法向集羣中的其他節點廣播發送,如果readOnly中有隻讀消息,會在消息的上下文中攜帶readOnly中最後一條消息的消息ID,Follower節點收到後會在上下文中攜帶該消息ID響應。

//廣播發送心跳
func (r *raft) bcastHeartbeat() {
	//獲取readOnly最新一次的上下文
	lastCtx := r.readOnly.lastPendingRequestCtx()
	if len(lastCtx) == 0 {
		r.bcastHeartbeatWithCtx(nil)
	} else {
		r.bcastHeartbeatWithCtx([]byte(lastCtx))
	}
}

//遍歷向集羣中的其他節點發送心跳消息
func (r *raft) bcastHeartbeatWithCtx(ctx []byte) {
	r.forEachProgress(func(id uint64, _ *Progress) {
		if id == r.id {   //過濾當前節點
			return
		}
		r.sendHeartbeat(id, ctx)   //向指定的節點發送MsgBeat消息
	})
}

//向指定節點發送消息
func (r *raft) sendHeartbeat(to uint64, ctx []byte) {
	// Attach the commit as min(to.matched, r.committed).
	// When the leader sends out heartbeat message,
	// the receiver(follower) might not be matched with the leader
	// or it might not have all the committed entries.
	// The leader MUST NOT forward the follower's commit to
	// an unmatched index.
	//由於集羣中的節點不一定都收到全部已提交的Entry記錄,所以心跳消息攜帶commit提交位置
   消息類型爲MsgHeartbeat
	commit := min(r.getProgress(to).Match, r.raftLog.committed)
	m := pb.Message{
		To:      to,
		Type:    pb.MsgHeartbeat,
		Commit:  commit,
		Context: ctx,
	}

	r.send(m)
}

Follower節點接收到MsgHeartbeat消息的處理流程

    

Follower收到心跳計時器會重置選舉計時器,並且指定Leader,並且調用handleHeartbeat方法處理消息。

func  stepFollower(r *raft,m pb.Message) error {
	switch m.Type {
//其他類型消息的處理略
......

   	case pb.MsgHeartbeat:  //如果是心跳消息
		r.electionElapsed = 0 //重置選舉計時器
		r.lead = m.From	//指定leader
		r.handleHeartbeat(m)	//處理心跳消息
        }
}

處理心跳消息比較簡單,節點嘗試更新raftLog中已提交的位置,並且發送心跳回復消息MsgHeartbeatResp

 func (r *raft) handleHeartbeat(m pb.Message) {
	r.raftLog.commitTo(m.Commit)  //更新raftLog中已提交的位置
	r.send(pb.Message{To: m.From, Type: pb.MsgHeartbeatResp, Context: m.Context})  //心跳回復
}

MsgHeartbeatResp消息

    1.設置該節點的RecentActive字段爲true,表示該節點存活

    2.將Paused設爲false,表示可以繼續向該Follower節點發送消息

    3.如果消息隊列滿了,則釋放inflights中第一個消息,這樣就可以開始後續消息的發送

    4.當Leader節點收到Follower節點的MsgHeartbeat消息之後,會比較對應的Match值與Leader節點的raftLog,從而判斷Follower節點是否已擁有了全部的Entry記錄

    如果該Follower缺少Entry,則Leader會發送缺少的Entry記錄

    5.如果該節點的只讀模式不是ReadOnlySafe,則返回

    6.統計目前爲止響應上述攜帶消息ID的MsgHeartbeat消息的節點個數,如果沒有超過半數則退出

    7.如果響應心跳的節點數超過半數,則會清空readOnly中指定消息ID及其之前的所有相關記錄

        實現如下

                    根據MsgReadIndex消息的From字段,判斷該MsgReadIndex消息是否爲Follower節點轉發到Leader節點的消息

                          如果是客戶端直接發送到Leader節點的消息,

                          則將MsgReadIndex消息對應的已提交位置以及其消息ID封裝成ReadState實例,添加到raft.readStates中保存。

                          後續會有其他goroutine讀取該數組,並對相應的MsgReadIndex消息進行響應

                          如果其他Follower節點轉發到Leader節點的MsgReadIndex消息,

                           則Leader會向Follower節點返回相應的MsgReadIndex消息,並由Follower節點響應Client。

func stepLeader(r *raft, m pb.Message) error {
	//其他類型的消息略
  case pb.MsgHeartbeatResp:  //處理心跳回復消息
		pr.RecentActive = true //設置RecentActive字段
		pr.resume()	//設置Paused爲false,即可以重新發送消息

		// free one slot for the full inflights window to allow progress.
		if pr.State == ProgressStateReplicate && pr.ins.full() { //隊列已滿
			pr.ins.freeFirstOne() //釋放第一個消息,這樣就可以開始後續消息的發送
		}
		//判斷Follower是否已擁有全部的Entry記錄
		if pr.Match < r.raftLog.lastIndex() {
			r.sendAppend(m.From)	//通過向指定節點發送MsgApp消息完成Entry記錄的複製
		}


		if r.readOnly.option != ReadOnlySafe || len(m.Context) == 0 {
			return nil
		}

		ackCount := r.readOnly.recvAck(m)
		if ackCount < r.quorum() {
			return nil
		}

		rss := r.readOnly.advance(m)
		for _, rs := range rss {
			req := rs.req
			if req.From == None || req.From == r.id {
				/*
				根據MsgReadIndex消息的From字段,判斷該MsgReadIndex消息是否爲Follower節點轉發到Leader節點的消息
				如果是客戶端直接發送到Leader節點的消息,
				則將MsgReadIndex消息對應的已提交位置以及其消息ID封裝成ReadState實例,添加到raft.readStates中保存。
				後續會有其他goroutine讀取該數組,並對相應的MsgReadIndex消息進行響應
				*/
				r.readStates = append(r.readStates, ReadState{Index: rs.index, RequestCtx: req.Entries[0].Data})
			} else {
				//如果其他Follower節點轉發到Leader節點的MsgReadIndex消息,
				// 則Leader會向Follower節點返回相應的MsgReadIndex消息,並由Follower節點響應。
				r.send(pb.Message{To: req.From, Type: pb.MsgReadIndexResp, Index: rs.index, Entries: req.Entries})
			}
		}
}

  MsgCheckQuorum消息

       MsgCheckQuorum消息和MsgBeat消息的Term字段都爲0,因爲它們都屬於本地消息。當Leader 的心跳計時器超時,並且開啓了checkQuorum模式(raft的checkQuorum字段爲true)。該Leader節點就會發送MsgCheckQuorum消息檢測與集羣中其他節點是否保持半數以上的連接,如果沒有則變成Follower節點。

      MsgCheckQuorum類型的消息比較簡單,這裏就不在累述。

func stepLeader(r *raft, m pb.Message) error {
	switch m.Type {
	//其他類型的消息省略
	case pb.MsgCheckQuorum:
		if !r.checkQuorumActive() {  //檢測當前節點是否與集羣中大多數節點連通,如果不連通則切換成Follower
			r.logger.Warningf("%x stepped down to follower since quorum is not active", r.id)
			r.becomeFollower(r.Term, None)
		}
		return nil
}

   

 

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