一文帶你瞭解 Raft 一致性協議的關鍵點

此文已由做者孫建良受權網易雲社區發佈。git

歡迎訪問網易雲社區,瞭解更多網易技術產品運營經驗。github

Raft 協議的發佈,對分佈式行業是一大福音,雖然在覈心協議上基本都是師繼 Paxos 祖師爺(lamport) 的精髓,基於多數派的協議。可是 Raft 一致性協議的貢獻在於,定義了可易於實現的一致性協議的事實標準。把一致性協議從 「陽春白雪」 變成了讓普通學生、IT 碼農等均可以上手試一試玩一玩的東西,MIT 的分佈式教學課程 6.824 都是直接使用 Raft 來介紹一致性協議。web

從《In Search of An Understandable Consensus Algorithm (Extend Version)》論文中,咱們能夠看到,與其餘一致性協議的論文不一樣的點是,Diego 基本已經算是把一個易於工程實現的算法講得很是明白了,just do it,沒有太多爭議和發揮的空間,即使如此,要實現一個工業級的靠譜的 Raft 仍是要花很多力氣。算法

Raft 一致性協議相對來講易於實現主要歸結爲如下幾個緣由:json

○ 模塊化的拆分:把一致性協議劃分爲 Leader 選舉、MemberShip 變動、日誌複製、SnapShot 等相對比較解耦的模塊;安全

○ 設計的簡化:好比不容許相似 Paxos 算法的亂序提交、使用 Randomization 算法設計 Leader Election 算法以簡化系統的狀態,只有 Leader、Follower、Candidate 等等。網絡

本文不打算對 Basic Raft 一致性協議的具體內容進行說明,而是介紹記錄一些關鍵點,由於絕大部份內容原文已經介紹的很詳實,有意者還可把 Raft 做者 Diego Ongaro 200 多頁的博士論文刷一遍(連接在文末,可自取)。併發

Pointsapp

Old Term LogEntry 處理dom

舊 Term 未提交日誌的提交依賴於新一輪的日誌的提交

這個在原文 「5.4.2 Committing entries from previews terms」 有說明,可是在看的時候可能會以爲有點繞。

Raft 協議約定,Candidate 在使用新的 Term 進行選舉的時候,Candidate 可以被選舉爲 Leader 的條件爲:

○ 獲得一半以上(包括本身)節點的投票
○ 獲得投票的前提是:Candidate 節點的最後一個LogEntry 的 Term 比投票節點大,或者在 Term 同樣狀況下,LogEnry 的 SN (serial number) 必須大於等於投票者。

而且有一個安全截斷機制:

○ Follower 在接收到 logEntry 的時候,若是發現發送者節點當前的 Term 大於等於 Follower 當前的 Term;而且發現相同序號的(相同 SN)LogEntry 在 Follower 上存在,未 Commit,而且 LogEntry Term 不一致,那麼 Follower 直接截斷從 (SN~文件末尾)的全部內容,而後將接收到的 LogEntryAppend 到截斷後的文件末尾。

在以上條件下,Raft 論文列舉了一個 Corner Case ,以下圖所示:

○ (a):S1 成爲 Leader,Append Term2 的LogEntry(黃色)到 S一、S2 成功;
○ (b):S1 Crash,S5 使用 Term(3) 成功競選爲 Term(3) 的 Leader(經過得到 S三、S四、S5 的投票),而且將 Term 爲 3 的 LogEntry(藍色) Append 到本地;
○ (c):S5 Crash, S1 使用 Term(4) 成功競選爲Leader(經過得到 S一、S二、S3 的投票),將黃色的 LogEntry 複製到 S3,獲得多數派響應(S一、S二、S3) 的響應,提交黃色 LogEntry 爲 Commit,並將 Term 爲 4 的 LogEntry (紅色) Append 到本地;
○ (d) :S5 使用新的 Term(5) 競選爲 Leader (獲得 S二、S三、S4 的投票),按照協議將全部全部節點上的黃色和紅色的 LogEntry 截斷覆蓋爲本身的 Term 爲 3 的 LogEntry。

進行到這步的時候咱們已經發現,黃色的 LogEnry(2) 在被設置爲 Commit 以後從新又被否認了。

因此協議又強化了一個限制;

○ 只有當前 Term 的 LogEntry 提交條件爲:知足多數派響應以後(一半以上節點 Append LogEntry 到日誌)設置爲 commit;
○ 前一輪 Term 未 Commit 的 LogEntry 的 Commit 依賴於高輪 Term LogEntry 的 Commit

如圖所示 (c) 狀態 Term2 的 LogEntry(黃色) 只有在 (e)狀態 Term4 的 LogEntry(紅色)被 commit 纔可以提交。

提交 NO-OP LogEntry 提交系統可用性

在 Leader 經過競選剛剛成爲 Leader 的時候,有一些等待提交的 LogEntry (即 SN > CommitPt 的 LogEntry),有多是 Commit 的,也有多是未 Commit 的(PS: 由於在 Raft 協議中 CommitPt 不用實時刷盤)。

