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類型消息的處理流程。