在一文搞懂raft算法一文中,從raft論文出發,詳細介紹了raft的工作流程以及對特殊情況的處理。但算法、協議這種偏抽象的東西,僅僅看論文還是比較難以掌握的,需要看看在工業界的具體實現。本文關注MongoDB是如何在復制集中使用raft協議的,對raft協議做了哪些擴展。
閱讀本文,需要對MongoDB復制集replication有一定認識,特別是replicat set protocol version。
在帶著問題學習分布式系統之中心化復制集一文中,介紹了中心化副本控制協議。在raft(mongodb pv1)中,也是通過先選舉出leader(primary),然后通過leader(primary)管理整個復制集。
在3.2以及之后的版本中,mongodb默認使用protocol version 1。從官方的一些資料、視頻可以看到,這個是一個raft-like的協議。本文主要從leader-election和log replication這兩個角度來對比mongodb rs pv1與raft,并試圖分析差異的原因。
需要注意的是,本文所有對MongoDB復制集的分析都是基于MongoDb3.4
本文地址:https://www.cnblogs.com/xybaby/p/10165564.html
leader election
首先對raft協議中leader election做幾點總結:
- 同一任期內最多只能投一票,先來先得
- 選舉人必須比自己知道的更多(比較term,log index)
- 為了understandability,raft中節點之間沒有ranking,公平參與投票
選舉、投票資格
為了簡化協議,使得raft更容易理解,raft中所有節點都能發起選舉、參與投票。但在MongoDB中,有更為豐富的選舉控制策略,我們從Replica Set Configuration就能看出來,replica set中的節點可以配置以下屬性
members: [{_id: <int>,host: <string>,arbiterOnly: <boolean>,buildIndexes: <boolean>,hidden: <boolean>,priority: <number>,tags: <document>,slaveDelay: <int>,votes: <number>},...],
- arbiterOnly: Arbiter上沒有用戶數據,只能投票,不能發起選舉,其作用在于用盡量少的資源使得復制集中節點數目為奇數。
- hidden:雖然有數據,但對客戶端不可見,可以用來做備份等其他用途。hidden的priority一定是0,因此不可以發起選舉,但是可以投票
- priority:
A number that indicates the relative eligibility of a member to become a primary.
priority為0時是不能發起選舉的。 - votes:是否可以參與投票,mongodb復制集中最多可以有50個節點,但最多只有7個可以投票,其作用在于降低復雜度。
priority
這里再單獨強調一下priority,mongodb中
Changing the balance of priority in a replica set will trigger one or more elections. If a lower priority secondary is elected over a higher priority secondary, replica set members will continue to call elections until the highest priority available member becomes primary.
通過rs.reconfig()修改節點的優先級的時候,會觸發重新選舉。整個復制集會不斷發起選舉,直到最高優先級的節點成為primary。當然,在選舉-投票的過程中,還是必須滿足候選者數據足夠新的約束。
priority很有用,比如在multi datacenter deploy的情況下,我們可能根據用戶的分布情況來確定primary在哪個datacenter。
heartbeat
raft中,只有leader給follower發心跳信息(心跳是沒有log-entry的Append Entries rpc),然后follower回復心跳消息。
Followers are passive: they issue no requests on their own but simply respond to requests from leaders and candidates.
在mongodb中,節點兩兩之間有心跳
Replica set members send heartbeats (pings) to each other every two seconds. If a heartbeat does not return within 10 seconds, the other members mark the delinquent member as inaccessible.
primary handover
在raft中,只有當leader收到來自term更高的節點的消息時,才會切換到follower狀態。如果出現網絡分割(network partition),那么這個過期的leader還會一直認為自己是leader
If a candidate or leader discovers
that its term is out of date, it immediately reverts to follower
state.
在mongodb中,primary在election timeout時間還沒有收到來自majority 節點的消息時,會主動切換成secondary。這樣可以避免過期的Primary(stale primary)繼續對外提供服務,尤其是MongoDB允許writeConcern:1.
選舉過程 - 預投票
raft中,在election timeout超時后,立即會發起選舉,執行以下操作
- 增加節點本地的 current term ,切換到candidate狀態
- 投自己一票
- 并行給其他節點發送 RequestVote RPCs
- 等待其他節點的回復
- 如果得到majority投票,成為leader
mongodb增加了一個預投票的過程(dry-run),即在不增加新的term的情況下先問問其他節點,是否可能給自己投票,得到大多數節點的肯定回復之后才會發起真正的選舉過程。其作用在于盡可能減少不必要的主從切換,這部分后面還會提到。
log replication
復制集中,各個節點數據的一致性是必須要解決的問題。而對于客戶端(應用)而言,復制集則需要承諾已提交的數據不能回滾。
同步or異步
在帶著問題學習分布式系統之中心化復制集一文中,介紹了復制集中數據的兩種復制方式,并分析了各自的優缺點。簡而言之,同步方式可靠性更高,但可用性更差,網絡延時更大;異步模式則恰好相反。
raft協議則是這兩種方式的折中,當log復制到了大多數的節點就可以向客戶端返回了。大多數節點既保證了數據的可靠性:數據不會被回滾;又保證了有較高的可用性:只有有超過一半節點存活整個系統就能正常工作。
MongoDB通過Write Concern選項將選擇權交給了用戶,用戶可以根據實際情況來選擇將數據復制到了多少節點再向客戶端返回。writeconcern有三個參數
- w:寫到多少節點即可向客戶端返回
- 1,默認值,即寫primary即可返回,性能最高,延遲最低
- majority,同raft,寫到大多數節點才返回
- tag set,寫到指定的節點才返回,用于特殊場景
- j:是否寫到journal(保證持久化)
- wtimeout:多長時間如果沒有寫到
w
個節點就向客戶端返回錯誤
由于默認寫到primary即可向客戶端返回,那么不難想到,如果oplog尚未同步到secondary,primary掛掉,那么新選舉出來的Primary可能沒有最新的已經向客戶端確認的數據,導致數據的回滾,后面會提到mongodb通過catchup
來盡量避免回滾。
data flow
在帶著問題學習分布式系統之中心化復制集中也給出了兩種數據從primary到secondary的方式:主從模式,鏈式模式。其中,主從模式是priamry推送給所有的secondary,顯然raft就是這種模式。
而在MongoDB中,可以通過參數settings.chainingAllowed控制使用主從模式,還是鏈式模式。默認值為True,即默認情況下,mongodb中secondary可以從其他secondary同步數據,這樣secondary可以選擇一個離自己最近(心跳延時最小的)節點來復制oplog,在MongoDB中,稱oplog的同步源為SyncSource。
push or pull
raft中,leader并行將數據push到follower。而在MongoDB中,primary將數據寫到local.oplog.rs,secondary定期從其SyncSource(參考上一節,不一定是從priamry拉數據,也可能是從其他secondary)讀取oplog,并應用到本地。
深入淺出MongoDB復制一文中給出了一個oplog拉取的流程
MongoDB選擇了pull的策略,顯然會加大在writeConcern: majority時的延遲,但對于默認的鏈式復制,pull是更合適的,因為secondary更清楚自己的SyncSource。
append vs apply
在諸多共識算法中,都是將command封裝到有序、持久化的log當中,raft和MongoD也是如此。
對于raft,leader先將log先append到本地的log entries,然后等到收到majority節點的回復后再apply log到狀態機,如下入所示:
但是在mongodb中,即使客戶端要求writeconcern:majority,primary也是先apply,將變更作用到狀態機,再寫oplog。之后,secondary再從其SyncSource的local.oplog.rs collection 拉取oplog,本地apply,然后寫oplog。
MongoDB先Apply再寫oplog,以及異步復制的機制,會導致即使數據無法寫到大多數節點(可能primary與其他節點間網絡故障),即使向客戶端返回寫入失敗,寫到primary的數據也不會回滾。
catchup
catchup既與write concern有關,也跟leader election有關。
mongodb中,有這么一個參數settings.catchUpTimeoutMillis
, 其作用是
Time limit in milliseconds for a newly elected primary to sync (catch up) with the other replica set members that may have more recent writes.
The newly elected primary ends the catchup period early once it is fully caught up with other members of the set. During the catchup period, the newly elected primary is unavailable for writes from clients.
也就是說,在primary選舉出來之后,會有一段時間,讓primary嘗試去其他節點讀取到更新的寫操作(more recent)。直到追加到最新的oplog,或者超時,primary才進入工作狀態(接收客戶端寫請求)
究其原因,MongoDB允許用戶自定義writeconcern,且默認只要求寫到primary。因此選舉的時候即使得到了大多數節點的投票,且primary的數據在這些大多數節點中是最新的,但原來的primary可能沒有參與投票,那么就可能導致數據的回滾。catchup能夠盡量避免回滾的出現,如果無法在settings.catchUpTimeoutMillis
時間內完成catchup,也會將回滾的內容寫入一個rollback文件。
差異的思考
MongoDB作為一個分布式數據庫系統,既要支持OLTP,又要支持OLAP,既要滿足水平伸縮,又要保證高可用、高可靠,還要支持分布式事務(Mongodb 4.x)。因此為了盡量滿足不同場景下的業務需求,MongoDB提供了大量的選項,供用戶選擇,更加靈活。對于復制集這一塊而言,選項包括但不限于:
- WriteConcern
- ReadConcern
- ReadPreference
- settings.chainingAllowed
- settings.catchUpTimeoutMillis
所以,作為MongoDB的用戶,首先得清楚這些可選項的意義,然后根據自己的業務需求,合理配置。
從這些選項的默認值以及MongoDB的實現,個人覺得,在CAP這個問題上,MongoDB應該是更傾向于A(availability,可用性)的。
而諸如鏈式復制,Leader Priority這些特性,在分布式系統的部署層面來說都是很有用的,比如multi datacenter,很多分布式存儲系統也支持同樣的特性。Raft協議雖然說是為工業實現提供了很好的指導,但到具體的應用,還是得有諸多的調整和完善。
dry-run or pre-vote
MongoDB中的預投票是對raft協議很好的改進,之前,我在看Raft論文的時候也想到了一些corner case,在論文中并沒有很清楚的闡述,但預投票能很好解決這些問題。
事實上,MongoDB中的預投票(dry-run)并不是獨創的,在raft協議的超長版解釋Consensus: Bridging Theory and Practice中,raft協議的作者就建議實現pre-vote來增加系統的魯棒性。而在Four modifications for the Raft consensus algorithm(PS,該文的作者就是MongoDB的開發者)詳細闡述了Pre-vote的原因以及實現方法。Pre-vote是為了防止一個隔離的follower不斷發起選舉 導致term值的激增,以及不必要的主從切換。
如上圖所以,系統由s1 s2 s3三個節點組成,其中s1是leader,另外兩個節點是follower。
pre-vote考慮的是這樣一種情況,(s2)與(s1 s3)之間出現了網絡分割(network partition),那么按照raft算法,s2會不斷的嘗試發起選舉,意味著不斷的增加term。那么當網絡自愈之后,s2將消息發送到s1 s3. 按照raft論文figure2 Rules for servers
If RPC request or response contains term T > currentTerm: set currentTerm = T, convert to follower
因此s1 會切換到follower, s1 s3 term修改為57,但s2的log 大概率是過舊的(out of date),因此s2無法獲得選舉,s1 s3會在election timeout后發起選舉,其中一個成為term 58的leader。
pre-vote 避免了term inflation,但更重要的是,避免了一次沒有必要的重新選舉: s1一定會切換到follower,然后s1或者s3再次發起選舉,在這個過程中,由于沒有leader,整個系統其實是不可用的(至少不可寫)。
references
一文搞懂raft算法
replication
MongoDB and Raft
MongoDB復制集技術內幕:工作原理及新版本改進方向
MongoDB 高可用復制集內部機制:Raft 協議
Consensus: Bridging Theory and Practice
Four modifications for the Raft consensus algorithm