從JRaft來看Raft協議實現細節

img

分佈式系統和一致性問題

一致性問題(consensus problem)是分佈式系統須要解決的一個核心問題。分佈式系統通常是由多個地位相等的節點組成,各個節點之間的交互就比如幾我的聚在一塊兒討論問題。讓咱們設想一個更具體的場景,好比三我的討論中午去哪裏吃飯,第一我的說附近剛開了一個火鍋店,據說味道很是不錯;但第二我的說,很差,吃火鍋花的時間過久了,仍是隨便喝點粥算了;而第三我的說,那個粥店我昨天剛去過,太難喝了,還不如去吃麥當勞。結果,三我的僵持不下,始終達不成一致。node

有人說,這還很差解決,投票唄。因而三我的投了一輪票,結果每一個人仍然堅持本身的提議,問題仍是沒有解決。有人又想了個主意,乾脆咱們選出一個leader,這個leader說什麼,咱們就聽他的,這樣你們就不用爭了。因而,你們開始投票選leader。結果很悲劇,每一個人都以爲本身應該作這個leader。三我的終於發現,「選leader」這件事仍然和原來的「去哪裏吃飯」這個問題在本質上是同樣的,一樣難以解決。git

這時恐怕有些讀者們內心在想,這三我的是有毛病吧……就吃個飯這麼點小事,用得着爭成這樣嗎?實際上,在分佈式系統中的每一個節點之間,若是沒有某種嚴格定義的規則和協議,它們之間的交互就真的有可能像上面說的情形同樣。整個系統達不成一致,就根本無法工做。web

因此,就有聰明人設計出了一致性協議(consensus protocol),像咱們常見的好比Paxos、Raft、Zab之類。與前面幾我的商量問題相似,若是翻譯成Paxos的術語,至關於每一個節點能夠提出本身的提議(稱爲proposal,裏面包含提議的具體值),協議的最終目標就是各個節點根據必定的規則達成相同的proposal。但以誰的提議爲準呢?咱們容易想到的一個規則多是這樣:哪一個節點先提出提議,就以誰的爲準,後提出的提議無效。算法

可是,在一個分佈式系統中的狀況可比幾我的聚在一塊兒討論問題複雜多了,這裏邊還有網絡延遲的問題,舉個簡單的例子,假設節點A和B分別幾乎同時地向節點X和Y發出了本身的proposal,但因爲消息在網絡中的延遲狀況不一樣,最後結果是:X先收到了A的proposal,後收到了B的proposal;可是Y正好相反,它先收到了B的proposal,後收到了A的proposal。這樣在X和Y分別看來,誰先誰後就難以達成一致了。數組

若是考慮到節點宕機和消息丟失的可能性,狀況還會更復雜。節點宕機能夠當作是消息丟失的特例,至關於發給這個節點的消息所有丟失了。這在CAP的理論框架下,至關於發生了網絡分割(network partitioning),也就是對應CAP中的P。好比,有若干個節點聯繫不上了,也就是說,對於其它節點來講,它們發送給這些節點的消息收不到任何迴應。真正的緣由,多是網絡中間不通了,也多是那些目的節點宕機了,也多是消息無限期地被延遲了。總之,就是系統中有些節點聯繫不上了,它們不能再參與決策,但也不表明它們過一段時間不能從新聯繫上。安全

爲了表達上更直觀,下面咱們仍是假設某些節點宕機了。那在這個時候,剩下的節點在缺乏了某些節點參與決策的狀況下,還能不能對於提議達成一致呢?即便是達成了一致,那麼在那些宕機的節點從新恢復過來以後(注意這時候它們對於其它節點之間已經達成一致的提議可能一無所知),它們會不會對於已經達成的一致提議從新提出異議,從而形成混亂?全部這些問題,都是分佈式一致性協議須要解決的。服務器

實際上,理解問題自己比理解問題的答案要重要的多。網絡

拜占庭將軍問題

在分佈式系統理論中,這個問題被抽象成了一個著名的問題——拜占庭將軍問題(Byzantine Generals Problem)。架構

