一文搞懂Raft算法

  raft是工程上使用較爲普遍的強一致性、去中心化、高可用的分佈式協議。在這裏強調了是在工程上,由於在學術理論界,最耀眼的仍是大名鼎鼎的Paxos。但Paxos是:少數真正理解的人以爲簡單,還沒有理解的人以爲很難,大多數人都是隻知其一;不知其二。本人也花了不少時間、看了不少材料也沒有真正理解。直到看到raft的論文,兩位研究者也提到,他們也花了很長的時間來理解Paxos,他們也以爲很難理解,因而研究出了raft算法。html

   raft是一個共識算法(consensus algorithm),所謂共識,就是多個節點對某個事情達成一致的見解,即便是在部分節點故障、網絡延時、網絡分割的狀況下。這些年最爲火熱的加密貨幣(比特幣、區塊鏈)就須要共識算法,而在分佈式系統中,共識算法更多用於提升系統的容錯性,好比分佈式存儲中的複製集(replication),在帶着問題學習分佈式系統之中心化複製集一文中介紹了中心化複製集的相關知識。raft協議就是一種leader-based的共識算法,與之相應的是leaderless的共識算法。node

  本文基於論文In Search of an Understandable Consensus Algorithm對raft協議進行分析,固然,仍是建議讀者直接看論文。git

  本文地址:http://www.javashuo.com/article/p-relcvclb-be.htmlgithub

raft算法概覽

  Raft算法的頭號目標就是容易理解(UnderStandable),這從論文的標題就能夠看出來。固然,Raft加強了可理解性,在性能、可靠性、可用性方面是不輸於Paxos的。web

Raft more understandable than Paxos and also provides a better foundation for building practical systems算法

   爲了達到易於理解的目標,raft作了不少努力,其中最主要是兩件事情:mongodb

  • 問題分解
  • 狀態簡化

   問題分解是將"複製集中節點一致性"這個複雜的問題劃分爲數個能夠被獨立解釋、理解、解決的子問題。在raft,子問題包括,leader electionlog replicationsafetymembership changes。而狀態簡化更好理解,就是對算法作出一些限制,減小須要考慮的狀態數,使得算法更加清晰,更少的不肯定性(好比,保證新選舉出來的leader會包含全部commited log entry)數組

Raft implements consensus by first electing a distinguished leader, then giving the leader complete responsibility for managing the replicated log. The leader accepts log entries from clients, replicates them on other servers, and tells servers when it is safe to apply log entries to their state machines. A leader can fail or become disconnected from the other servers, in which case a new leader is elected.安全

   上面的引文對raft協議的工做原理進行了高度的歸納:raft會先選舉出leader,leader徹底負責replicated log的管理。leader負責接受全部客戶端更新請求,而後複製到follower節點,並在「安全」的時候執行這些請求。若是leader故障,followes會從新選舉出新的leader。網絡

   這就涉及到raft最新的兩個子問題: leader election和log replication

leader election

   raft協議中,一個節點任一時刻處於如下三個狀態之一:

  • leader
  • follower
  • candidate

   給出狀態轉移圖能很直觀的直到這三個狀態的區別

  能夠看出全部節點啓動時都是follower狀態;在一段時間內若是沒有收到來自leader的心跳,從follower切換到candidate,發起選舉;若是收到majority的形成票(含本身的一票)則切換到leader狀態;若是發現其餘節點比本身更新,則主動切換到follower。

   總之,系統中最多隻有一個leader,若是在一段時間裏發現沒有leader,則你們經過選舉-投票選出leader。leader會不停的給follower發心跳消息,代表本身的存活狀態。若是leader故障,那麼follower會轉換成candidate,從新選出leader。

