SOFAStack
Scalable Open Financial Architecture Stack
是螞蟻金服自主研發的金融級分佈式架構,包含了構建金融級雲原生架構所需的各個組件,是在金融場景裏錘鍊出來的最佳實踐。
本文爲《剖析 | SOFAJRaft 實現原理》第四篇,本篇做者力鯤,來自螞蟻金服《剖析 | SOFAJRaft 實現原理》系列由 SOFA 團隊和源碼愛好者們出品,項目代號:<SOFA:JRaftLab/>,目前領取已經完成,感謝你們的參與。java
SOFAJRaft 是一個基於 Raft 一致性算法的生產級高性能 Java 實現,支持 MULTI-RAFT-GROUP,適用於高負載低延遲的場景。git
SOFAJRaft :https://github.com/sofastack/sofa-jraftgithub
在 Raft 算法中,選舉是很重要的一部分,所謂選舉也就是在多個節點中選出一個 Leader 節點,由他來對外提供寫服務 (以及默認狀況下的讀服務)。算法
在剖析源碼時,對選舉機制的理解常常會遇到兩極分化的狀況,對於瞭解 Raft 算法基本原理的同窗,閱讀源碼就是品味實現之巧妙的過程,而對初體驗的同窗,卻會陷入丈二和尚的窘境,彷彿墜入雲裏霧裏。架構
爲了提高文章的可讀性,我仍是但願花一部分篇幅講清楚選舉機制的基本原理,以便後面集中注意力於代碼實現。下面是一段圖文比喻,幫助理解的同時也讓整篇文章不至於過早陷入細節的討論。dom
一個分佈式集羣能夠當作是由多條戰船組成的一支艦隊,各船之間經過旗語來保持信息交流。這樣的一支艦隊中,各船既不會互相徹底隔離,但也無法像陸地上那樣保持很是密切的聯繫,天氣、海況、船距、船隻戰損狀況致使船艦之間的聯繫存在但不可靠。分佈式
艦隊做爲一個統一的做戰集羣,須要有統一的共識、步調一致的命令,這些都要依賴於旗艦指揮。各艦船要服從於旗艦發出的指令,當旗艦不能繼續工做後,須要有別的戰艦接替旗艦的角色。性能
圖1 - 全部船有責任隨時準備接替旗艦spa
如何在艦隊中,選出一艘獲得你們承認的旗艦,這就是 SOFAJRaft 中選舉要解決的問題。3d
什麼時候能夠發起選舉?換句話說,觸發選舉的標準是什麼?這個標準必須對全部戰艦一致,這樣就可以在標準獲得知足時,全部艦船公平的參與競選。在 SOFAJRaft 中,觸發標準就是通訊超時,當旗艦在規定的一段時間內沒有與 Follower 艦船進行通訊時,Follower 就能夠認爲旗艦已經不能正常擔任旗艦的職責,則 Follower 能夠去嘗試接替旗艦的角色。這段通訊超時被稱爲 Election Timeout (簡稱 ET), Follower 接替旗艦的嘗試也就是發起選舉請求。
圖2 - ET 觸發其餘船競選旗艦
在選舉中,只有當艦隊中超過一半的船都贊成,發起選舉的船纔可以成爲旗艦,不然就只能開始一輪新的選舉。因此若是 Follower 採起儘快發起選舉的策略,試圖儘早爲艦隊選出可用的旗艦,就可能引起一個潛在的風險:可能多艘船幾乎同時發起選舉,結果其中任何一支船都沒能得到超過半數選票,致使這一輪選舉無果,而後失敗的 Follower 們再一次幾乎同時發起選舉,又一次失敗,再選舉 again,再失敗 again ···
圖3 - 同時發起選舉,選票被瓜分
爲避免這種狀況,咱們採用隨機的選舉觸發時間,當 Follower 發現旗艦失聯以後,會選擇等待一段隨機的時間 Random(0, ET) ,若是等待期間沒有選出旗艦,則 Follower 再發起選舉。
圖4 - 隨機等待時間
SOFAJRaft 的選舉中包含了對兩個屬性的判斷:LogIndex 和 Term,這是整個選舉算法的核心部分,也是容易讓人產生困惑的地方,所以咱們作一下解釋:
一、Term
咱們會對艦隊中旗艦的歷史進行編號,好比艦隊的第1任旗艦、第2任旗艦,這個數字咱們就用 Term 來表示。因爲艦隊中同時最多隻能有一艘艦船擔任旗艦,因此每個 Term 只歸屬於一艘艦船,顯然 Term 是單調遞增的。
二、LogIndex
每任旗艦在職期間都會發布一些指令(稱其爲「旗艦令」,類比「總統令」),這些旗艦令固然也是要編號歸檔的,這個編號咱們用 Term 和 LogIndex 兩個維度來標識,表示「第 Term 任旗艦發佈的第 LogIndex 號旗艦令」。不一樣於現實中的總統令,咱們的旗艦令中的 LogIndex 是一直遞增的,不會由於旗艦的更迭而從頭開始計算。
圖5 - 總統令 Vs 旗艦令,LogIndex 稍有區別
全部的艦船都儘量保存了過去從旗艦接收到的旗艦令,因此咱們選舉的標準就是哪艘船保存了最完整的旗艦令,那他就最有資格接任旗艦。具體來講,參與投票的船 V 不會對下面兩種候選者 C 投票:一種是 lastTermC < lastTermV;另外一種是 (lastTermV == lastTermC) && (lastLogIndexV > lastLogIndexC)。
稍做解釋,第一種狀況說明候選者 C 最後一次通訊過的旗艦已經不是最新的旗艦了,至少比 V 更滯後,因此它所知道的旗艦令也不可能比 V 更完整。第二種狀況說明,雖然 C 和 V 都與同一個旗艦有過通訊,可是候選者 C 從旗艦處得到的旗艦令不如 V 完整 (lastLogIndexV > lastLogIndexC),因此 V 不會投票給它。
圖6 - Follower 船 b 拒絕了船 c 而投票給船 a,船 a 旗艦令有一個空白框表示「第 Term 任旗艦」沒有發佈過任何旗艦令
如上一小節所說,SOFAJRaft 將 LogIndex 和 Term 做爲選舉的評選標準,因此當一艘船發起選舉以前,會自增 Term 而後填到選舉請求裏發給其餘船隻 (多是一段很複雜的旗語),表示本身競選「第 Term + 1 任」旗艦。
這裏要先說明一個機制,它被用來保證各船隻的 Term 同步遞增:當參與投票的 Follower 船收到這個投票請求後,若是發現本身的 Term 比投票請求裏的小,就會自覺更新本身的 Term 向候選者看齊,這樣可以很方便的將 Term 遞增的信息同步到整個艦隊中。
圖7 - Follower 船根據投票請求更新本身的 Term
可是這種機制也帶來一個麻煩,若是一艘船由於本身的緣由沒有看到旗艦發出的旗語,他就會自覺得是的試圖競選成爲新的旗艦,雖然不斷髮起選舉且一直未能當選(由於旗艦和其餘船都正常通訊),可是它卻經過本身的投票請求實際擡升了全局的 Term,這在 SOFAJRaft 算法中會迫使旗艦 stepdown (從旗艦的位置上退下來)。
圖8 - 自覺得是的搗亂者,迫使旗艦 stepdown
因此咱們須要一種機制阻止這種「搗亂」,這就是預投票 (pre-vote) 環節。候選者在發起投票以前,先發起預投票,若是沒有獲得半數以上節點的反饋,則候選者就會識趣的放棄參選,也就不會擡升全局的 Term。
圖9 - Pre-vote 預投票
在上面的比喻中,咱們能夠看到整個選舉操做的主線任務就是:
這個過程可用下圖表示:
圖10 - 一次成功的選舉
在代碼層面,主要是由四個方法來處理這個流程:
com.alipay.sofa.jraft.core.NodeImpl#preVote //預投票 com.alipay.sofa.jraft.core.NodeImpl#electSelf //投票 com.alipay.sofa.jraft.core.NodeImpl#handlePreVoteRequest //處理預投票請求 com.alipay.sofa.jraft.core.NodeImpl#handleRequestVoteRequest //處理投票請求
代碼邏輯比較直觀,因此咱們用流程圖來簡述各個方法中的處理。
圖11 - 預投票 Vs 投票
圖中可見,預投票請求 preVote 和投票請求 electSelf 的流程基本相似,只是有幾個細節不太同樣:
處理預投票和投票請求的邏輯也比較相似,一樣用圖來表示。
圖12 - 處理預投票請求
圖13 - 處理投票請求
圖中可見,處理兩種請求的流程也基本相似,只是處理投票請求的時候,會有 stepdown 機制,強制使 Leader 從其 Leader 的身份退到 Follower。在具體的實現中,Leader 會經過租約的機制來避免一些沒有必要的 stepdown,關於租約機制,能夠參見以前的系列文章《SOFAJRaft 線性一致讀實現剖析 | SOFAJRaft 實現原理》。
咱們在本文中採用了類比的方式來剖析源碼,主要是爲了讓讀者能更容易的理解如何在分佈式環境中達成共識,其實這也是整個 SOFAJRaft 要實現的目標。
行文至此,做者感受已經把選舉說清楚了,若是還有沒提到的地方,或者一些流程中的分支任務,歡迎從源碼中進一步尋找答案。最後貼出上面提到的四個方法的源碼。
圖14 - preVote 預投票
圖15 - electSelf 投票
圖16 - handlePreVoteRequest 處理預投票
圖17 - handleRequestVoteRequest 處理投票
公衆號:金融級分佈式架構(Antfin_SOFA)