拜占庭帝國派出多支軍隊去圍攻一支敵軍,每支軍隊有一個將軍,但因爲彼此距離較遠,他們之間只能經過信使傳遞消息。敵方很強大,固而必須有超過半數的拜占庭軍隊一同參與進攻纔可能擊敗敵人。在此期間,將軍們彼此之間須要經過信使傳遞消息並協商一致後,在同一時間點發動進攻。併發

相關論文:

《The Byzantine Generals Problem》
《Reaching Agreement in the Presence of Faults》

三將軍的難題

假設只有三個拜占庭將軍,分別爲A、B、C,他們要討論的只有一件事情:明天是進攻仍是撤退。爲此,將軍們須要依據「少數服從多數」原則投票表決,只要有兩我的意見達成一致就能夠了。

舉例來講,A和B投進攻,C投撤退:

  1. 那麼A的信使傳遞給B和C的消息都是進攻;
  2. B的信使傳遞給A和C的消息都是進攻;
  3. 而C的信使傳給A和B的消息都是撤退。

若是稍微作一個改動:三個將軍中出了一個叛徒呢?叛徒的目的是破壞忠誠將軍間一致性的達成,讓拜占庭的軍隊遭受損失。

做爲叛徒的C,你必然不會按照常規出牌,因而你讓一個信使告訴A的內容是你「要進攻」,讓另外一個信使告訴B的則是你「要撤退」。

至此,A將軍看到的投票結果是:進攻方 :撤退方 = 2 : 1 ,而B將軍看到的是 1 : 2 。次日,忠誠的A衝上了戰場,卻發現只有本身一支軍隊發起了進攻,而一樣忠誠的B,卻早已撤退。最終,A的軍隊敗給了敵人。

Raft算法要成立都是創建在一個前提下的:不存在惡意節點,才能達成一致。不然,這些著名的算法會隨之失效。

從一個Counter例子提及

需求

提供一個 Counter,Client 每次計數時能夠指定步幅,也能夠隨時發起查詢。

這個看似簡單的需求,主要有三個功能點:

  • 實現:Counter server,具有計數功能,具體運算公式爲:Cn = Cn-1 + delta;
  • 提供寫服務,寫入 delta 觸發計數器運算;
  • 提供讀服務,讀取當前 Cn 值;

除此以外,咱們還有一個可用性的可選需求,須要有備份機器,讀寫服務不能不可用。

系統架構1.0

根據剛纔分析出來的功能需求,咱們設計出 1.0 的架構,這個架構很簡單,一個節點 Counter Server 提供計數功能,接收客戶端發起的計數請求和查詢請求。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/1567322241939-c5f843d0-b882-4635-86ab-a9400b01c518.png

可是這樣的架構設計存在這樣兩個問題:一是 Server 是一個單點,一旦 Server 節點故障服務就不可用了;二是運算結果都存儲在內存當中,節點故障會致使數據丟失。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled.png

系統架構1.5

針對問題一,當節點故障之時,咱們要新起一臺備用機器。針對問題二,咱們優化一下,加一個本地文件存儲。這樣每次計數器完成運算以後都將數據落盤。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%201.png

可是同時也引來另外的問題:磁盤 IO 很頻繁,同時這種冷備的模式也依然會致使一段時間的服務不可用。

系統架構2.0

因爲上面的問題僅僅經過加機器已經沒法解決,因此咱們提出架構 2.0,採用集羣的模式提供服務。咱們用三個節點組成集羣,由一個節點對外提供服務,當 Server 接收到 Client 發來的寫請求以後,Server 運算出結果,而後將結果複製給另外兩臺機器,當收到其餘全部節點的成功響應以後,Server 向 Client 返回運算結果。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%202.png

可是這樣的架構也存在這問題:

  • 咱們選擇哪一臺 Server 扮演 Leader 的角色對外提供服務;
  • 當 Leader 不可用以後,選擇哪一臺接替它;
  • Leader 處理寫請求的時候須要等到全部節點都響應以後才能響應 Client;
  • 也是比較重要的,咱們沒法保證 Leader 向 Follower 複製數據是有序的,因此任一時刻三個節點的數據均可能是不同的;