因此爲了防止出現非線性一致性(Non Linearizable Consistency);即以前已經響應客戶端的已經 Commit 的請求回退,而且爲了不出現上圖中的 Corner Case,每每咱們須要經過下一個 Term 的 LogEntry 的 Commit 來實現以前的 Term 的 LogEntry 的 Commit (隱式commit),才能保障提供線性一致性。

可是有可能接下來的客戶端的寫請求不能及時到達,那麼爲了保障 Leader 快速提供讀服務,系統可首先發送一個 NO-OP LogEntry 來保障快速進入正常可讀狀態。

Current Term、VotedFor 持久化

上圖其實隱含了一些須要持久化的重要信息,即 Current Term、VotedFor! 爲何(b) 狀態 S5 使用的 Term Number 爲 3,而不是 2?

由於競選爲 Leader 就必須是使用新的 Term 發起選舉,而且獲得多數派階段的贊成,贊成的操做爲將 Current Term、VotedFor 持久化。

好比(a) 狀態 S1 爲何能競選爲 Leader?首先 S1 知足成爲 Leader 的條件,S2~S5 均可以接受 S1 成爲發起 Term 爲 2 的 Leader 選舉。S2~S5 贊成 S1 成爲 Leader 的操做爲:將 Current Term 設置爲 二、VotedFor 設置爲 S2 而且持久化,而後返回 S1。即 S1 成功成爲 Term 爲 2 的 Leader 的前提是一個多數派已經記錄 Current Term 爲 2 ,而且 VotedFor 爲 S2。那麼 (b) 狀態 S5 如使用 Term 爲 2 進行 Leader 選舉,必然得不到多數派贊成,由於 Term 2 已經投給 S1,S5 只能 將 Term++ 使用Term 爲3 進行從新發起請求。

Current Term、VotedFor 如何持久化?
type CurrentTermAndVotedFor {
Term int64 json:"Term"
VotedFor int64 json:"Votedfor"
Crc int32
}
//current state
var currentState CurrentTermAndVotedFor
.. set value and calculate crc ...
content, err := json.Marshal(currentState)

//flush to disk
f, err := os.Create("/dist/currentState.txt")
f.Write(content)
f.Sync()

簡單的方法,只須要保存在一個單獨的文件,如上爲簡單的 go 語言示例;其餘簡單的方式好比在設計 Log File 的時候,Log File Header 中包含 Current Term 以及 VotedFor 的位置。

若是再深刻思考一層,其實這裏頭有一個疑問?如何保證寫了一半(寫入一半而後掛了)的問題?寫了 Term、沒寫 VoteFor?或者只寫了 Term 的高 32 位?
能夠看到磁盤可以保證 512 Byte 的寫入原子性,這個在知乎事務性 (Transactional)存儲須要硬件參與嗎?(連接見文末) 這個問答上就能找到答案。因此最簡單的方法是直接寫入一個 tmpfile,寫入完成以後,將 tmpfile mv 成CurrentTermAndVotedFor 文件,基本可保障更新的原子性。其餘方式好比採用 Append Entry 的方式也能夠實現。

Cluser Membership 變動

在 raft 的 Paper 中,簡要說明了一種一次變動多個節點的 Cluser Membership 變動方式。可是沒有給出更多的在 Security 以及 Avaliable 上的更多的說明。

其實如今開源的 Raft 實現通常都不會使用這種方式,好比 Etcd raft 都是採用了更加簡潔的一次只能變動一個節點的 「single Cluser MemberShip Change」 算法。

固然 single cluser MemberShip 並不是 Etcd 自創,其實 Raft 協議做者 Diego 在其博士論文中已經詳細介紹了 Single Cluser MemberShip Change 機制,包括 Security、Avaliable 方面的詳細說明,而且做者也說明了在實際工程實現過程當中更加推薦 Single 方式,首先由於簡單,再則全部的集羣變動方式均可以經過 Single 一次一個節點的方式達到任何想要的 Cluster 狀態。

原文:「Raft restrict the types of change that allowed: only one server can be added or removed from the cluster at once. More complex changes in membership are implemented as a series of single-server-change」.

Safty

回到問題的第一大核心要點:Safety,membership 變動必須保持 raft 協議的約束:同一時間(同一個 Term)只能存在一個有效的 Leader。

<一>:爲何不能直接變動多個節點,直接從 Old 變爲 New 有問題? for example change from 3 Node to 5 Node?

如上圖所示,在集羣狀態變動過程當中,在紅色箭頭處出現了兩個不相交的多數派(Server三、Server四、Server 5 認知到新的 5 Node 集羣;而 一、2 Server 的認知仍是處在老的 3 Node 狀態)。在網絡分區狀況下(好比 S一、S2 做爲一個分區;S三、S四、S5 做爲一個分區),2個分區分別能夠選舉產生2個新的 Leader(屬於configuration< Cold>的 Leader 以及 屬於 new configuration < Cnew > 的 Leader) 。

固然這就致使了 Safty 無法保證;核心緣由是對於 Cold 和 CNew 不存在交集,不存在一個公共的交集節點充當仲裁者的角色。

可是若是每次只容許出現一個節點變動(增長 or 減少),那麼 Cold 和 CNew 總會相交。 以下圖所示:

<二>: 如何實現 Single membership change

論文中提如下幾個關鍵點:

○ 因爲 Single 方式不管如何 Cold 和 CNew 都會相交,因此 raft 採用了直接提交一個特殊的 replicated LogEntry 的方式來進行 single 集羣關係變動。

○ 跟普通的 LogEntry 提交的不一樣點,configuration LogEntry 不須要 commit 就生效,只須要 append 到 Log 中便可。( PS: 原文 「The New configuration takes effect on each server as soon as it is added to the server’s log」)。

○ 後一輪 MemberShip Change 的開始必須在前一輪 MemberShip Change Commit 以後進行,以免出現多個 Leader 的問題。

關注點 1
如圖所示,如在前一輪 membership configure Change 未完成以前,又進行下一次 membership change 會致使問題,因此外部系統須要確保不會在第一次 Configuration 爲成功狀況下,發起另一個不一樣的 Configuration 請求。( PS:因爲增長副本、節點宕機丟失節點進行數據恢復的狀況都是由外部觸發進行的,只要外部節點可以確保在前一輪未完成以前發起新一輪請求,便可保障。)

關注點 2
跟其餘客戶端的請求不同的,Single MemberShip Change LogEntry 只須要 Append 持久化到 Log(而不須要 commit)就能夠應用。

一方面是可用性方面的考慮,以下所示:Leader S1 接收到集羣變動請求將集羣狀態從(S一、S二、S三、S4)變動爲 (S二、S三、S4);提交到全部節點以後 commit 以後,返回客戶端集羣狀態變動完成(以下狀態 a),S1 退出(以下狀態b);因爲 Basic Raft 並不須要 commit 消息實施傳遞到其餘 S一、S二、S3 節點,S1 退出以後,S一、S二、S3 因爲沒有接收到 Leader S1 的心跳,致使進行選舉,可是不幸的是 S4 故障退出。假設這個時候 S二、S3 因爲 Single MemberShip Change LogEntry 沒有 Commit 仍是以(S一、S二、S三、S4)做爲集羣狀態,那麼集羣無法繼續工做。可是實質上在(b)狀態 S1 返回客戶端集羣狀態變動請求完成以後,實質上是認爲可獨立進入正常狀態。

另外一方面,即便沒有提交到一個多數派,也能夠截斷,沒什麼問題。(這裏很少作展開)

另外一方面可靠性&正確性

Raft 協議 Configuration 請求和普通的用戶寫請求是能夠並行的,因此在併發進行的時候,用戶寫請求提交的備份數是沒法確保是在 Configuration Change 以前的備份數仍是備份以後的備份數。可是這個沒有辦法,由於在併發狀況下原本就無法保證,這是保證 Configuration 截斷系統持續可用帶來的代價。(只要確保在多數派存活狀況下不丟失便可(PS:一次變動一個節點狀況下,返回客戶端成功,其中必然存在一個提交了客戶端節點的 Server 被選舉爲Leader)。

關注點 3
Single membership change 其餘方面的 safty 保障是跟原始的 Basic Raft 是同樣的(在各個協議處理細節上對此類請求未有任何特殊待遇),即只要一個多數派(無論是新的仍是老的)將 single membership change 提交併返回給客戶端成功以後,接下來不管節點怎麼重啓,都會確保新的 Leader 將會在已經知曉(應用)新的,前一輪變動成功的基礎上處理接下來的請求:能夠是讀寫請求、固然也能夠是新的一輪 Configuration 請求。

初始狀態如何進入最小備份狀態

好比如何進入3副本的集羣狀態。可使用系統元素的 Single MemberShip 變動算法實現。

剛開始節點的副本狀態最簡單爲一個節點 1(本身贊成本身很是簡單),獲得返回以後,再選擇添加一個副本,達到 2個副本的狀態。而後再添加一個副本,變成三副本狀態,知足對系統可用性和可靠性的要求,此時該 raft 實例可對外提供服務。

其餘須要關注的事項

○ servers process incoming RPC requests without consulting their current configurations. server 處理在 AppendEntries & Voting Request 的時候不用考慮本地的 configuration 信息。

○ CatchUp:爲了保障系統的可靠性和可用性,加入 no-voting membership 狀態,進行 CatchUp,須要加入的節點將歷史 LogEntry 基本所有 Get 到以後再發送 Configuration。

○ Disruptive serves:爲了防止移除的節點因爲沒有接收到新的 Leader 的心跳,而發起 Leader 選舉而擾繞當前正在進行的集羣狀態。集羣中節點在 Leader 心跳租約期間內收到 Leader 選舉請求能夠直接 Deny。(PS:固然對於一些肯定性的事情,好比發現 Leader listen port reset,那麼能夠發起強制 Leader 選舉的請求)。

參考文獻:

Raft Paper:
https://raft.github.io/raft.pdf
Raft 博士論文:
https://web.stanford.edu/~ous...
事務性(Transactional)存儲須要硬件參與嗎?
https://www.zhihu.com/questio...

文章來源: 網易雲社區

相關文章
相關標籤/搜索