term

   從上面能夠看出,哪一個節點作leader是你們投票選舉出來的,每一個leader工做一段時間,而後選出新的leader繼續負責。這根民主社會的選舉很像,每一屆新的履職期稱之爲一屆任期,在raft協議中,也是這樣的,對應的術語叫term

   term(任期)以選舉(election)開始,而後就是一段或長或短的穩定工做期(normal Operation)。從上圖能夠看到,任期是遞增的,這就充當了邏輯時鐘的做用;另外,term 3展現了一種狀況,就是說沒有選舉出leader就結束了,而後會發起新的選舉,後面會解釋這種split vote的狀況。

選舉過程詳解

   上面已經說過,若是follower在election timeout內沒有收到來自leader的心跳,(也許此時尚未選出leader,你們都在等;也許leader掛了;也許只是leader與該follower之間網絡故障),則會主動發起選舉。步驟以下:

  • 增長節點本地的 current term ,切換到candidate狀態
  • 投本身一票
  • 並行給其餘節點發送 RequestVote RPCs
  • 等待其餘節點的回覆

   在這個過程當中,根據來自其餘節點的消息,可能出現三種結果

  1. 收到majority的投票(含本身的一票),則贏得選舉,成爲leader
  2. 被告知別人已當選,那麼自行切換到follower
  3. 一段時間內沒有收到majority投票,則保持candidate狀態,從新發出選舉

   第一種狀況,贏得了選舉以後,新的leader會馬上給全部節點發消息,廣而告之,避免其他節點觸發新的選舉。在這裏,先回到投票者的視角,投票者如何決定是否給一個選舉請求投票呢,有如下約束:

  • 在任一任期內,單個節點最多隻能投一票
  • 候選人知道的信息不能比本身的少(這一部分,後面介紹log replication和safety的時候會詳細介紹)
  • first-come-first-served 先來先得

   第二種狀況,好比有三個節點A B C。A B同時發起選舉,而A的選舉消息先到達C,C給A投了一票,當B的消息到達C時,已經不能知足上面提到的第一個約束,即C不會給B投票,而A和B顯然都不會給對方投票。A勝出以後,會給B,C發心跳消息,節點B發現節點A的term不低於本身的term,知道有已經有Leader了,因而轉換成follower。

   第三種狀況,沒有任何節點得到majority投票,好比下圖這種狀況:

   總共有四個節點,Node C、Node D同時成爲了candidate,進入了term 4,但Node A投了NodeD一票,NodeB投了Node C一票,這就出現了平票 split vote的狀況。這個時候你們都在等啊等,直到超時後從新發起選舉。若是出現平票的狀況,那麼就延長了系統不可用的時間(沒有leader是不能處理客戶端寫請求的),所以raft引入了randomized election timeouts來儘可能避免平票狀況。同時,leader-based 共識算法中,節點的數目都是奇數個,儘可能保證majority的出現。

log replication

   當有了leader,系統應該進入對外工做期了。客戶端的一切請求來發送到leader,leader來調度這些併發請求的順序,而且保證leader與followers狀態的一致性。raft中的作法是,將這些請求以及執行順序告知followers。leader和followers以相同的順序來執行這些請求,保證狀態一致。

Replicated state machines

   共識算法的實現通常是基於複製狀態機(Replicated state machines),何爲複製狀態機:

If two identical, deterministic processes begin in the same state and get the same inputs in the same order, they will produce the same output and end in the same state.

   簡單來講:相同的初識狀態 + 相同的輸入 = 相同的結束狀態。引文中有一個很重要的詞deterministic,就是說不一樣節點要以相同且肯定性的函數來處理輸入,而不要引入一下不肯定的值,好比本地時間等。如何保證全部節點 get the same inputs in the same order,使用replicated log是一個很不錯的注意,log具備持久化、保序的特色,是大多數分佈式系統的基石。

  所以,能夠這麼說,在raft中,leader將客戶端請求(command)封裝到一個個log entry,將這些log entries複製(replicate)到全部follower節點,而後你們按相同順序應用(apply)log entry中的command,則狀態確定是一致的。

  下圖形象展現了這種log-based replicated state machine