因此爲了保證複製數據的順序和內容,這就有了共識算法的用武之地,咱們使用SOFAJRaft來構建咱們的3.0 架構。

系統架構3.0

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%203.png

3.0 架構中,Counter Server 使用 SOFAJRaft 來組成一個集羣,Leader 的選舉和數據的複製都交給 SOFAJRaft 來完成。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%204.png

在時序圖中咱們能夠看到,Counter 的業務邏輯從新變得像架構 1.0 中同樣簡潔,維護數據一致的工做都交給 SOFAJRaft 來完成,因此圖中灰色的部分對業務就不感知了。

在使用 SOFAJRaft 的 3.0 架構中,SOFAJRaft 幫咱們完成了 Leader 選舉、節點間數據同步的工做,除此以外,SOFAJRaft 只須要半數以上節點響應便可,再也不須要集羣全部節點的應答,這樣能夠進一步提升寫請求的處理效率。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%205.png

Raft 共識算法

小論文:《In Search of an Understandable consensus Algorithm》

大論文:《Consensus:Bridging theory and practice》

Raft 是一種共識算法,其特色是讓多個參與者針對某一件事達成徹底一致:一件事,一個結論。同時對已達成一致的結論,是不可推翻的。能夠舉一個銀行帳戶的例子來解釋共識算法:假如由一批服務器組成一個集羣來維護銀行帳戶系統,若是有一個 Client 向集羣發出「存 100 元」的指令,那麼當集羣返回成功應答以後,Client 再向集羣發起查詢時,必定可以查到被存儲成功的這 100 元錢,就算有機器出現不可用狀況,這 100 元的帳也不可篡改。這就是共識算法要達到的效果。

Raft 中的基本概念

Raft-node 的 3 種角色/狀態

在一個由 Raft 協議組織的集羣中有三類角色:

  1. Leader(領袖)
  2. Follower(羣衆)
  3. Candidate(候選人)

就像一個民主社會,領袖由民衆投票選出。剛開始沒有領袖,全部集羣中的參與者都是羣衆,那麼首先開啓一輪大選,在大選期間全部羣衆都能參與競選,這時全部羣衆的角色就變成了候選人,民主投票選出領袖後就開始了這屆領袖的任期,而後選舉結束,全部除領袖的候選人又變回羣衆角色服從領袖領導。這裏提到一個概念「任期」,用術語 Term 表達。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%206.png

Message 的 3 種類型

  1. RequestVote RPC:由 Candidate 發出,用於發送投票請求;
  2. AppendEntries (Heartbeat) RPC:由 Leader 發出,用於 Leader 向 Followers 複製日誌條目,也會用做 Heartbeat (日誌條目爲空即爲 Heartbeat);
  3. InstallSnapshot RPC:由 Leader 發出,用於快照傳輸,雖然多數狀況都是每一個服務器獨立建立快照,可是Leader 有時候必須發送快照給一些落後太多的 Follower,這一般發生在 Leader 已經丟棄了下一條要發給該Follower 的日誌條目(Log Compaction 時清除掉了) 的狀況下。

任期邏輯時鐘

  1. 時間被劃分爲一個個任期 (term),term id 按時間軸單調遞增;
  2. 每個任期的開始都是 Leader 選舉,選舉成功以後,Leader 在任期內管理整個集羣,也就是 「選舉 + 常規操做」
  3. 每一個任期最多一個 Leader,可能沒有 Leader (spilt-vote 致使)。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%207.png

Leader election

選舉步驟

