理解分散式一致性與Raft演算法

吳紋羽發表於2019-08-06

理解分散式一致性與Raft演算法

永遠繞不開的CAP定理

出於可用性及負載方面考慮,一個分散式系統中資料必然不會只存在於一臺機器,一致性簡單地說就是分散式系統中的各個部分保持資料一致

1-1PF3102KOJ.jpg-18kB

但讓資料保持一致往往並不像看上去那麼簡單,假設我們有兩臺機器A與B,這時A更新了資料,A需要將更新的指令同步到B,如果A到B網路傳輸到B資料落地的總時間為500ms,那麼這個500ms就是可能造成資料不一致的時間視窗,假如兩臺機器分屬不同機房,甚至分屬不同國家的機房,其時間視窗會更大,具體會造成什麼影響呢?

舉個栗子?,假如使用者a進行轉賬操作,節點A更新了資料,他在轉賬後顯示餘額為0,但他重新整理一下頁面請求到了節點B發現自己的餘額又回到了原來的餘額,然而這只是顯示不一致,但假設他又在節點B上進行在進行了轉賬操作,轉賬中的餘額校驗也依舊訪問的是節點B,那麼可能會造成的影響不言而喻。

CAP定理

在談分散式一致性之前,我們首先了解一個定理,那就是CAP定理,請注意,CAP是定理而非理論,CAP定理證明了一個分散式系統只能同時滿足一致性(Consistency)、可用性(Availability)和分割槽容錯性(Partition tolerance)這三項中的兩項。

truth-of-cap-theorem-diagram1.png-55.2kB

  • 一致性(Consistency):指的就是整個叢集的所有節點資料保持一致
  • 可用性(Availability):在資料同步過程中,叢集是否是可用狀態
  • 分割槽容忍性(Partition tolerance):是否能夠容忍網路分割槽的發生

C和A相對好理解,這裡著重說一下P(Partition tolerance)分割槽容忍性,聽著比較拗口,說實話,剛開始看到他的時候也是一臉茫然,分割槽?什麼是分割槽?其實分割槽(Partition)簡單的說就是伺服器之間的網路通訊斷了,兩邊的伺服器變成兩個獨立的叢集,這就是所謂的分割槽,斷了的原因有很多比如交換機故障,路由器故障,掃地阿姨把網線拔了等等,然後什麼是容忍性(tolerance),這個就很好理解了,是不是發生分割槽了我的服務就不再提供服務了呢,當然不是,否則也就沒有高可用一說了,那麼我們能否說不做網路故障可能發生的假設呢,答案必然是不能的,首先網路延遲是必然的,網路延遲的過程中也可以將其當做發生分割槽,另外網路故障也可以說是必然的,詳見墨菲定律(滑稽臉)

其實P也不是完全不能拋棄的,很簡單,我們幹掉網線,只保留一臺單機資料庫,就只有一個區,何來分割槽一說,對啊,所以說我們常見的傳統單機資料庫(RDBMS)就是可以滿足CA的,如:MySQL,Oracle等等,當然,前提是你沒做主從之類的分散式方案。由此可見,在所有分散式系統中P幾乎都是不可拋棄的,所以說我們的選擇也就只剩兩個了AP和CP。

如何理解CP與AP?

舉個簡單例子,若我們叢集有兩臺機器,而兩臺機器網路發生中斷而導致出現分割槽:

  • 如果我們在雙發無法通訊的情況下繼續允許兩邊進行寫入,則必然造成資料的不一致,這時我們實現了AP而拋棄了C。
  • 但如果我們禁止其中一方進行寫入,這樣就可以保證系統的一致性了,但我們卻因為將一中一個副本置為不可用而導致了A屬性的喪失,也是說實現了CP。