請求完整流程

  當系統(leader)收到一個來自客戶端的寫請求,到返回給客戶端,整個過程從leader的視角來看會經歷如下步驟:

  • leader append log entry
  • leader issue AppendEntries RPC in parallel
  • leader wait for majority response
  • leader apply entry to state machine
  • leader reply to client
  • leader notify follower apply log

  能夠看到日誌的提交過程有點相似兩階段提交(2PC),不過與2PC的區別在於,leader只須要大多數(majority)節點的回覆便可,這樣只要超過一半節點處於工做狀態則系統就是可用的。

  那麼日誌在每一個節點上是什麼樣子的呢

  不難看到,logs由順序編號的log entry組成 ,每一個log entry除了包含command,還包含產生該log entry時的leader term。從上圖能夠看到,五個節點的日誌並不徹底一致,raft算法爲了保證高可用,並非強一致性,而是最終一致性,leader會不斷嘗試給follower發log entries,直到全部節點的log entries都相同。

  在上面的流程中,leader只須要日誌被複制到大多數節點便可向客戶端返回,一旦向客戶端返回成功消息,那麼系統就必須保證log(實際上是log所包含的command)在任何異常的狀況下都不會發生回滾。這裏有兩個詞:commit(committed),apply(applied),前者是指日誌被複制到了大多數節點後日志的狀態;然後者則是節點將日誌應用到狀態機,真正影響到節點狀態。

The leader decides when it is safe to apply a log entry to the state machines; such an entry is called committed. Raft guarantees that committed entries are durable and will eventually be executed by all of the available state machines. A log entry is committed once the leader that created the entry has replicated it on a majority of the servers

safety

  在上面提到只要日誌被複制到majority節點,就能保證不會被回滾,即便在各類異常狀況下,這根leader election提到的選舉約束有關。在這一部分,主要討論raft協議在各類各樣的異常狀況下如何工做的。

  衡量一個分佈式算法,有許多屬性,如

  • safety:nothing bad happens,
  • liveness: something good eventually happens.

  在任何系統模型下,都須要知足safety屬性,即在任何狀況下,系統都不能出現不可逆的錯誤,也不能向客戶端返回錯誤的內容。好比,raft保證被複制到大多數節點的日誌不會被回滾,那麼就是safety屬性。而raft最終會讓全部節點狀態一致,這屬於liveness屬性。

  raft協議會保證如下屬性

Election safety

  選舉安全性,即任一任期內最多一個leader被選出。這一點很是重要,在一個複製集中任什麼時候刻只能有一個leader。系統中同時有多餘一個leader,被稱之爲腦裂(brain split),這是很是嚴重的問題,會致使數據的覆蓋丟失。在raft中,兩點保證了這個屬性:

  • 一個節點某一任期內最多隻能投一票;
  • 只有得到majority投票的節點纔會成爲leader。

  所以,某一任期內必定只有一個leader

log matching

  頗有意思,log匹配特性, 就是說若是兩個節點上的某個log entry的log index相同且term相同,那麼在該index以前的全部log entry應該都是相同的。如何作到的?依賴於如下兩點

  • If two entries in different logs have the same index and term, then they store the same command.
  • If two entries in different logs have the same index and term, then the logs are identical in all preceding entries.

  首先,leader在某一term的任一位置只會建立一個log entry,且log entry是append-only。其次,consistency check。leader在AppendEntries中包含最新log entry以前的一個log 的term和index,若是follower在對應的term index找不到日誌,那麼就會告知leader不一致。

  在沒有異常的狀況下,log matching是很容易知足的,但若是出現了node crash,狀況就會變得負責。好比下圖

  注意:上圖的a-f不是6個follower,而是某個follower可能存在的六個狀態

  leader、follower均可能crash,那麼follower維護的日誌與leader相比可能出現如下狀況

  • 比leader日誌少,如上圖中的ab
  • 比leader日誌多,如上圖中的cd
  • 某些位置比leader多,某些日誌比leader少,如ef(多少是針對某一任期而言)

  當出現了leader與follower不一致的狀況,leader強制follower複製本身的log