這裏摘取了論文中的步驟來進行說明:

  1. When servers start up, they begin as followers
  2. If a follower receives no communication over a period of time called the election timeout, then it assumes there is no viable leader and begins an election to choose a new leader
  3. To begin an election, a follower increments its current term and transitions to candidate state
  4. then votes for itself and issues RequestVote RPCs in parallel to each of
    the other servers in the cluster.
  5. A candidate wins an election if it receives votes from a majority of the servers in the full cluster for the same term
  6. Once a candidate wins an election, it becomes leader
  7. a candidate may receive an AppendEntries RPC
  8. If the leader’s term is at least as large as the candidate’s current term, then the candidate recognizes the leader as legitimate and returns to follower state
  9. If the term in the RPC is smaller than the candidate’s current term, then the candidate rejects the RPC and continues in candidate state

vote split

if many followers become candidates at the same time, votes could be split so that no candidate obtains a majority. When this happens, each candidate will time out and start a new election by incrementing its term and initiating another round of RequestVote RPCs.

Raft uses randomized election timeouts to ensure that split votes are rare and that they are resolved quickly.

下面來使用一個個的例子來具體說明選舉的過程

選舉要解決什麼

一個分佈式集羣能夠當作是由多條戰船組成的一支艦隊,各船之間經過旗語來保持信息交流。這樣的一支艦隊中,各船既不會互相徹底隔離,但也無法像陸地上那樣保持很是密切的聯繫,天氣、海況、船距、船隻戰損狀況致使船艦之間的聯繫存在但不可靠。

艦隊做爲一個統一的做戰集羣,須要有統一的共識、步調一致的命令,這些都要依賴於旗艦指揮。各艦船要服從於旗艦發出的指令,當旗艦不能繼續工做後,須要有別的戰艦接替旗艦的角色。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%208.png

如何在艦隊中,選出一艘獲得你們承認的旗艦,這就是 SOFAJRaft 中選舉要解決的問題。

什麼時候能夠發起選舉

在 SOFAJRaft 中,觸發標準就是通訊超時,當旗艦在規定的一段時間內沒有與 Follower 艦船進行通訊時,Follower 就能夠認爲旗艦已經不能正常擔任旗艦的職責,則 Follower 能夠去嘗試接替旗艦的角色。這段通訊超時被稱爲 Election Timeout (簡稱 ET), Follower 接替旗艦的嘗試也就是發起選舉請求。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%209.png

什麼時候真正發起選舉

在選舉中,只有當艦隊中超過一半的船都贊成,發起選舉的船纔可以成爲旗艦,不然就只能開始一輪新的選舉。因此若是 Follower 採起儘快發起選舉的策略,試圖儘早爲艦隊選出可用的旗艦,就可能引起一個潛在的風險:可能多艘船幾乎同時發起選舉,結果其中任何一支船都沒能得到超過半數選票,致使這一輪選舉無果,這就是上面所說的vote split。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2010.png

爲避免這種狀況,咱們採用隨機的選舉觸發時間,當 Follower 發現旗艦失聯以後,會選擇等待一段隨機的時間 Random(0, ET) ,若是等待期間沒有選出旗艦,則 Follower 再發起選舉。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2011.png

哪些候選者值得選票

SOFAJRaft 的選舉中包含了對兩個屬性的判斷:LogIndex 和 Term,這是整個選舉算法的核心部分。

  1. Term:咱們會對艦隊中旗艦的歷史進行編號,好比艦隊的第1任旗艦、第2任旗艦,這個數字咱們就用 Term 來表示。因爲艦隊中同時最多隻能有一艘艦船擔任旗艦,因此每個 Term 只歸屬於一艘艦船,顯然 Term 是單調遞增的。
  2. LogIndex:每任旗艦在職期間都會發布一些指令(稱其爲「旗艦令」,類比「總統令」),這些旗艦令固然也是要編號歸檔的,這個編號咱們用 Term 和 LogIndex 兩個維度來標識,表示「第 Term 任旗艦發佈的第 LogIndex 號旗艦令」。不一樣於現實中的總統令,咱們的旗艦令中的 LogIndex 是一直遞增的,不會由於旗艦的更迭而從頭開始計算。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2012.png

