etcd raft 處理流程圖系列3-wal的儲存和執行

charlieroro發表於2021-08-30

儲存和節點的建立

raftexample中的儲存其實有兩種,一個是通過raft.NewMemoryStorage()進行建立的raft.raftStorage,關聯到單個raft節點,另一個是通過newKVStore建立的kv儲存,用於服務來自外部的訪問。

節點啟動時raft.raftStorage的載入

上一篇中主要圍繞replayWAL介紹wal的讀寫,到本文為止可以完整拼接出該函式的處理邏輯。其中snapshot的作用是通過index限定了載入的wal日誌的範圍。

  1. 一開始會通過loadSnapshot函式找出一個有效且最新的snapshot。保證有效的方式是:從raftNode.waldir中讀取所有snapshotType型別的資料(snaps),以及最新的stateType型別的資料(state),然後過濾出所有index<=state.Commit的snapshot(state.Commit表示已提交的日誌的index,從本地載入的snapshot的index不能超過state.Commit,否則被認為是無效的歷史資料);保證最新的方式是:通過對比raftNode.snapdir(注意對比時檔案是倒序的)和上一步獲取到的snaps(即通過對比Snapshot.TermSnapshot.Index),找出最新的snapshot。

  2. openWAL函式根據第1步中獲取到的snapshot的index找出符合條件(即index<=snapshot.index)的wal檔案

  3. ReadAll()會從上一步的wal檔案中讀取index大於snapshot.index的entryType型別的表項(wal的start就是snapshot),以及stateType型別的狀態資訊。

    case entryType:
    	e := mustUnmarshalEntry(rec.Data)
    	// 0 <= e.Index-w.start.Index - 1 < len(ents)
    	if e.Index > w.start.Index {
    	 // prevent "panic: runtime error: slice bounds out of range [:13038096702221461992] with capacity 0"
    		up := e.Index - w.start.Index - 1
    		if up > uint64(len(ents)) {
    			// return error before append call causes runtime panic
    			return nil, state, nil, ErrSliceOutOfRange
    		}
    		// The line below is potentially overriding some 'uncommitted' entries.
    		ents = append(ents[:up], e)
    	}
    	w.enti = e.Index
    

總結一下,上面的關係如下,state限定了讀取的snapshot的範圍(起點),而snapshot限定了讀取的ents的範圍。

etcd raft 處理流程圖系列3-wal的儲存和執行

三者的大小關係如下:snapshot.Index<=state.Commit,ents>snapshot.Index。

etcd raft 處理流程圖系列3-wal的儲存和執行

後續就是將snapshot、state和ents儲存到raftStorage中(raftexample的儲存實際並不支援持久化,下面以"落庫"表示將資料儲存到儲存中),需要注意的是,落庫的內容也受snapshot的限制,即只儲存index>snapshot.index的表項(ApplySnapshot函式會使用snapshot的覆蓋原有資料,由此可見raftStorage中並沒有儲存完整的資料)。從上圖可以看出所有處理邏輯都以state為起點進行的,state表示raft的處理結果,可以直接落庫。當儲存snapshot時,需要與儲存中的snapshot進行比較,只有當index大於儲存中的snapshot的index時才會被儲存。在儲存ents時,由於ReadAll介面(見上)對讀取的ents的範圍作了限制,因此只可能出現以下3種情況:1)ents的資料的最大index小於儲存中的最小index,這種情況不做任何處理;2)ents的資料的最小index等於儲存中的最小index,這種情況直接追加到儲存即可;3)ents的資料和儲存的資料有交叉,這種情況需要剔除重疊的資料,並追加新的資料。

etcd raft 處理流程圖系列3-wal的儲存和執行

snapshot中除了儲存了索引相關的內容,還儲存了與叢集狀態有關的資訊(snapshot.Metadata.ConfState),用於恢復叢集狀態。

kvStore的儲存

節點執行中主要通過raftNode.commitC來通知kvStore。一種是在接收到snapshot時先儲存snapshot,然後通過給rc.commitC傳遞nil來觸發kvStore從snap目錄中載入snapshot;另一種就是直接通過rc.commitC將接收到的entries儲存到kvStore。

etcd raft 處理流程圖系列3-wal的儲存和執行

raftLog

raft通過raftLog與raftStorage儲存進行互動,unstable可以看作是storage的快取,儲存著未進入raftStorage的資料(entires和snapshot)。當需要獲取raftLog的首末索引時,會優先從unstable中查詢,若找不到,再從raftStorage中查詢。

etcd raft 處理流程圖系列3-wal的儲存和執行

raftNode的建立

