以前兩篇文章介紹了作Elasticell的原因和Multi-Raft的實現細節:git
Elasticell-緣起github
這篇主要介紹Elasticell在Raft上作的一些優化工做。異步
一次簡單的Raft就一個值達成一致的過程性能
Leader收到一個請求優化
Leader存儲這個Raft Log,而且給Follower發送AppendEntries消息spa
Follower收到而且存儲這個Raft Log,給Leader回覆AppendEntries消息線程
Leader收到大多數的Follower的回覆,給Follower發送AppendEntries消息,確認這個Log已經被Commit了進程
Leader本身Apply這個Log事件
Leader Apply完成後,回覆客戶端
Follower收到Commit的AppendEntries消息後,也開始Apply這個Log
若是都順序處理這些,性能就可想而知了,下面咱們來看一下在Elasticell中針對Raft所作的一些優化工做。
Append Log並行化
2這個步驟,Leader能夠AppendEntries消息和Raft Log的存儲並行。爲何?
若是Leader沒有Crash,那麼和順序的處理結果一致。
若是Leader Crash了,那麼若是大於n/2+1的follower收到了這個消息並Append成功,那麼這個Raft Log就必定會被Commit,新選舉出來的Leader會響應客戶端;不然這個Raft Log就不會被Commit,客戶端就會超時/錯誤/或重試後的結果(看實現方式)。
異步Apply
一旦一個Log被Committed,那麼何時被Apply都不會影響正確性,因此能夠異步的Apply這個Log。
物理鏈接多路複用
當系統中的Raft-Group愈來愈多的時候,每一個Raft-Group中的全部副本都會兩兩建鏈,從物理上看,最後會看到2臺物理機器(能夠是物理機,虛機,容器等等)之間會存在大量的TCP連接,形成連接爆炸。Elasticell的作法:
使用鏈路複用技術,讓單個Store進程上全部的Raft-Group都複用一個物理連接
包裝Raft消息,增長Header(Header中存在Raft-Group的元信息),這樣在Store收到Raft消息的時候,就可以知道這些消息是屬於哪個Raft-Group的,從而驅動Raft。
Batching & Pipelining
不少同窗會認爲實現強一致存儲會影響性能,其實並不是如此,在合理的優化實現下,強一致存儲對於系統的吞吐量並不會有多大的影響,這主要來自於一致性協議的兩個重要的細節Batching和Pipelining,理念能夠參見論文[1],事實上,在阿里近期反覆提到的X-DB跨機房優化中也實現了相似的功能X-Paxos,所以下面看看Raft的Batching和Pipelining如何在Elasticell中達到相似的效果。
在Elasticell中Batching在各個階段都有涉及,Batching能夠提升系統的吞吐量(和Latency矛盾)。Elasticell中的單個Raft-Group使用一個Goroutine來處理Raft事件,好比Step,Request,Raft Ready,Tick,Apply Result等等。
Proposal階段,收集在上一次和本次處理Raft事件之間的全部請求,並把相同類型的請求作合併,並作一個Proposal,減小Raft的網絡請求
Raft Ready階段, 收集在上一次和本次處理Raft事件之間的全部的Ready信息,Leader節點Batch寫入Raft Log
Apply階段,因爲Apply是異步處理,能夠把相同類型的操做合併Apply(例如把多個Redis的Set操做合併爲一個MSet操做),減小CGO調用
Raft的Leader給Follower發送AppendEntries的時候,若是等待上一次的AppendEntries返回,再發下一個AppendEntries,那麼必然性能不好。因此須要作Pipelining來加速,不等上一次的AppendEntries返回,持續的發送AppendEntries。
若是要保證性能和正確性,須要作到如下兩點:
Leader到某一個Follower之間的發送管道必須是有序的,保證Follower有序的處理AppendEntries。
可以處理丟失AppendEntries的情況,好比連續發送了Index是2,3,4的三個Append消息,其中3這個消息丟包了,Follower收到了2和4,那麼Leader必須從新發送3,4兩個Append消息(由於4這個消息會被Follower丟棄)。
對於第二點,Etcd的庫已經作了處理,在Follower收到Append消息的時候,會檢查是否是匹配已經接收到的最後一個Raft Log,若是不匹配,就返回Reject消息,那麼按照Raft協議,Leader收到這個Reject消息,就會從3(4-1)重試。
Elasticell的實現方式:
保證用於發送Raft消息的連接在每兩個節點直接只有一個
把當前節點待發送的Raft消息按照對端節點的ID作簡單的hash,放到不一樣的線程中去,由這些線程負責發送(線程的數量就至關於Pipelining的管道數)
這樣就能保證每一個Follower收到的Raft消息是有序的,而且每一個Raft都只有一個Goroutine來處理Raft事件,這些消息可以保證被順序的處理。
Batching和Pipelining的trade off
Batching可以提升系統的吞吐量(會帶來系統Latency增大),Pipelining可以下降系統的Latency(也能在必定程度上提升吞吐量),這個2個優化在決策的時候是有衝突的(在Pipelining中發送下一個請求的時候,須要等多少的Batch Size,也許多等一會就回收集更多的請求),目前Elasticell採用的方式是在不影響Pipelining的前提下,儘量多的收集2次Pipelining之間的請求Batching處理策略,顯然這並非一個最優的解決方案。
尚未作的優化
以上是Elasticell目前已經作的一些優化,還有一些是將來須要作的:
不使用RocksDB存儲Raft Log,因爲Raft Log和RocksDB的WAL存在功能重複的地方,這樣就多了一次文件IO
Raft的heartbeat合併,當一個節點上的Raft-Group的不少的時候,heartbeat消息過多
Batching Apply的時候,當前節點上全部正在Apply的Raft-Group一塊兒作Batching而不是在一個Raft-Group上作Batching
更高效的Batching和Pipelining模式,參考論文[1]
瞭解更多
https://github.com/deepfabric/elasticell
參考
[1] Tuning Paxos for high-throughput with batching and pipelining