具體來講,參與投票的船 V 不會對下面兩種候選者 C 投票:一種是 lastTermC < lastTermV;另外一種是 (lastTermV == lastTermC) && (lastLogIndexV > lastLogIndexC)。

第一種狀況說明候選者 C 最後一次通訊過的旗艦已經不是最新的旗艦了;第二種狀況說明,雖然 C 和 V 都與同一個旗艦有過通訊,可是候選者 C 從旗艦處得到的旗艦令不如 V 完整 (lastLogIndexV > lastLogIndexC),因此 V 不會投票給它。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2013.png

什麼狀況下會發生step down

step down能夠發生在Candidate回退到Follower,也能夠發生在Leader中。若是是Candidate發生step down,那麼放棄競選本屆 Leader。若是是Leader,那麼會回退到Follower狀態,從新開啓選舉。

以下兩種狀況會讓 Candidate 退回 (step down) 到 Follower:

  1. 若是在 Candidate 等待 Servers 的投票結果期間收到了其餘擁有更高 Term 的 Server 發來的投票請求;
  2. 若是在 Candidate 等待 Servers 的投票結果期間收到了其餘擁有更高 Term 的 Server 發來的心跳;

而對於Leader來講,當發現有 Term 更高的 Leader 時也會退回到 Follower 狀態。

如何避免未入流的候選者「搗亂」

SOFAJRaft 將 LogIndex 和 Term 做爲選舉的評選標準,因此當一艘船發起選舉以前,會自增 Term 而後填到選舉請求裏發給其餘船隻 (多是一段很複雜的旗語),表示本身競選「第 Term + 1 任」旗艦。

這裏要先說明一個機制,它被用來保證各船隻的 Term 同步遞增:當參與投票的 Follower 船收到這個投票請求後,若是發現本身的 Term 比投票請求裏的小,就會自覺更新本身的 Term 向候選者看齊,這樣可以很方便的將 Term 遞增的信息同步到整個艦隊中。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2014.png

可是這種機制也帶來一個麻煩,若是一艘船由於本身的緣由沒有看到旗艦發出的旗語,他就會自覺得是的試圖競選成爲新的旗艦,雖然不斷髮起選舉且一直未能當選(由於旗艦和其餘船都正常通訊),可是它卻經過本身的投票請求實際擡升了全局的 Term,這在 SOFAJRaft 算法中會迫使旗艦 stepdown (從旗艦的位置上退下來)。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2015.png

因此咱們須要一種機制阻止這種「搗亂」,這就是預投票 (pre-vote) 環節。候選者在發起投票以前,先發起預投票,若是沒有獲得半數以上節點的反饋,則候選者就會識趣的放棄參選,也就不會擡升全局的 Term。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2016.png

在上面的比喻中,咱們能夠看到整個選舉操做的主線任務就是:

  1. Candidate 被 ET 觸發
  2. Candidate 開始嘗試發起 pre-vote 預投票
  3. Follower 判斷是否定可該 pre-vote request
  4. Candidate 根據 pre-vote response 來決定是否發RequestVoteRequest
  5. Follower 判斷是否定可該 RequestVoteRequest
  6. Candidate 根據 response 來判斷本身是否當選

Log replication

  1. leader對外提供服務

    一旦leader被選舉出來後,就須要對外提供服務了。下面是論文的原文:

    Once a leader has been elected, it begins servicing client requests. Each client request contains a command to be executed by the replicated state machines.

    翻譯:一旦leader被選舉出來後,它須要對外提供服務。每一個發送給leader的請求都會被複制的狀態機執行。

  2. leader執行日誌複製

    The leader appends the command to its log as a new entry, then issues AppendEntries RPCs in parallel to each of the other servers to replicate the entry.

    翻譯:leader會將每次請求的指令做爲一個對象寫入日誌中,而後經過AppendEntries操做通知其餘Follower複製該日誌。

  3. 日誌複製成功

    當leader複製給Follower的時候,有兩種狀況,一種是日誌被安全的複製到Follower節點中:

    When the entry has been safely replicated (as described below), the leader applies the entry to its state machine and returns the result of that execution to the client.

    翻譯:當日志被安全的複製到Follower後,leader會將該請求交給狀態機執行,而後返回執行結果給客戶端。

  4. 日誌複製失敗