下圖展示了raft節點的建立過程,從下圖可以看到,raft節點啟動時主要涉及的是raftLogProgressTrackermsgs。上一節已經講過raftlog,它作為raft的儲存,包括unstable和storage。ProgressTracker是raft中用來跟蹤叢集(config)以及各個節點的狀態(prs)。raft叢集中的節點分為Voters,Learners和LearnersNext三大類,其中前兩個是互斥的,即一個raft節點只能屬於其中一類。最後一類存在的原因是為了在voters(outgoing)向learner轉換過程中保證一致性(受raft的joint行為限制,raft的狀態變更是通過joint方式運作的),是個臨時狀態。Voters又分為incoming(Voters[0])和outgoing(Voters[1])兩種,分別表示新狀態和原始狀態。當發生狀態變更時,會將當前狀態拷貝到outgoing中,根據新狀態型別(ConfChangeAddNode/ConfChangeAddLearnerNode/ConfChangeRemoveNode/ConfChangeUpdateNode)來執行joint操作,如果raft處於joint狀態(outgoing長度大於0),則不能再次執行joint(有可能存在中間狀態)。

raftLog中有兩個標識:committed和applied,committed標識已經進入storage中的最大日誌位置,而applied表示已經處理過的訊息(如配置變更等)。其中applied <= committed。

ProgressTracker中儲存的節點狀態有如下三種:

  • StateProbe:每個心跳週期只能傳送一個複製訊息,同時用來探測follower的進度。
  • StateReplicate:為follower可以快速接收賦值日誌的理想狀態
  • StateSnapshot:在傳送賦值訊息前需要傳送snapshot,來讓follower轉變為StateReplicate狀態
etcd raft 處理流程圖系列3-wal的儲存和執行

原圖地址

raft的joint操作以及LearnersNext存在的原因見下:

	// When we turn a voter into a learner during a joint consensus transition,
	// we cannot add the learner directly when entering the joint state. This is
	// because this would violate the invariant that the intersection of
	// voters and learners is empty. For example, assume a Voter is removed and
	// immediately re-added as a learner (or in other words, it is demoted):
	//
	// Initially, the configuration will be
	//
	//   voters:   {1 2 3}
	//   learners: {}
	//
	// and we want to demote 3. Entering the joint configuration, we naively get
	//
	//   voters:   {1 2} & {1 2 3}
	//   learners: {3}
	//
	// but this violates the invariant (3 is both voter and learner). Instead,
	// we get
	//
	//   voters:   {1 2} & {1 2 3}
	//   learners: {}
	//   next_learners: {3}
	//
	// Where 3 is now still purely a voter, but we are remembering the intention
	// to make it a learner upon transitioning into the final configuration:
	//
	//   voters:   {1 2}
	//   learners: {3}
	//   next_learners: {}
	//
	// Note that next_learners is not used while adding a learner that is not
	// also a voter in the joint config. In this case, the learner is added
	// right away when entering the joint configuration, so that it is caught up
	// as soon as possible.

上圖中的涉及新raft節點的建立,整個過程也比較簡單。首先根據replayWAL獲取到的snapshot來恢復叢集和節點的狀態,然後根據本節點是否是leader來執行更新raftLog的提交記錄和訊息傳送。

raftNode的執行

下圖給出了raftNode的基本運作流程,僅含初始的選舉流量,但後續處理方式也大體類似,區別在於傳遞和處理的訊息型別不同。在raftExample的講解中可以看到啟動了一個serveChannels的服務,該服務用於從n.readyc中接收封裝好的ready訊息,然後進行儲存、應用(配置)和傳送等操作,最後通過n.advancec通知node節點處理結果,node以此執行acceptReady操作,更新提交記錄和應用記錄等。

serveChannels中會建立一個100ms的時鐘,定時向n.tickc傳送觸發訊息。一開始,所有節點角色都是follower,此時會觸發tickElection(follower和candidate為tickElection,leader為tickHeartbeat),嘗試將角色轉變為candidate。此外ProgressTracker.Votes中儲存了支援本節點作為leader的票數,超過一半節點同意時會轉變為leader。

n.readyc中傳遞的ready訊息封裝了本節點的狀態變更以及待傳送的資訊,n.readyc中的訊息進而會傳遞給serveChannels,後續通過transport.send傳送給其他節點,反之亦然。

etcd raft 處理流程圖系列3-wal的儲存和執行

TIPs

  • etcd的raft角色有三種:leader、follower、learner。在etcd 3.4之前出現可能會出現如下問題:

    • 新加入一個節點,leader會將快照同步到該節點,但如果快照數量過大,可能會導致超時,導致節點加入失敗
    • 新加一個節點時,如果新的節點配置錯誤(如url錯誤),可能會導致raft選舉失敗,叢集不可用

    為了避免如上問題,加入了一個新的角色learner,它作為一個單獨的節點,在日誌同步完成之前不參與選舉,etcd中需要通過member promote命令來讓learner參與選舉。

參考

儲存模組原始碼簡析

etcd的raft實現之tracker&quorum

相關文章