etcd中raft協議的消息(五)—— 客戶端只讀相關的消息(MsgReadIndex和MsgReadIndexResp消息)

readOnly模式

       在看MsgReadIndex類型的消息之前需要先對readOnly的模式有所瞭解,raft結構體中的readOnly作用是批量處理只讀請求,只讀請求有兩種模式,分別是ReadOnlySafe和ReadOnlyLeaseBased,ReadOnlySafe是ETCD作者推薦的模式,因爲這種模式不受節點之間時鐘差異和網絡分區的影響,我們主要看一下ReadOnlySafe使用的實現方式。

readOnly的模式的ReadOnlySafe模式會用到readOnly結構體,主要用於批量處理只讀請求,readOnly的實現如下:

type readIndexStatus struct {
	req   pb.Message   //記錄了對應的MsgReadIndex請求
	index uint64		//該MsgReadIndex請求到達時,對應的已提交位置
	acks  map[uint64]struct{} //記錄了該MsgReadIndex相關的MsgHeartbeatResp響應的信息
}

type readOnly struct {
	option           ReadOnlyOption  //當前只讀請求的處理模式,ReadOnlySafe ReadOnlyOpt 和	ReadOnlyLeaseBased兩種模式
	/*
	在etcd服務端收到MsgReadIndex消息時,會爲其創建一個唯一的消息ID,並作爲MsgReadIndex消息的第一條Entry記錄。
	在pendingReadIndex維護了消息ID與對應請求readIndexStatus實例的映射
	*/
	pendingReadIndex map[string]*readIndexStatus
	readIndexQueue   []string   //記錄了MsgReadIndex請求對應的消息ID,這樣可以保證MsgReadIndex的順序
}

//初始化readOnly
func newReadOnly(option ReadOnlyOption) *readOnly {
	return &readOnly{
		option:           option,
		pendingReadIndex: make(map[string]*readIndexStatus),
	}
}

// addRequest adds a read only reuqest into readonly struct.
// `index` is the commit index of the raft state machine when it received
// the read only request.
// `m` is the original read only request message from the local or remote node.
//將已提交的位置(raftLog.committed)以及MsgReadIndex消息的相關信息存到readOnly中
/*
1.獲取消息ID,在ReadIndex消息的第一個記錄中記錄了消息ID
2.判斷該消息是否已經記錄在pendingReadIndex中,如果已存在則直接返回
3.如果不存在,則維護到pendingReadIndex中,index是當前Leader已提交的位置,m是請求的消息
4.並將消息ID追加到readIndexQueue隊列中
*/
func (ro *readOnly) addRequest(index uint64, m pb.Message) {
	ctx := string(m.Entries[0].Data)	//在ReadIndex消息的第一個記錄中,記錄了消息ID
	if _, ok := ro.pendingReadIndex[ctx]; ok {	//如果存在,則不再記錄該MsgReadIndex請求
		return
	}
	ro.pendingReadIndex[ctx] = &readIndexStatus{index: index, req: m, acks: make(map[uint64]struct{})} //創建MsgReadIndex對應的readIndexStatus實例,並記錄到pendingReadIndex
	ro.readIndexQueue = append(ro.readIndexQueue, ctx)  //記錄消息ID
}

// recvAck notifies the readonly struct that the raft state machine received
// an acknowledgment of the heartbeat that attached with the read only request
// context.
/*
recvAck通知readonly結構,即raft狀態機接受了對只讀請求上下文附加的心跳的確認。
1.消息的Context即消息ID,根據消息id獲取對應的readIndexStatus
2.如果獲取不到則返回0
3.記錄了該Follower節點返回的MsgHeartbeatResp響應的信息
4.返回Follower響應的數量
*/
func (ro *readOnly) recvAck(m pb.Message) int {
	rs, ok := ro.pendingReadIndex[string(m.Context)]
	if !ok {
		return 0
	}

	rs.acks[m.From] = struct{}{}
	// add one to include an ack from local node
	return len(rs.acks) + 1
}

// advance advances the read only request queue kept by the readonly struct.
// It dequeues the requests until it finds the read only request that has
// the same context as the given `m`.
//清空readOnly中指定消息ID及之前的所有記錄
/*
1.遍歷readIndexQueue隊列,如果能找到該消息的Context,則返回該消息及之前的所有記錄rss,
	並刪除readIndexQueue隊列和pendingReadIndex中對應的記錄
2.如果沒有Context對應的消息ID,則返回nil
*/
func (ro *readOnly) advance(m pb.Message) []*readIndexStatus {
	var (
		i     int
		found bool
	)

	ctx := string(m.Context)
	rss := []*readIndexStatus{}

	for _, okctx := range ro.readIndexQueue {
		i++
		rs, ok := ro.pendingReadIndex[okctx]
		if !ok {
			panic("cannot find corresponding read state from pending map")
		}
		rss = append(rss, rs)
		if okctx == ctx {
			found = true
			break
		}
	}

	if found {
		ro.readIndexQueue = ro.readIndexQueue[i:]
		for _, rs := range rss {
			delete(ro.pendingReadIndex, string(rs.req.Entries[0].Data))
		}
		return rss
	}

	return nil
}