另外一種狀況是Follower出現故障的狀況:

If followers crash or run slowly, or if network packets are lost, the leader retries AppendEntries RPCs indefinitely (even after it has responded to the client) until all followers eventually store all log entries.

翻譯:若是Follower宕機或者運行很慢,亦或是網絡包丟失,那麼leader會重複的進行AppendEntries操做,直到Follower正常處理該日誌複製。

LogEntry

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2017.png

如上圖所示,每一個方格表明一個LogEntry,能夠看到Log是由一個個LogEntry組成的,理想狀況下全部實例上該數組都是一致的。Log元素根據狀態的不一樣,又分爲未提交和已提交。只有已提交的LogEntry纔會返回客戶端寫入成功。

最上面一行是log index,也就是下標值,單調遞增,且連續的。方格內的數字表明的是term任期。

committed entry:A log entry is committed once the leader that created the entry has replicated it on a majority of the servers

也就是說若是一個日誌被複制到大多數的節點中,那麼這個日誌才能算是一個已提交的日誌。

Once a follower learns that a log entry is committed, it applies the entry to its local state machine (in log order).

一旦Follower得知這個LogEntry已提交,那麼就會將這個LogEntry放到狀態機中執行。

Follower日誌不一致

通常的狀況下,leader和Follower的日誌是保持一致的,而後現實中leader並不能保證不會crash,因此日誌可能會出現以下所示不一致的狀況:

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2018.png

如上圖,Follower可能比leader日誌少,可能會有多餘的日誌,可能會既丟失日誌也出現多餘的日誌。

因此Raft須要載保證日誌的一致性下作這幾件事:

  1. consistency check

    因爲leader在發送LogEntry的時候會帶上index和term,因此Follower在收到LogEntry以後要去檢測這條LogEntry是不是和以前的日誌是連續的,因此 Follower 會拒絕沒法和本地已有 Log 保持連續的複製請求,那麼這種狀況下就須要走 Log 恢復的流程。

  2. find the latest log entry

    若是不一致的話,那麼須要找到leader和Follower雙方都承認的那條日誌,這條日誌必須在Follower中是連續的,而且是在leader中存在的,具體操做以下:

    1. 因爲leader會爲每一個Follower維護一個nextIndex表,因此leader知道Follower最新的日誌須要發送哪條。

      The leader maintains a nextIndex for each follower,
      which is the index of the next log entry the leader will send to that follower.

    2. 當leader首次當選的時候,會將nextIndex設置爲本身最新的log的下一個Index

      When a leader first comes to power, it initializes all nextIndex values to the index just after the last one in its log.

    3. Leader 節點在經過 Replicator 和 Follower 創建鏈接以後,要發送一個 Probe 類型的探針請求,目的是知道 Follower 已經擁有的的日誌位置

    4. 若是發現日誌不一致(term和index要一致),那麼leader將會decrement nextIndex,而後從新發送AppendEntries請求,直至達到一個雙方都承認的日誌位置

      If a follower’s log is inconsistent with the leader’s, the AppendEntries consistency check will fail in the next AppendEntries RPC. After a rejection, the leader decrements nextIndex and retries the AppendEntries RPC. Eventually nextIndex will reach a point where the leader and follower logs match.

    5. 當leader發送的AppendEntries請求是成功的時候,那麼Follower會清除衝突的日誌,並接受leader的日誌。

      Eventually nextIndex will reach a point where the leader and follower logs match. When this happens, AppendEntries will succeed, which removes any conflicting entries in the follower’s log and appends entries from the leader’s log

下面講一下JRaft中日誌複製的細節

被複制的日誌是有序且連續的

SOFAJRaft 在日誌複製時,其日誌傳輸的順序也要保證嚴格的順序,全部日誌既不能亂序也不能有空洞 (也就是說不能被漏掉)。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2019.png

