etcd系列-----raft協議:重要數據結構介紹(Entry、Message、storage、unstable)

一些基礎結構體介紹:

Entry

Entry記錄:在前面介紹Raft協議時提到,節點之間傳遞的是消息(Message), 每條消息中可以攜帶多條Entry記錄,每條Entry記錄對應一個獨立的操作。
在Entry中其中封裝瞭如下信息:
    :Term( uint64類型): 該Entry所在的任期號。
    : Index ( uint64類型): 該Entry對應的索引號。
    :Type( EntryType 類型):該Entry記錄的類型。 該字段有兩個可選項:一個是Entry Normal,表示普通的數據操作;另一個是EntryConfChange,表示集羣的變更操作。
    : Data ( []byte類型): 具體操作使用的數據。

記錄的本地Log的基本單位也是Entry記錄。有的文章也會將Entry記錄稱爲“日誌記錄”,在etcd 中還有一個WAL日誌的概念,這兩者並非完全等價,所以需要注意一下,避免兩者混淆。

Message

在每個節點中在raft模塊中另一個比較重要結構體就是raftpb.Message。在raft模塊的實現中,Message是所有消息的抽象,包括了各種類型消息所需要的字段,其中核心宇段的含義如下。
    :Type ( MessageType類型): 該字段定義了消息的類型,raft實現中就是通過該字段區分不同的消息井進行分類處理的,MessageType中共定義了19 種消息類型,後面會介紹每種消息類型的含義及相應的處理方式。
    :From ( uint64 類型): 發送消息的節點ID。在集羣中,每個節點都擁有一個唯一ID作爲標識。
    :To ( uint64類型):消息的目標節點ID。
    :Term ( uint64類型):發送消息的節點的Term值。 如果Term值爲0,則爲本地消息,在etcd刊負模塊的實現中,對本地消息進行特殊處理。
    :Entries ( []Entry類型): 如果是MsgApp類型的消息,則該字段中保存了Leader節點複製到Follower節點的Entry記錄。在其他類型消息中,該字段的含義後面會詳細介紹。
    :LogTerm ( uint64類型):該消息攜帶的第一條Entry記錄的Term值。
    :Index ( uint64 類型):記錄索引值,該索引值的具體含義與消息的類型相關。例如,MsgApp消息的Index宇段保存了其攜帶的Entry記錄(即Entries字段)中前一條記錄的Index值,而MsgAppResp消息的Index字段則是Follower節點提示Leader節點下次從哪個位置開始發送Entry記錄。
    :Commit ( uint64類型): 消息發送節點的提交位置(commitlndex)。
    :Snapshot ( Snapshot類型):在傳輸快照時,該字段保存了快照數據。
    :Reject ( bool 類型): 主要用於響應類型的消息,表示是否拒絕收到的消息。 例如,Follower節點收到Leader節點發來的MsgApp消息,如果Follower節點發現MsgApp消息攜帶的Entry記錄並不能直接追加到本地的raftLog中, 則會將響應消息的Reject宇段設置爲true,並且會在RejectHint字段中記錄合適的Entry索引值,供Leader節點參考。
    :RejectHint ( uint64類型):在Follower節點拒絕Leader節點的消息之後,會在該字段記錄一個Entry索引值供Leader節點。

raft結構體

