儲存和節點的建立
raftexample中的儲存其實有兩種,一個是通過raft.NewMemoryStorage()
進行建立的raft.raftStorage
,關聯到單個raft節點,另一個是通過newKVStore
建立的kv儲存,用於服務來自外部的訪問。
節點啟動時raft.raftStorage
的載入
上一篇中主要圍繞replayWAL介紹wal的讀寫,到本文為止可以完整拼接出該函式的處理邏輯。其中snapshot的作用是通過index限定了載入的wal日誌的範圍。
-
一開始會通過
loadSnapshot
函式找出一個有效且最新的snapshot。保證有效的方式是:從raftNode.waldir
中讀取所有snapshotType
型別的資料(snaps),以及最新的stateType
型別的資料(state),然後過濾出所有index<=state.Commit
的snapshot(state.Commit
表示已提交的日誌的index,從本地載入的snapshot的index不能超過state.Commit
,否則被認為是無效的歷史資料);保證最新的方式是:通過對比raftNode.snapdir
(注意對比時檔案是倒序的)和上一步獲取到的snaps(即通過對比Snapshot.Term
和Snapshot.Index
),找出最新的snapshot。 -
openWAL
函式根據第1步中獲取到的snapshot的index找出符合條件(即index<=snapshot.index)的wal檔案 -
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的範圍。
三者的大小關係如下:snapshot.Index<=state.Commit,ents>snapshot.Index。
後續就是將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的資料和儲存的資料有交叉,這種情況需要剔除重疊的資料,並追加新的資料。
snapshot中除了儲存了索引相關的內容,還儲存了與叢集狀態有關的資訊(snapshot.Metadata.ConfState),用於恢復叢集狀態。
kvStore
的儲存
節點執行中主要通過raftNode.commitC
來通知kvStore。一種是在接收到snapshot時先儲存snapshot,然後通過給rc.commitC傳遞nil
來觸發kvStore從snap目錄中載入snapshot;另一種就是直接通過rc.commitC將接收到的entries儲存到kvStore。
raftLog
raft通過raftLog與raftStorage
儲存進行互動,unstable可以看作是storage的快取,儲存著未進入raftStorage的資料(entires和snapshot)。當需要獲取raftLog的首末索引時,會優先從unstable中查詢,若找不到,再從raftStorage中查詢。
raftNode的建立
下圖展示了raft節點的建立過程,從下圖可以看到,raft節點啟動時主要涉及的是raftLog
、ProgressTracker
和msgs
。上一節已經講過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狀態
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傳送給其他節點,反之亦然。
TIPs
-
etcd的raft角色有三種:leader、follower、learner。在etcd 3.4之前出現可能會出現如下問題:
- 新加入一個節點,leader會將快照同步到該節點,但如果快照數量過大,可能會導致超時,導致節點加入失敗
- 新加一個節點時,如果新的節點配置錯誤(如url錯誤),可能會導致raft選舉失敗,叢集不可用
為了避免如上問題,加入了一個新的角色learner,它作為一個單獨的節點,在日誌同步完成之前不參與選舉,etcd中需要通過
member promote
命令來讓learner參與選舉。