複製日誌是併發的

SOFAJRaft 中 Leader 節點會同時向多個 Follower 節點複製日誌,在 Leader 中爲每個 Follower 分配一個 Replicator,專用來處理複製日誌任務。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2020.png

複製日誌是批量的

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2021.png

日誌複製中的快照

用 Snapshot 可以讓 Follower 快速跟上 Leader 的日誌進度,再也不回放很早之前的日誌信息,即緩解了網絡的吞吐量,又提高了日誌同步的效率。

複製日誌的 pipeline 機制

Pipeline 使得 Leader 和 Follower 雙方再也不須要嚴格聽從 「Request -Response - Request」 的交互模式,Leader 能夠在沒有收到 Response 的狀況下,持續的將複製日誌的 AppendEntriesRequest 發送給 Follower。

在具體實現時,Leader 只須要針對每一個 Follower 維護一個隊列,記錄下已經複製的日誌,若是有日誌複製失敗的狀況,就將其後的日誌重發給 Follower。這樣就能保證日誌複製的可靠性。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2022.png

複製日誌的細節

  1. 檢測Follower日誌狀態

    Leader 節點在經過 Replicator 和 Follower 創建鏈接以後,要發送一個 Probe 類型的探針請求,目的是知道 Follower 已經擁有的的日誌位置,以便於向 Follower 發送後續的日誌。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2023.png

  1. 用 Inflight 來輔助實現 pipeline

Inflight 是對批量發送出去的 logEntry 的一種抽象,他表示哪些 logEntry 已經被封裝成日誌複製 request 發送出去了。

JRaft%20Raft%2068a32851dce84485ae792f586153a231/Untitled%2024.png

Leader 維護一個 queue,每發出一批 logEntry 就向 queue 中 添加一個表明這一批 logEntry 的 Inflight,這樣當它知道某一批 logEntry 複製失敗以後,就能夠依賴 queue 中的 Inflight 把該批次 logEntry 以及後續的全部日誌從新複製給 follower。既保證日誌複製可以完成,又保證了複製日誌的順序不變。

線性一致性讀

所謂線性一致讀,一個簡單的例子是在 t1 的時刻咱們寫入了一個值,那麼在 t1 以後,咱們必定能讀到這個值,不可能讀到 t1 以前的舊值。

當 Client 向集羣發起寫操做的請求而且得到成功響應以後,該寫操做的結果要對全部後來的讀請求可見。

Raft Log read

實現線性一致讀最常規的辦法是走 Raft 協議,將讀請求一樣按照 Log 處理,經過 Log 複製和狀態機執行來獲取讀結果,而後再把讀取的結果返回給 Client。由於 Raft 原本就是一個爲了實現分佈式環境下線性一致性的算法,因此經過 Raft 很是方便的實現線性 Read,也就是將任何的讀請求走一次 Raft Log,等此 Log 提交以後在 apply 的時候從狀態機裏面讀取值,必定可以保證這個讀取到的值是知足線性要求的。

固然,由於每次 Read 都須要走 Raft 流程,Raft Log 存儲、複製帶來刷盤開銷、存儲開銷、網絡開銷,走 Raft Log不只僅有日誌落盤的開銷,還有日誌複製的網絡開銷,另外還有一堆的 Raft 「讀日誌」 形成的磁盤佔用開銷,致使 Read 操做性能是很是低效的,因此在讀操做不少的場景下對性能影響很大,在讀比重很大的系統中是沒法被接受的,一般都不會使用。

在 Raft 裏面,節點有三個狀態:Leader,Candidate 和 Follower,任何 Raft 的寫入操做都必須通過 Leader,只有 Leader 將對應的 Raft Log 複製到 Majority 的節點上面認爲這次寫入是成功的。因此若是當前 Leader 能肯定必定是 Leader,那麼可以直接在此 Leader 上面讀取數據,由於對於 Leader 來講,若是確認一個 Log 已經提交到大多數節點,在 t1 的時候 apply 寫入到狀態機,那麼在 t1 後的 Read 就必定能讀取到這個新寫入的數據。