這樣CAP理論是否變得好理解了一些?當然現在對於CAP理論的爭議也很大,但並不是懷疑CAP定理是否能被證偽,而是說CAP理論也許並不適用於我們通常對資料庫系統的描述,有時我們並不能簡單的將資料庫劃分為AP或CP。舉個栗子?,如果我們有一個single master+multiple slaves的mysql資料庫,當leader不可用時,使用者則不能進行寫入,也就喪失了A屬性,但由於MySQL是通過binlog非同步同步資料庫的,使用者也有可能讀到的是舊資料,所以說該系統也許既不滿足A也不滿足C,僅僅滿足了P屬性。

(關於CAP的爭議討論推薦閱讀:《請不要再稱資料庫是CP或者AP (Please stop calling databases CP or AP)》

async-replication-diagram.png-80.3kB

線性一致性與一致性級別

基於CAP理論的AP與CP互斥的原則,針對C的取捨,我們簡單劃分成了3個級別來描述(特殊場景下會更多):

強一致性(線性一致性)

強一致性可以理解為當寫入操作完成後,任何客戶端去訪問任何儲存節點的值都是最新的值,將分散式的一致性過程對客戶端透明,客戶端操作一個強一致性的資料庫時感覺自己操作的是一個單機資料庫,強一致性就是CAP定理中所描述的C(Consistency),同時下面的講解的Raft演算法就是實現線性一致性的一直

弱一致性

弱一致性是與強一致性對立的一種一致性級別,弱一致性簡單地說不去對一致性進行保證,客戶端在寫入成功後依舊可能會得到舊的值,這也就是捨棄C可能造成的問題,但某些系統下,對一致性的要求並不高,從而可以捨棄強一致策略可能帶來的效能與可用性消耗。

最終一致性

最終一致性也可以理解成弱一致性的一種,使用這種一致性級別,依舊可能在寫入後讀到舊值,但做出的改進是要求資料在有限的時間視窗內最終達到一致的狀態。也就說就算現在不一致,也早晚會達到一致,但狹義上的弱一致性並不對一致性做出任何保證,也許某些節點永遠不會達到一致,其實最終一致性的核心就是保證同步的請求不會丟失,在請求到達時節點的狀態變為最新狀態,而不考慮請求傳輸時的不一致視窗,DNS就是典型的最終一致性系統。

Raft演算法

Raft演算法的論文題目是《In Search of an Understandable Consensus Algorithm (Extended Version)》(《尋找一種易於理解的一致性演算法(擴充套件版)》),很容易理解,Raft演算法的初衷就是設計一個相較於Paxos更易於理解的強一致性演算法,雖說更好理解,但依舊畢竟是分散式一致性演算法,其演算法複雜程度及各種狀態的多樣性依舊需要較高的理解成本。但是花時間成本去學習Raft是值得的,理解Raft後能夠很大程度加深你對分散式及線性一致性的理解,這次僅是基於個人理解的描述性介紹Raft演算法,不對如選舉異常或當機等情況的處理做更多探究,如果有什麼疑問歡迎進行討論,同時感興趣的同學也推薦閱讀Raft原版論文(中文版):尋找一種易於理解的一致性演算法(擴充套件版)

Raft演算法作為單純的一致性演算法,使用場景並非僅僅在資料庫,Raft演算法將分散式一致性問題拆分為若干個子問題進行解決,其他的相關機制均是這三個子問題的延伸,接下面我們詳細闡述一下這三個子問題。
注:下面所涉及到所有RPC的協議欄位都可以在論文中找到

Leader選舉:避免多節點狀態競爭

通常Raft節點擁有5個節點,將這五個節點分為三種角色Leader,Follower和Candidate,所有節點只可能是這三種角色,並且所有節點的對等的,它們都可以成為這三種角色的其中之一。
其實雖然有三種角色,但進行抽象以後可以理解為叢集只有Leader與Follower兩種角色,Candidate是Leader的預備役而已。
簡單的講可以將Raft系統理解為一個一主多從(single master multiple slaves)的RDBMS(MySQL等),但RDBMS通常採用的方案是Master節點用於寫,而Followers用於讀,但Raft不同的是不管是讀和寫都要經由Master節點分發給Followers節點,這樣做的缺點是可能會導致Leader節點的負載會高於Followers,但這樣做的好處是實現了強一致性,強一致性的部分下面會詳細說明。
既然是單Leader,那麼假設我們的Leader當機了怎麼辦,這時候我們就可以將這個問題拆分為兩個部分:

  1. 如何發現Leader節點當機
  2. 如果發現Leader節點當機如何重新選舉Leader

第一個問題在Raft中的解決方案是增加心跳(heartbeat),Leader定期向所有Followers傳送心跳訊息,若follower在一段時間內沒有收到leader的選票(心跳超時,超時時間隨機,由Follower自己控制),則認為Leader當機,開始選舉
第二個問題引申出了三個概念

  1. 任期號(term)
  2. 選票(vote)
  3. 候選人(candidate)

當Follower認定Leader當機後,他會自告奮勇的認為自己應該成為新一輪的Leader,這時他會將自己的狀態轉換為candidate,並向Node廣播請求選票的RPC,如果超過半數的Node同意他成為新的Leader則代表他應得了這次選舉,他就會變成新的Leader。
我們來簡單說幾個異常的情況及Raft的處理方式:

  • 雖然心跳超時隨機的策略大幅度減少了兩個Followers同時超時的情況,但依舊不能保證不會出現兩個candidates同時超時的情況,假如說出現兩個candidate同時超時的情況就有可能接連發生兩個candidates同事發起選舉投票,兩個candidates將選票瓜分,最終沒有人能夠獲得超過半數的選票,這個異常機制的保障措施是candidates當角色發生轉換後candidates會重置超時時間,如若一段時間內沒有新的Leader產生,則重新發起新一輪的選舉,因為所有超時時間均是隨機的,所以第二次發生瓜分選票的可能性已經變得微乎其微。
    在論文中也驗證了小幅度的隨機既可以讓選票瓜分的情況大大減小:
只需要在選舉超時時間上使用很少的隨機化就可以大大避免選票被瓜分的情況。在沒有隨機化的情況下,在我們的測試裡,選舉過程往往都需要花費超過 10 秒鐘由於太多的選票瓜分的情況。僅僅增加 5 毫秒的隨機化時間,就大大的改善了選舉過程,現在平均的當機時間只有 287 毫秒。增加更多的隨機化時間可以大大改善最壞情況:通過增加 50 毫秒的隨機化時間,最壞的完成情況(1000 次嘗試)只要 513 毫秒。
  • 如果Candidate如果接收到其他Candidate的的選舉請求的話,如何認定究竟是繼續收集選票還是投票給其他candidate?如果大家一直互相謙讓或者一直互相競爭,豈不是最終誰也不能夠成為Leader了嗎?
    針對這個該問題,在請求選舉的RPC中Raft增加了任期號(term)的概念,在raft系統初始化時,所有node的term均為0,當某一個節點成為candidate時,該節點就會將term+1併發起選舉,follower僅會投票給RPC中的term大於等於自己的currenttTerm的RPC投票,這樣就避免了無限對等投票的可能性。Raft協議引入了任期號(term)的概念,任期號很簡單,所有node節點在初始化的時候,選票號都為1,當任何follower當選candidate的時候都會將選票號置加1,並附帶至發起選舉的RPC中,如若其他節點收到了選舉投票RPC,他會比對自身的選舉號是否比RPC中的選舉號小,如果小於RPC中的選舉號,則他會承認對方的權威
    raft-圖4.jpg-46kB

日誌複製:實現指令序列化,是實現強一致性的根本

只有leader節點可以和客戶端通訊,同時將log複製至所有follower,強制folloer與leader的log保持一致。
所謂日誌,其實就可以理解為增刪改查(CRUD)的命令(但其實raft並不關心這個日誌是做什麼的),這個命令在這裡稱為log,raft將log序列化,log依次複製並執行到每一個node,就能實現節點的強一致性,raft中解釋是“如果有任何的伺服器節點已經應用了一個確定的日誌條目到它的狀態機中,那麼其他伺服器節點不能在同一個日誌索引位置應用一個不同的指令”,也可以理解為raft日誌複製的安全性保證是確保所有序列按順序append,不能越過某一個log,這樣就保證了假如查詢發生在修改之後(但可能有不同的實現方式),那最終不論訪問的是哪一個節點,查詢必然發生在修改之後,這樣就可以確保拿到的資料是最新的了。
具體實現分為若干步:

  1. leader接收到客戶端發來的command
  2. leader將當前的index+1,賦給該log,並append到自己的Logs中
  3. leader廣播(RPC)該log給所有follower
  4. 當超過半數的follower迴應接收成功時,leader就將該log commit,並通知所有follower commit
    raft-圖6.jpg-67.8kB

安全性:保證不同的狀態機以相同的順序執行相同的指令

上面提到了Raft演算法是如何進行leader選舉和如何進行日誌複製的,至此其實已經可以實現分散式一致性了,但是如果想保證日誌提交精準無誤則需要更多地安全性保障措施

1.選舉限制

假如某個candidate在選舉成為leader時沒有包含所有的已提交日誌,這時就會出現日誌順序不一致的情況,在其他一致性演算法中會在選舉完成後進行補漏,但這大大增加了複雜性。而Raft則採用了一種簡單的方式避免了這種情況的發生

  1. Raft在RequestVote RPC(候選人請求成為Leader的RPC請求) 中增加了自己的日誌資訊
  2. 當followers收到RPC請求時則會把candidate的日誌資訊與自己的日誌資訊進行比較
  3. 假如follower的日誌資訊相較於candidate要更新,則拒絕這個選票,反之則同意該candidate成為leader

經過這一系列的步驟,則保證了僅允許包含了所有已提交日誌的candidate贏得選舉成為候選人,從而也就避免了leader缺少已提交日誌的情況了

2.延遲提交

上面提到過,我們進行日誌提交需要三個階段:

  1. leader將log複製到大多數followers
  2. follower將日誌複製,並告訴leader自己已經複製成功
  3. leader收到了大多數followers的複製成功響應,並提交日誌

但是這裡有一個問題,假如leader已經將日誌複製到了大多數followers,但卻在提交之前崩潰了,雖然raft演算法中規定後續的leader應該繼續完成之前的複製任務,但在下圖的情況下依舊會出現已經複製到大多數節點的log依舊被覆蓋掉了。

image.png-182.7kB

在 (a) 中,S1 是領導者,部分的複製了索引位置 2 的日誌條目。
在 (b) 中,S1 崩潰了,然後 S5 在任期 3 裡通過 S3、S4 和自己的選票贏得選舉,然後從客戶端接收了一條不一樣的日誌條目放在了索引 2 處。
然後到 (c),S5 又崩潰了;S1 重新啟動,選舉成功,開始複製日誌。在這時,來自任期 2 的那條日誌已經被複制到了叢集中的大多數機器上,但是還沒有被提交。
如果 S1 在 (d) 中又崩潰了,S5 可以重新被選舉成功(通過來自 S2,S3 和 S4 的選票),然後覆蓋了他們在索引 2 處的日誌

Raft為了避免這種情況發生,而規定了一個原則,Raft 永遠不會通過計算副本數目的方式去提交一個之前任期內的日誌條目,也就是說假如這個log的任期已經過了,就算是已經複製到了大多數節點,Raft也不會去提交它,那麼在這種情況下如何對之前任期的log進行提交呢,這時引入了Raft的Log Matching(日誌匹配原則),該原則的描述是“如果兩個日誌在相同的索引位置上的日誌條目的任期號相同, 那麼我們就認為日誌從頭到這個索引位置之間的條目完全相同”,這個機制的原理是leader在進行日誌複製時會檢查上一條日誌是否一致,如果不一致則會將上一條複製給follower,在複製上一條的過程中依舊會進行檢查,這樣一個遞迴的過程保證了Raft的Log Matching原則。

相關文章