前言
之前在學習MongoDB復制集時發現網上的很多相關的分享都是針對r3.2.0以前的版本,新版本對選舉機制做了較大的更改,但是在網上大多都是一筆帶過。于是寫過一篇《MongoDB選舉機制》。當時主要結合了官網最新的文檔和部分源碼。由于官網介紹比較泛泛,源碼閱讀的時候又受限于個人能力與時間,最終只整理了選舉相關的核心邏輯,對應復雜的條件檢查和數據結構沒有深入的了解。最近了解了一些分布式一致性算法之后,打算換一個角度從協議層面重新整理一下MongoDB的選舉。本文簡要介紹分布式系統一致性相關的概念,重點介紹Bully算法和Raft算法在MongoDB選舉中的應用。
分布式系統簡介
分布式系統是一個硬件或軟件組件分布在不同的網絡計算機上,彼此之間僅僅通過消息傳遞進行通信和協調的系統。分布式系統具有分布性、對等性、并發性、缺乏全局時鐘、故障總會發生的特點,由于這些特點導致了分布式系統中的一致性難題。
CAP理論
分布式系統的CAP理論:
CAP理論告訴我們,一個分布式系統不可能同時滿足一致性(C),可用性(A),分區容錯性(P)這三個基本需求,最多只能同時滿足其中兩項。
- 分布式環境中的一致性是指數據在多個副本之間是否能夠保持一致的特性。
- 可用性是指系統必須一致處于可用的狀態,對用用戶的請求能夠在有限時間內返回結果。其中,“有限時間”是相對的,是用戶可接受的合理響應時間;“返回結果”是期望的結果,可以是成功或失敗,不能是用戶不能接受的結果。
- 分布式系統在遇到網絡分區故障時,仍能夠對外提供滿足一致性和可用性的服務,除非整個網絡發生故障。
由于分區容錯性是分布式系統首先要保證的,所以只能在一致性和可用性之間找平衡。
BASE理論
BASE是Basically Available(基本可用)、Soft state(軟狀態)和Eventually consistent(最終一致)三個短語的簡寫,基于CAP發展而來,主要解決一致性和可用性之間的平衡。
- 基本可用是指允許故障發生時損失部分可用性,但不是說系統不可用。比如MongoDB選舉過程中系統是只讀的。
- 軟狀態是指允許數據存在中間狀態,既允許數據不同副本之間的同步過程存在延時。
- 最終一致是指數據副本在經過一段時間的軟狀態后最終達到一致。最終一致是一種特殊的弱一致性。
Paxos,Raft,ZAB等常見的分布式一致性算法都是符合BASE理論。
MongoDB的選舉機制
MongoDB復制集的目的是提供高可用性,如果一個寫操作被復制到大多數節點,只要任意大多數節點可用,那么數據就是可靠的。復制集一般包含一個Primary和若干個Secondary節點。選舉機制主要負責在集群初始化及節點發生變化時重新選舉主節點,保證系統可用性。
MongoDB節點之間維護心跳檢查,主節點選舉由心跳觸發。MongoDB復制集成員會向自己之外的所有成員發送心跳并處理響應信息,因此每個節點都維護著從該節點POV看到的其他所有節點的狀態信息。節點根據自己的集群狀態信息判斷是否需要發起選舉。
MongoDB r3.2.0以前選舉協議是基于Bully算法,從r3.2.0開始默認使用基于Raft算法的選舉策略。MongoDB選舉過程比較復雜,下面的在介紹算法應用時重點關注與算法應用相關的邏輯。
Bully算法
算法描述
Bully算法是一種相對簡單的協調者競選算法。算法的主要思想是集群的每個成員都可以聲明它是協調者并通知其他節點。別的節點可以選擇接受這個聲稱或是拒絕并進入協調者競爭。被其他所有節點接受的節點才能成為協調者。節點按照一些屬性來判斷誰應該勝出。
Bully算法中有三類消息:
- Election Message:宣布發起選舉
- Answer Message:響應選舉消息
- Victory Message:由選舉勝利者發送,宣布勝利
當一個節點從故障中恢復或者監測到主節點故障時,執行過程如下:
- 如果該節點具有最高ID,則向其他節點發送Victory Message,成為主節點并結束選舉,否則向所有編號比它大的節點發送Election Message;
- 如果該節點規定時間內沒有收到答復,則向其他節點發送Victory Message,成為主節點并結束選舉;
- 如果有ID比該節點大的節點響應,則等待Victory Message(如果一定時間后仍未收到Victory Message,則重新發起選舉)
- 如果一個節點收到較低ID發來的Election Message,它將發送一個Answer Message,并發送Election Message到更高ID的節點開始選舉。
- 如果一個節點收到Victory Message,則將發送者視為主節點。
舉例
下面是Bully算法選舉過程的一個經典例子:
- 1,最初集群中有5個節點,節點5是一個公認的協調者
- 2,如果節點5掛了,并且節點2和節點3同時發現了這一情況。兩個節點開始競選并發送競選消息給ID更大的節點。
- 3,節點4淘汰了節點2和3,節點3淘汰了節點2
- 4,這時候節點1察覺了節點5失效并向所有ID更大的節點發送了競選信息
- 5,節點2、3和4都淘汰了節點1
- 6,節點4發送競選信息給節點5
- 7,節點5沒有響應,所以節點4宣布自己當選并向其他節點通告了這一消息
算法應用
MongoDB在實現時對Bully算法做了一些調整:
- Bully ID對應MongoDB節點的priority,且兩個節點priority可能相等
- Secondary發現集群中沒有Primary后不止向更高priority節點,而是向所有節點發送選舉
- 收到選舉請求的節點發現候選節點中有更高priority節點時投veto票,每個veto會將得票數-10000
- MongoDB沒有vote權限的節點才能投票
- 發起選舉過程中的節點不會投票
- 選舉發起和投票過程中包含復雜的條件判斷,但不影響協議理解
- 通過審核的節點會投vote票,每個vote會將得票數+1
- 最終得票數超過集群可投票節點數一半時通過選舉,發起節點成為Primary。
- 當得票不足一半時,發起者隨機退讓,隨機sleep 0 到1 秒后重新發起選舉。
- 引入30s的“選舉鎖”,在持有鎖的時間內不得給其他發起者投票,避免一個節點同時給兩個發起者投票。
- 如果新選舉出的主節點立馬掛掉,至少需要30s時間重新選主。
Raft算法
新版本的MongoDB用Raft取代了Bully。
算法描述
Raft將問題拆成數個子問題分開解決:
- 領袖選舉(Leader Election)
- 記錄復寫(Log Replication)
- 安全性(Safety)
這里我們主要關注領袖選舉。
Raft cluster中為服務器定義了三種角色:
- Leader:領導者
- Flower:追隨者
- Candidate:候選者
在正常情況下,集群中只有一個 leader 并且其他的節點全部都是 follower 。Follower 都是被動的:他們不會發送任何請求,只是簡單的響應來自 leader 和 candidate 的請求。三種角色的轉換關系:
Raft用Term區分每一次任期(包含一次選舉),Term在Raft中起到邏輯時鐘(時序)的作用,Term是單調遞增的。每個節點都保存當前的Term,并在服務器間通信中包含Term字段。
Raft為實現一致性定義了兩種基本RPC:
- 請求投票(RequestVote) RPC 由 candidate 在選舉期間發起
- 追加條目(AppendEntries)RPC 由 leader 發起,用來復制日志和提供一種心跳機制
Raft 用心跳來觸發 leader 選舉。當服務器程序啟動時,他們都是 follower 。一個服務器節點只要能從 leader 或 candidate 處接收到有效的 RPC 就一直保持 follower 狀態。Leader 周期性地向所有 follower 發送心跳(不包含日志條目的 AppendEntries RPC)來維持自己的地位。如果一個 follower 在一段選舉超時時間內沒有接收到任何消息,它就假設系統中沒有可用的 leader ,然后開始進行選舉以選出新的 leader 。
選舉規則中有一些限制條件:
- 對于同一個任期,每個服務器節點只會投給一個 Candidate ,按照先來先服務的原則。
- 只有當候選人的操作日志至少跟投票人一樣新時,才投贊同票。
- 如果一個 Follower 在一段選舉超時時間內沒有接收到任何消息,則發起選舉。
- 每個節點的選舉超時有一定隨機性,新任期重置選舉超時。
下面詳細的介紹一下選舉的過程:
- follower 先增加自己的當前任期號并且轉換到 Candidate 狀態。
- 投票給自己并且并行地向集群中的其他服務器節點發送 RequestVote RPC。
- Candidate 會一直保持當前狀態直到以下三件事情之一發生:
- (a) 收到過半的投票,贏得了這次的選舉
- (b) 其他的服務器節點成為 leader
- (c) 一段時間之后沒有任何獲勝者。
Candidate 贏得選舉
當一個 Candidate 獲得集群中過半服務器節點針對同一個任期的投票,它就贏得了這次選舉并成為 Leader 。對于同一個任期,每個服務器節點只會投給一個 Candidate ,按照先來先服務的原則。一旦 Candidate 贏得選舉,就立即成為 Leader 。然后它會向其他的服務器節點發送心跳消息來確定自己的地位并阻止新的選舉。
其他節點成為Leader
在等待投票期間,Candidate 可能會收到另一個聲稱自己是 leader 的服務器節點發來的 AppendEntries RPC 。如果這個 Leader 的任期號(包含在RPC中)不小于 candidate 當前的任期號,那么 Candidate 會承認該 Leader 的合法地位并回到 Follower 狀態。 如果 RPC 中的任期號比自己的小,那么 Candidate 就會拒絕這次的 RPC 并且繼續保持 Candidate 狀態。
沒有獲勝者
第三種可能的結果是 Candidate 既沒有贏得選舉也沒有輸:如果有多個 follower 同時成為 candidate ,那么選票可能會被瓜分以至于沒有 candidate 贏得過半的投票。當這種情況發生時,每一個候選人都會超時,然后通過增加當前任期號來開始一輪新的選舉。然而,如果沒有其他機制的話,該情況可能會無限重復。
舉例
這里還是用Bully算法中的案例來說明:
- 1,最初集群中有5個節點,節點5是 Leader,此時集群Term為1
- 2,如果節點5掛了,并且節點2和節點3同時發現這一情況,兩個節點同時發起選舉
- 3,節點2,3 Term變為2,轉換為Candidate,給自己投一票并分別向其中中其他節點發送RequestVote RPC。
- 假如節點1,4都先收到節點2,Term2 RequestVote:
- 將Term 記為2
- 都會為節點2投票,拒絕后收到的投票,此時節點2勝出,切換為Leader 并發送心跳
- 節點3 受到心跳,且Term不小于自己Term,則轉換為Flower,集群達到最終一致
- 假如節點1,4分別先收到節點2,3Term2 的RequestVote:
- 將Term 記為2
- 節點2,3分別收到1票,拒絕后收到的投票,Candidate節點各自總共兩票,都不到多半(5/2 + 1),沒有獲勝者
- 此時會由第一個選舉超時的節點,重新發起新一輪Term為3的選舉
- 假如節點1,4都先收到節點2,Term2 RequestVote:
算法應用
MongoDB在實現時對Raft算法做了一些調整:
- Secondary節點不只是被動接受,也會發起心跳監測
- 當Secondary節點發現集群中沒有Primary或者自己priority比Primary高時發起選舉
- 保留了優先級但只考慮自己和當前主結點的優先級,其他結點的優先級不決定選舉與否,也不影響投票
對比
對比新舊版本的選舉:
- Raft為MongoDB選舉引入Term,取消Bully的選舉鎖,效率更高,更優雅的避免重復投票,減少投票等待時間。
- Raft弱化了priority功能,可能出現非最高priority候選節點當選的情況,后續的心跳中會發現,并重新選舉。
- 取消了veto,選舉不一定需要等待心跳超時。
- 主節點的降級有自己發起,效率更高。
參考
深入理解NoSQL數據庫分布式算法及策略
維基百科-Bully algorithm
《從Paxos到ZooKeeper 分布式一致性原理與實踐》
《Time, Clocks, and the Ordering of Events in a Distributed System》
《MongoDB 中的Raft一致性協議》