在etcd-ra負模塊中,raft結構體是其核心數據結構,在結構體raft中封裝了當前節點所有的
核心數據。先看其核心字段。
    :id  ( uint64類型): 當前節點在集羣的ID
    :Term ( uint64類型): 當前任期號。如果Message的Term字段爲0,則表示該消息是本地消息,例如,MsgHup、 MsgProp、 MsgReadlndex 等消息,都屬於本地消息。
    :Vote ( uint64類型):當前任期中當前節點將選票投給了哪個節點,未投票時, 該字段爲None。
    :raftlog( *raftlog類型): 在前面介紹過,在Ra企協議中的每個節點都會記錄本地Log,在raft模塊中,使用結構體raftLog表示本地Log, 在raftLog中還涉及日誌的緩存等相關內容,後面會介紹。
    :maxlnflight ( int 類型): 對於當前節點來說,己經發送出去但未收到響應的消息個數上限。如果處於該狀態的消息超過maxlnflight這個|現值,則暫停當前節點的消息發送,這是爲了防止集羣中的某個節點不斷髮送消息,引起網絡阻塞或是壓垮其他節點, 從而影響其他節點的正常運行。
    :maxMsgSize ( uint64類型): 單條消息的最大字節數。
    :prs ( map[uint64]*Progress類型): 在前面介紹Raft協議時提到過, Leader 節點會記錄集羣中其他節點的日誌複製情況(Nextlndex和Matchlndex)。在raft模塊中,每個Follower節點對應的Nextlndex值和Matchlndex值都封裝在Progress實例中, 除此之外, 每個Progress實例中還封裝了對應Follower節點的相關信息, 這裏簡單介紹主要字段。
         .Match ( uint64類型): 對應Follower節點當前己經成功複製的Entry記錄的索引值。
         .Next ( uint64類型): 對應Follower節點下一個待複製的Entry記錄的索引值。
         .State( ProgressState丁ype類型): 對應Follower節點的複製狀態, 其可選項的含義後面詳細介紹。
         .Paused ( boo I 類型):當前Leader節點是否可以向該Progress實例對應的Follower節點發送消息。
         .PendingSnapshot ( uint64類型): 當前正在發送的快照數據信息。
         .RecentActive ( bool類型): 從當前Leader節點的角度來看,該Progress實例對應的Follower節點是否存活。
         .ins ( *inflights類型): 記錄了己經發送出去但未收到響應的消息信息。
    :state (StateType 類型): 當前節點在集羣中的角色,可選值分爲StateFollower、StateCandidate、 StateLeader和StatePreCandidat巳四種狀態。
    :votes (map[uint64]bool類型):在選舉過程,如果當前節點收到了來自某個節點的投票, 則會將votes 中對應的值設置爲true,通過統計votes這個map, 就可以確定當前節點收到的投票是否超過半數。
    :msgs ([]pb. Message類型): 緩存了當前節點等待發送的消息。
    :lead ( uint64類型): 當前集羣中Leader節點的ID。
    :electionElapsed ( int 類型):選舉計時器的指針,其單位是邏輯時鐘的刻度,邏輯時鐘每推進一次,該字段值就會增加1。
    :electionTimeout ( int 類型): 選舉超時時間,當electionE!apsed 宇段值到達該值時,就會觸發新一輪的選舉。
    :heartbeatElapsed ( int 類型): 心跳計時器的指針,其單位也是邏輯時鐘的刻度,邏輯時鐘每推進一次,該字段值就會增加1 。
    :heartbeatTimeout ( int類型):心跳超時時間,當heartbeatElapsed字段值到達該值時,就會觸發Leader節點發送一條心跳消息。
    :tick ( func()類型): 當前節點推進邏輯時鐘的函數。如果當前節點是Leader,則指向raft.tickHeartbeat()函數,如果當前節點是Follower 或是Candidate,則指向raft.tickElection()函數。
    :step ( stepFunc類型): 當前節點收到消息時的處理函數。如果是Leader節點, 則該字段指向stepLeader()函數,如果是Follower節點,則該字段指向stepFollower()函數,如果是處於preVote階段的節點或是Candidate節點,則該字段指向stepCandidate()函數。

Config結構體

Config結構體主要用於配置參數的傳遞,在創建raft實例時需要的參數會通過Config實例傳遞進去。 Config的主要字段如下。
    :ID ( uint64類型):當前節點的ID。
    :peers ( []uint64類型):記錄了集羣中所有節點的ID。
    :ElectionTick ( int類型):用於初始化raft.electionTimeout,即邏輯時鐘連續推進多少次後,就會觸發Follower節點的狀態切換及新一輪的Leader選舉。
    :HeartbeatTick ( int 類型):用於初始化raftheartbeatTimeout,即邏輯時鐘連續推進多少次後,就觸發Leader節點發送心跳消息。
    :Storage ( Storage類型): 當前節點保存raft日誌記錄使用的存儲,後面會j接收其接口及其實現。
    :Applied ( uint64類型):當前已經應用的記錄位置(己應用的最後一條Entry記錄的索引值),該值在節點重啓時需要設置,否則會重新應用己經應用過ntry記錄。
    :MaxSizePerMsg ( uint64類型):用於初始化raft.maxMsgSize字段,每條消息的最大字節數。
    :MaxlnflightMsgs ( int類型):用於初始化ra丘maxlnflight,即已經發送出去且未收到響應的最大消息個數。

Storage