To bring a follower’s log into consistency with its own, the leader must find the latest log entry where the two logs agree, delete any entries in the follower’s log after that point, and send the follower all of the leader’s entries after that point.

  leader會維護一個nextIndex[]數組,記錄了leader能夠發送每個follower的log index,初始化爲eader最後一個log index加1, 前面也提到,leader選舉成功以後會當即給全部follower發送AppendEntries RPC(不包含任何log entry, 也充小心跳消息),那麼流程總結爲:

s1 leader 初始化nextIndex[x]爲 leader最後一個log index + 1
s2 AppendEntries裏prevLogTerm prevLogIndex來自 logs[nextIndex[x] - 1]
s3 若是follower判斷prevLogIndex位置的log term不等於prevLogTerm,那麼返回 false,不然返回True
s4 leader收到follower的恢復,若是返回值是True,則nextIndex[x] -= 1, 跳轉到s2. 不然
s5 同步nextIndex[x]後的全部log entries

leader completeness vs elcetion restriction

  leader完整性:若是一個log entry在某個任期被提交(committed),那麼這條日誌必定會出如今全部更高term的leader的日誌裏面。這個跟leader election、log replication都有關。

  • 一個日誌被複制到majority節點纔算committed
  • 一個節點獲得majority的投票才能成爲leader,而節點A給節點B投票的其中一個前提是,B的日誌不能比A的日誌舊。下面的引文指處瞭如何判斷日誌的新舊

voter denies its vote if its own log is more up-to-date than that of the candidate.

If the logs have last entries with different terms, then the log with the later term is more up-to-date. If the logs end with the same term, then whichever log is longer is more up-to-date.

  上面兩點都提到了majority:commit majority and vote majority,根據Quorum,這兩個majority必定是有重合的,所以被選舉出的leader必定包含了最新的committed的日誌。

  raft與其餘協議(Viewstamped Replication、mongodb)不一樣,raft始終保證leade包含最新的已提交的日誌,所以leader不會從follower catchup日誌,這也大大簡化了系統的複雜度。

corner case

stale leader

  raft保證Election safety,即一個任期內最多隻有一個leader,但在網絡分割(network partition)的狀況下,可能會出現兩個leader,但兩個leader所處的任期是不一樣的。以下圖所示

  系統有5個節點ABCDE組成,在term1,Node B是leader,但Node A、B和Node C、D、E之間出現了網絡分割,所以Node C、D、E沒法收到來自leader(Node B)的消息,在election time以後,Node C、D、E會分期選舉,因爲知足majority條件,Node E成爲了term 2的leader。所以,在系統中貌似出現了兩個leader:term 1的Node B, term 2的Node E, Node B的term更舊,但因爲沒法與Majority節點通訊,NodeB仍然會認爲本身是leader。

  在這樣的狀況下,咱們來考慮讀寫。

  首先,若是客戶端將請求發送到了NodeB,NodeB沒法將log entry 複製到majority節點,所以不會告訴客戶端寫入成功,這就不會有問題。

  對於讀請求,stale leader可能返回stale data,好比在read-after-write的一致性要求下,客戶端寫入到了term2任期的leader Node E,但讀請求發送到了Node B。若是要保證不返回stale data,leader須要check本身是否過期了,辦法就是與大多數節點通訊一次,這個可能會出現效率問題。另外一種方式是使用lease,但這就會依賴物理時鐘。

  從raft的論文中能夠看到,leader轉換成follower的條件是收到來自更高term的消息,若是網絡分割一直持續,那麼stale leader就會一直存在。而在raft的一些實現或者raft-like協議中,leader若是收不到majority節點的消息,那麼能夠本身step down,自行轉換到follower狀態。

State Machine Safety

  前面在介紹safety的時候有一條屬性沒有詳細介紹,那就是State Machine Safety:

State Machine Safety: if a server has applied a log entry at a given index to its state machine, no other server will ever apply a different log entry for the same index.

  若是節點將某一位置的log entry應用到了狀態機,那麼其餘節點在同一位置不能應用不一樣的日誌。簡單點來講,全部節點在同一位置(index in log entries)應該應用一樣的日誌。可是彷佛有某些狀況會違背這個原則:

  上圖是一個較爲複雜的狀況。在時刻(a), s1是leader,在term2提交的日誌只賦值到了s1 s2兩個節點就crash了。在時刻(b), s5成爲了term 3的leader,日誌只賦值到了s5,而後crash。而後在(c)時刻,s1又成爲了term 4的leader,開始賦值日誌,因而把term2的日誌複製到了s3,此刻,能夠看出term2對應的日誌已經被複制到了majority,所以是committed,能夠被狀態機應用。不幸的是,接下來(d)時刻,s1又crash了,s5從新當選,而後將term3的日誌複製到全部節點,這就出現了一種奇怪的現象:被複制到大多數節點(或者說可能已經應用)的日誌被回滾。

  究其根本,是由於term4時的leader s1在(C)時刻提交了以前term2任期的日誌。爲了杜絕這種狀況的發生:

Raft never commits log entries from previous terms by counting replicas.
Only log entries from the leader’s current term are committed by counting replicas; once an entry from the current term has been committed in this way, then all prior entries are committed indirectly because of the Log Matching Property.

  也就是說,某個leader選舉成功以後,不會直接提交前任leader時期的日誌,而是經過提交當前任期的日誌的時候「順手」把以前的日誌也提交了,具體怎麼實現了,在log matching部分有詳細介紹。那麼問題來了,若是leader被選舉後沒有收到客戶端的請求呢,論文中有提到,在任期開始的時候發當即嘗試複製、提交一條空的log

Raft handles this by having each leader commit a blank no-op entry into the log at the start of its term.

  所以,在上圖中,不會出現(C)時刻的狀況,即term4任期的leader s1不會複製term2的日誌到s3。而是如同(e)描述的狀況,經過複製-提交 term4的日誌順便提交term2的日誌。若是term4的日誌提交成功,那麼term2的日誌也必定提交成功,此時即便s1crash,s5也不會從新當選。

leader crash

  follower的crash處理方式相對簡單,leader只要不停的給follower發消息便可。當leader crash的時候,事情就會變得複雜。在這篇文章中,做者就給出了一個更新請求的流程圖。
例子
  咱們能夠分析leader在任意時刻crash的狀況,有助於理解raft算法的容錯性。

總結

  raft將共識問題分解成兩個相對獨立的問題,leader election,log replication。流程是先選舉出leader,而後leader負責複製、提交log(log中包含command)

  爲了在任何異常狀況下系統不出錯,即知足safety屬性,對leader election,log replication兩個子問題有諸多約束

leader election約束:

  • 同一任期內最多隻能投一票,先來先得
  • 選舉人必須比本身知道的更多(比較term,log index)

log replication約束:

  • 一個log被複制到大多數節點,就是committed,保證不會回滾
  • leader必定包含最新的committed log,所以leader只會追加日誌,不會刪除覆蓋日誌
  • 不一樣節點,某個位置上日誌相同,那麼這個位置以前的全部日誌必定是相同的
  • Raft never commits log entries from previous terms by counting replicas.

  本文是在看完raft論文後本身的總結,不必定全面。我的以爲,若是隻是相對raft協議有一個簡單瞭解,看這個動畫演示就足夠了,若是想深刻了解,仍是要看論文,論文中Figure 2對raft算法進行了歸納。最後,仍是找一個實現了raft算法的系統來看看更好。

references

https://web.stanford.edu/~ouster/cgi-bin/papers/raft-atc14
https://raft.github.io/
http://thesecretlivesofdata.com/raft/

相關文章
相關標籤/搜索