// lastPendingRequestCtx returns the context of the last pending read only
// request in readonly struct.
//返回記錄中最後一個消息ID
func (ro *readOnly) lastPendingRequestCtx() string {
	if len(ro.readIndexQueue) == 0 {
		return ""
	}
	return ro.readIndexQueue[len(ro.readIndexQueue)-1]
}

MsgReadIndex消息

 

 

 客戶端發往集羣的只讀請求使用MsgReadIndex消息表示,通過Node接口的ReadIndex方法請求到集羣中

node節點實現了ReadIndex方法

func (n *node) ReadIndex(ctx context.Context, rctx []byte) error {
	return n.step(ctx, pb.Message{Type: pb.MsgReadIndex, Entries: []pb.Entry{{Data: rctx}}})
}

如果客戶端發到的節點是Follower狀態,則該Follower會向消息轉發到當前集羣的Leader

func stepFollower(r *raft, m pb.Message) error {
case pb.MsgReadIndex:
   if r.lead == None {
      r.logger.Infof("%x no leader at term %d; dropping index reading msg", r.id, r.Term)
      return nil
   }
   m.To = r.lead
   r.send(m)
}

所以真正處理MsgReadIndex消息的是當前集羣中的Leader

處理流程如下:

ETCD服務是集羣模式

            1.日誌中已提交的位置所在的任期號不等於當前任期號則返回nil

            2.如果當前節點處理readOnly的模式是ReadOnlySafe(ReadOnlySafe是ETCD作者推薦的模式,因爲這種模式不受節點之間時鐘差異和網絡分區的影響)

                   (1)則將此次請求消息寫入到readOnly中

                   (2)向集羣中的其他節點發送心跳,心跳消息的上下文中攜帶了該消息ID

            3.如果當前節點處理readOnly的模式是ReadOnlyLeaseBased

                       (1)獲取當前節點已提交的位置ri

                      (2)如果客戶端直接請求到該Leader節點,則直接將只讀消息追加到readStates隊列中,等待其他goroutine處理

                      (3)如果該消息是其他Follower轉發回來的消息,則向該Follower節點發送MsgReadIndexResp類型的消息

 

ETCD服務是單節點

                      單節點的情況直接將客戶端請求的消息追加到readStates隊列中,等待其他goroutine處理

func stepLeader(r *raft, m pb.Message) error {

   switch m.Type {
		//其他類型的消息處理省略
		case pb.MsgReadIndex:
		if r.quorum() > 1 { //集羣場景
			if r.raftLog.zeroTermOnErrCompacted(r.raftLog.term(r.raftLog.committed)) != r.Term {
				// Reject read only request when this leader has not committed any log entry at its term.
				return nil
			}

			// thinking: use an interally defined context instead of the user given context.
			// We can express this in terms of the term and index instead of a user-supplied value.
			// This would allow multiple reads to piggyback on the same message.
			/*
			Leader節點檢測自身在當前任期中是否已提交Entry記錄,如果沒有,則無法進行讀取操作
			*/
			switch r.readOnly.option {
			case ReadOnlySafe:
				//記錄當前節點的raftLog.committed字段值,即已提交位置
				r.readOnly.addRequest(r.raftLog.committed, m)
				r.bcastHeartbeatWithCtx(m.Entries[0].Data)  //發送心跳
			case ReadOnlyLeaseBased:
				ri := r.raftLog.committed
				if m.From == None || m.From == r.id { // from local member
					r.readStates = append(r.readStates, ReadState{Index: r.raftLog.committed, RequestCtx: m.Entries[0].Data})
				} else {
					r.send(pb.Message{To: m.From, Type: pb.MsgReadIndexResp, Index: ri, Entries: m.Entries})
				}
			}
		} else {//單節點情況
			r.readStates = append(r.readStates, ReadState{Index: r.raftLog.committed, RequestCtx: m.Entries[0].Data})
		}

		return nil
	}

MsgReadIndexResp消息

        從上面的流程中我們看到,當客戶端將只讀請求發送到Follower節點,Follower節點會將只讀請求發送到Leader節點,Leader節點進行一些列處理後會向該節點返回MsgReadIndexResp消息。下面我們主要看一下Follower接收到MsgReadIndexResp消息後的處理流程。

   Follower接收到MsgReadIndexResp消息會將該消息追加到該節點的readStates隊列中

    

func stepFollower(r *raft, m pb.Message) error {
	switch m.Type {
			case pb.MsgReadIndexResp:
		if len(m.Entries) != 1 {
			r.logger.Errorf("%x invalid format of MsgReadIndexResp from %x, entries count: %d", r.id, m.From, len(m.Entries))
			return nil
		}
		r.readStates = append(r.readStates, ReadState{Index: m.Index, RequestCtx: m.Entries[0].Data})
	}

從以上可以看到客戶端的只讀請求最終會寫入到客戶端請求節點的readStates隊列中,等待其他goroutine來處理,以上也是MsgReadIndex和MsgReadIndexResp類型消息的處理流程。

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