也就是說,這樣相比Raft Log read來講,少了一個Log複製的過程,取而代之的是隻要確認本身的leader身份就能夠直接從leader上面直接讀取數據,從而保證數據必定是準確的。

那麼如何確認 Leader 在處理此次 Read 的時候必定是 Leader 呢?在 Raft 論文裏面,提到兩種方法:

  • ReadIndex Read
  • Lease Read

ReadIndex Read

第一種是 ReadIndex Read,當 Leader 須要處理 Read 請求時,Leader 與過半機器交換心跳信息肯定本身仍然是 Leader 後可提供線性一致讀:

  1. Leader 將本身當前 Log 的 commitIndex 記錄到一個 Local 變量 ReadIndex 裏面;
  2. 接着向 Followers 節點發起一輪 Heartbeat,若是半數以上節點返回對應的 Heartbeat Response,那麼 Leader就可以肯定如今本身仍然是 Leader;
  3. Leader 等待本身的 StateMachine 狀態機執行,至少應用到 ReadIndex 記錄的 Log,直到 applyIndex 超過 ReadIndex,這樣就可以安全提供 Linearizable Read,也沒必要管讀的時刻是否 Leader 已飄走;
  4. Leader 執行 Read 請求,將結果返回給 Client。

使用 ReadIndex Read 提供 Follower Read 的功能,很容易在 Followers 節點上面提供線性一致讀,Follower 收到 Read 請求以後:

  1. Follower 節點向 Leader 請求最新的 ReadIndex;
  2. Leader 仍然走一遍以前的流程,執行上面前 3 步的過程(肯定本身真的是 Leader),而且返回 ReadIndex 給 Follower;
  3. Follower 等待當前的狀態機的 applyIndex 超過 ReadIndex;
  4. Follower 執行 Read 請求,將結果返回給 Client。

ReadIndex Read 使用 Heartbeat 方式代替了日誌複製,省去 Raft Log 流程。相比較於走 Raft Log 方式,ReadIndex Read 省去磁盤的開銷,可以大幅度提高吞吐量。雖然仍然會有網絡開銷,可是 Heartbeat 原本就很小,因此性能仍是很是好的。

Lease Read

雖然 ReadIndex Read 比原來的 Raft Log Read 快不少,但畢竟仍是存在 Heartbeat 網絡開銷,因此考慮作更進一步的優化。

Raft 論文裏面說起一種經過 Clock + Heartbeat 的 Lease Read 優化方法,也就是 Leader 發送 Heartbeat 的時候首先記錄一個時間點 Start,當系統大部分節點都回復 Heartbeat Response,因爲 Raft 的選舉機制,Follower 會在 Election Timeout 的時間以後才從新發生選舉,下一個 Leader 選舉出來的時間保證大於 Start+Election Timeout/Clock Drift Bound,因此能夠認爲 Leader 的 Lease 有效期能夠到 Start+Election Timeout/Clock Drift Bound 時間點。Lease Read 與 ReadIndex 相似但更進一步優化,不只節省 Log,並且省掉網絡交互,大幅提高讀的吞吐量而且可以顯著下降延時。

Lease Read 基本思路是 Leader 取一個比 Election Timeout 小的租期(最好小一個數量級),在租約期內不會發生選舉,確保 Leader 不會變化,因此跳過 ReadIndex 的發送Heartbeat的步驟,也就下降了延時。

因而可知 Lease Read 的正確性和時間是掛鉤的,依賴本地時鐘的準確性,所以雖然採用 Lease Read 作法很是高效,可是仍然面臨風險問題,也就是存在預設的前提即各個服務器的 CPU Clock 的時間是準的,即便有偏差,也會在一個很是小的 Bound 範圍裏面,時間的實現相當重要,若是時鐘漂移嚴重,各個服務器之間 Clock 走的頻率不同,這套 Lease 機制可能出問題。

相關文章
相關標籤/搜索