MemoryStorage 是raft模塊爲Storage 接口提供的一個實現,從名字也能看出,MemoryStorage在內存中維護上述狀態信息(hardState字段)、快照數據(snapshot宇段)及所有的Entry記錄(ents 字段,[]raftpb.Entry類型〕,在MemoryStorage.ents字段中維護了快照數據之後的所有Entry記錄。另外需要注意的是,MemoryStorage繼承了sync.Mutex, MemoryStorage 中的大部分操作是需要加鎖同步的。 通過這裏的介紹,我們大概可以瞭解MemoryStorage的結構,如圖示

 MemoryStorage 中追加Entry記錄,該功能主要由MemoryStorage.Append()方法完成:

func (ms *MemoryStorage) Append(entries []pb.Entry) error {
	if len(entries) == 0 {
		return nil
	}

	ms.Lock()
	defer ms.Unlock()

	first := ms.firstIndex()
	last := entries[0].Index + uint64(len(entries)) - 1

	// shortcut if there is no new entry.
	if last < first {
		return nil//entries切片中所有的Entry都已經過時,無須添加任何Entry
	}
	//first之前的Entry已經記入Snapshot中,不應該再記錄到ents中,所以將這部分Entry截掉
	if first > entries[0].Index {
		entries = entries[first-entries[0].Index:]
	}
    //計算entries切片中第一條可用的Entry與first之間的差距
	offset := entries[0].Index - ms.ents[0].Index
	switch {
	case uint64(len(ms.ents)) > offset:
	    //保留MemoryStorage.ents中first~offset的部分,offset之後的部分被拋棄
	    //然後將待追加的Entry追加到MemoryStorage.ents中
		ms.ents = append([]pb.Entry{}, ms.ents[:offset]...)
		ms.ents = append(ms.ents, entries...)
	case uint64(len(ms.ents)) == offset:
	    //直接將待追加的日誌記錄(entries)追加到MemoryStorage中
		ms.ents = append(ms.ents, entries...)
	default:
		raftLogger.Panicf("missing log entry [last: %d, append at: %d]",
			ms.lastIndex(), entries[0].Index)
	}
	return nil
}

隨着系統的運行, MemoryStorage.ents 中保存的En位y記錄會不斷增加,爲了減小內存的壓力,定期創建快照來記錄當前節點的狀態並壓縮MemoryStorage.ents數組的空間是非常有必要的, 這樣就可以降低內存使用。這個過程中涉及三個方法, 首先是CreateSnapshot()方法, 它會接收當前集羣狀態,以及SnapShot相關數據來更新snapshot字段,具體實現如下:

//簡單說明該方法的參數: i是新建Snapshot包含的最大的索引值,cs是當前集羣的狀態,data是新建Snapshot的具體數據
func (ms *MemoryStorage) CreateSnapshot(i uint64, cs *pb.ConfState, data []byte) (pb.Snapshot, error) {
	ms.Lock()
	defer ms.Unlock()
	//邊界檢查,l必須大於當前Snapshot包含的最大Index佳,並且小於MemoryStorage的LastIndex佳,否則拋出異常
	if i <= ms.snapshot.Metadata.Index {
		return pb.Snapshot{}, ErrSnapOutOfDate
	}

	offset := ms.ents[0].Index
	if i > ms.lastIndex() {
		raftLogger.Panicf("snapshot %d is out of bound lastindex(%d)", i, ms.lastIndex())
	}
    //更新MemoryStorage.snapshot的元數據
	ms.snapshot.Metadata.Index = i
	ms.snapshot.Metadata.Term = ms.ents[i-offset].Term
	if cs != nil {
		ms.snapshot.Metadata.ConfState = *cs
	}
	//更新具體的快照數據
	ms.snapshot.Data = data
	return ms.snapshot, nil
}

新建Snapshot之後,一般會調用MemoryStorage.Compact()方法將MemoryStorage.ents中指定索引之前的Entry記錄全部拋棄,從而實現壓縮MemoryStorage.ents 的目的,具體實現如下:

func (ms *MemoryStorage) Compact(compactIndex uint64) error {
	ms.Lock()
	defer ms.Unlock()
	offset := ms.ents[0].Index
	//邊界檢測
	if compactIndex <= offset {
		return ErrCompacted
	}
	if compactIndex > ms.lastIndex() {
		raftLogger.Panicf("compact %d is out of bound lastindex(%d)", compactIndex, ms.lastIndex())
	}
    //創建新的切片,用來存儲compactIndex之後的Entry
	i := compactIndex - offset
	ents := make([]pb.Entry, 1, 1+uint64(len(ms.ents))-i)
	ents[0].Index = ms.ents[i].Index
	ents[0].Term = ms.ents[i].Term
	//將compactlndex之後的Entry拷貝到ents中,並更新MemoryStorage.ents 字段
	ents = append(ents, ms.ents[i+1:]...)
	ms.ents = ents
	return nil
}

 最後,上層模塊可以通過MemoryStorage.Snapshot()方法獲取SnapShot。

unstable結構體

unstable 使用內存數組維護其中所有的Entry記錄,對於Leader節點而言,它維護了客戶端請求對應的Entry記錄;對於Follower節點而言,它維護的是從Leader節點複製來的Entry記錄。無論是Leader節點還是Follower節點,對於剛剛接收到的Entry記錄首先都會被存儲在unstable中。然後按照Raft協議將unstable中緩存的這些Entry記錄交給上層模塊進行處理,上層模塊會將這些Entry記錄發送到集羣其他節點或進行保存(寫入Storage中)。之後,上層模塊會調用Advance()方法通知底層的raft模塊將unstable 中對應的Entry記錄刪除(因爲己經保存到了Storage中)。正因爲unstable中保存的Entry記錄並未進行持久化,可能會因節點故障而意外丟失,所以被稱爲unstable。

unstable中的主要字段。
     :entries ( []pb.Entry類型):用於保存未寫入Storage中的Entry記錄。
     :offset ( uint64類型): entries 中的第一條Entry記錄的索引值。
     :snapshot (pb.Snapshot類型):快照數據,該快照數據也是未寫入Storage中的。

在unstable 中提供了很多與Storage類似的方法,在raftLog中,很多方法都是先嚐試調用unstable的相應方法,在其失敗後(unstable的方法返回(0, false)即表示失敗),再嘗試調用Storage的對應方法。
unstable.maybeFirstlndex()方法會嘗試獲取unstable 的第一條Entry 記錄的索引值,unstable.maybeLastlndex()方法會嘗試獲取unstable 的最後一條Entry記錄的索引值,如果獲取失敗則返回(0, false),unstable.maybeTerm()方法的主要功能是嘗試獲取指定Entry記錄的Term值,根據條件查找指定的Entry記錄的位置。

當unstable.entries 中的Entry記錄己經被寫入Storage之後,會調用unstable.stableTo()方法清除entries 中對應的Entry記錄,stableTo()方法的具體實現如下:

func (u *unstable) stableTo(i, t uint64) {
    //查找指定Entry記錄的Term佳,若查找失敗則表示對應的Entry不在unstable中,直接返回
	gt, ok := u.maybeTerm(i)
	if !ok {
		return
	}
	// if i < offset, term is matched with the snapshot
	// only update the unstable entries if term is matched with
	// an unstable entry.
	if gt == t && i >= u.offset {
	//指定索引位之前的Entry記錄都已經完成持久化,則將其之前的全部Entry記錄刪除
		u.entries = u.entries[i+1-u.offset:]
		u.offset = i + 1
		//隨着多次追加日誌和截斷日誌的操作unstable.entires底層的數組會越來越大,shrinkEntriesArray方法會在底層數組長度超過實際佔用的兩倍時,對底層數據進行縮減
		u.shrinkEntriesArray()
	}
}
func (u *unstable) shrinkEntriesArray() {
	// We replace the array if we're using less than half of the space in
	// it. This number is fairly arbitrary, chosen as an attempt to balance
	// memory usage vs number of allocations. It could probably be improved
	// with some focused tuning.
	const lenMultiple = 2
	if len(u.entries) == 0 {
		u.entries = nil
	} else if len(u.entries)*lenMultiple < cap(u.entries) {
        //重新創建切片,複製原有切片中的數據,重直entries字段
		newEntries := make([]pb.Entry, len(u.entries))
		copy(newEntries, u.entries)
		u.entries = newEntries
	}
}

 同理,當unstable.snapshot字段指向的快照被寫入Storage之後, 會調用unstable.stableSnapTo()方法將snapshot字段清空.unstable.truncateAndAppend()方法的主要功能是向unstable.entries中追加Entry記錄其實現與Storage.Append()方法類似也會涉及截斷的場景:

func (u *unstable) truncateAndAppend(ents []pb.Entry) {
    //獲取第一條待追加的Entry記錄的索引值
	after := ents[0].Index
	switch {
	case after == u.offset+uint64(len(u.entries)):
		// after is the next index in the u.entries
		// directly append
		//若待追加的記錄與e口tries中的記錄正好連續,則可以直接向entries中追加
		u.entries = append(u.entries, ents...)
	case after <= u.offset:
	//直接用待追加的Entry記錄替換當前的entries字段, 並支新offset
		u.logger.Infof("replace the unstable entries from index %d", after)
		// The log is being truncated to before our current offset
		// portion, so set the offset and replace the entries
		u.offset = after
		u.entries = ents
	default:
	    //after在offset~last之間,則after~last之間的Entry記錄衝突。 這裏會將offset~after 之間的記錄保留,拋棄after之後的記錄,然後完成追加操作
        //unstable.slice()方法會檢測after是否合法,並返回offset~after的切片
		// truncate to after and copy to u.entries
		// then append
		u.logger.Infof("truncate the unstable entries before index %d", after)
		u.entries = append([]pb.Entry{}, u.slice(u.offset, after)...)
		u.entries = append(u.entries, ents...)
	}
}

 

 

 

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