一致性協議淺析:從邏輯時鐘到Raft

前言

春節在家閒着沒事看了幾篇論文,把一致性協議的幾篇論文都過了一遍。在看這些論文以前,我一直有一些疑惑,好比一樣是有Leader和兩階段提交,Zookeeper的ZAB協議和Raft有什麼不一樣,Paxos協議到底要怎樣才能用在實際工程中,這些問題我都在這些論文中找到了答案。接下來,我將嘗試以本身的語言給你們講講這些協議,使你們可以理解這些算法。同時,我本身也有些疑問,我會在個人闡述中提出,也歡迎你們一塊兒討論。水平有限,文中不免會有一些紕漏門也歡迎你們指出。html

邏輯時鐘

邏輯時鐘其實算不上是一個一致性協議,它是Lamport大神在1987年就提出來的一個想法,用來解決分佈式系統中,不一樣的機器時鐘不一致可能帶來的問題。在單機系統中,咱們用機器的時間來標識事件,就能夠很是清晰地知道兩個不一樣事件的發生次序。可是在分佈式系統中,因爲每臺機器的時間可能存在偏差,沒法經過物理時鐘來準確分辨兩個事件發生的前後順序。但實際上,在分佈式系統中,只有兩個發生關聯的事件,咱們纔會去關心二者的先來後到關係。好比說兩個事務,一個修改了rowa,一個修改了rowb,他們兩個誰先發生,誰後發生,其實咱們並不關心。那所謂邏輯時鐘,就是用來定義兩個關聯事件的發生次序,即‘happens before’。而對於不關聯的事件,邏輯時鐘並不能決定其前後,因此說這種‘happens before’的關係,是一種偏序關係。
算法

圖和例子來自於這篇博客數據庫

此圖中,箭頭表示進程間通信,ABC分別表明分佈式系統中的三個進程。服務器

邏輯時鐘的算法其實很簡單:每一個事件對應一個Lamport時間戳,初始值爲0網絡

若是事件在節點內發生,時間戳加1併發

若是事件屬於發送事件,時間戳加1並在消息中帶上該時間戳app

若是事件屬於接收事件,時間戳 = Max(本地時間戳,消息中的時間戳) + 1分佈式

這樣,全部關聯的發送接收事件,咱們都能保證發送事件的時間戳小於接收事件。若是兩個事件之間沒有關聯,好比說A3和B5,他們的邏輯時間同樣。正是因爲他們沒有關係,咱們能夠隨意約定他們之間的發生順序。好比說咱們規定,當Lamport時間戳同樣時,A進程的事件發生早於B進程早於C進程,這樣咱們能夠得出A3 ‘happens before’ B5。而實際在物理世界中,明顯B5是要早於A3發生的,但這都沒有關係。性能

邏輯時鐘貌似目前並無被普遍的應用,除了DynamoDB使用了vector clock來解決多版本的前後問題(若是有其餘實際應用的話請指出,多是我孤陋寡聞了),Google的Spanner 也是採用物理的原子時鐘來解決時鐘問題。可是從Larmport大師的邏輯時鐘算法上,已經能夠看到一些一致性協議的影子。學習

Replicated State Machine

說到一致性協議,咱們一般就會講到複製狀態機。由於一般咱們會用複製狀態機加上一致性協議算法來解決分佈式系統中的高可用和容錯。許多分佈式系統,都是採用複製狀態機來進行副本之間的數據同步,好比HDFS,Chubby和Zookeeper。

所謂複製狀態機,就是在分佈式系統的每個實例副本中,都維持一個持久化的日誌,而後用必定的一致性協議算法,保證每一個實例的這個log都徹底保持一致,這樣,實例內部的狀態機按照日誌的順序回放日誌中的每一條命令,這樣客戶端來讀時,在每一個副本上都能讀到同樣的數據。複製狀態機的核心就是圖中 的Consensus模塊,即今天咱們要討論的Paxos,ZAB,Raft等一致性協議算法。

Paxos

Paxos是Lamport大神在90年代提出的一致性協議算法,你們一直都以爲難懂,因此Lamport在2001又發表了一篇新的論文《Paxos made simple》,在文中他本身說Paxos是世界上最簡單的一致性算法,很是容易懂……可是業界仍是一致認爲Paxos比較難以理解。在我看過Lamport大神的論文後,我以爲,除去複雜的正確性論證過程,Paxos協議自己仍是比較好理解的。可是,Paxos協議仍是過於理論,離具體的工程實踐還有太遠的距離。我一開始看Paxos協議的時候也是一頭霧水,看來看去發現Paxos協議只是爲了單次事件答成一致,並且答成一致後的值沒法再被修改,怎麼用Paxos去實現複製狀態機呢?另外,Paxos協議答成一致的值只有Propose和部分follower知道,這協議到底怎麼用……可是,若是你只是把Paxos協議當作一個理論去看,而不是考慮實際工程上會遇到什麼問題的話,會容易理解的多。Lamport的論文中對StateMachine的應用只有一個大概的想法,並無具體的實現邏輯,想要直接把Paxos放到複製狀態機裏使用是不可能的,得在Paxos上補充不少的東西。這些是爲何Paxos有這麼多的變種。

Basic-Paxos

Basic-Paxos即Lamport最初提出的Paxos算法,其實很簡單,用三言兩語就能夠講完,下面我嘗試着用我本身的語言描述下Paxos協議,而後會舉出一個例子。要理解Paxos,只要記住一點就行了,Paxos只能爲一個值造成共識,一旦Propose被肯定,以後值永遠不會變,也就是說整個Paxos Group只會接受一個提案(或者說接受多個提案,但這些提案的值都同樣)。至於怎麼才能接受多個值來造成複製狀態機,你們能夠看下一節Multi-Paxos.

Paxos協議中是沒有Leader這個概念的,除去Learner(只是學習Propose的結果,咱們能夠不去討論這個角色),只有Proposer和Acceptor。Paxos而且容許多個Proposer同時提案。Proposer要提出一個值讓全部Acceptor答成一個共識。首先是Prepare階段,Proposer會給出一個ProposeID n(注意,此階段Proposer不會把值傳給Acceptor)給每一個Acceptor,若是某個Acceptor發現本身歷來沒有接收過大於等於n的Proposer,則會回覆Proposer,同時承諾再也不接收ProposeID小於等於n的提議的Prepare。若是這個Acceptor已經承諾過比n更大的propose,則不會回覆Proposer。若是Acceptor以前已經Accept了(完成了第二個階段)一個小於n的Propose,則會把這個Propose的值返回給Propose,不然會返回一個null值。當Proposer收到大於半數的Acceptor的回覆後,就能夠開始第二階段accept階段。可是這個階段Propose可以提出的值是受限的,只有它收到的回覆中不含有以前Propose的值,他才能自由提出一個新的value,不然只能是用回覆中Propose最大的值作爲提議的值。Proposer用這個值和ProposeID n對每一個Acceptor發起Accept請求。也就是說就算Proposer以前已經獲得過acceptor的承諾,可是在accept發起以前,Acceptor可能給了proposeID更高的Propose承諾,致使accept失敗。也就是說因爲有多個Proposer的存在,雖然第一階段成功,第二階段仍然可能會被拒絕掉。

下面我舉一個例子,這個例子來源於這篇博客

假設有Server1,Server2, Server3三個服務器,他們都想經過Paxos協議,讓全部人答成一致他們是leader,這些Server都是Proposer角色,他們的提案的值就是他們本身server的名字。他們要獲取Acceptor1~3這三個成員贊成。首先Server2發起一個提案【1】,也就是說ProposeID爲1,接下來Server1發起來一個提案【2】,Server3發起一個提案【3】.

首先是Prepare階段:

假設這時Server1發送的消息先到達acceptor1和acceptor2,它們都沒有接收過請求,因此接收該請求並返回【2,null】給Server1,同時承諾再也不接受編號小於2的請求; 

緊接着,Server2的消息到達acceptor2和acceptor3,acceptor3沒有接受過請求,因此返回proposer2 【1,null】,並承諾再也不接受編號小於1的消息。而acceptor2已經接受Server1的請求並承諾再也不接收編號小於2的請求,因此acceptor2拒絕Server2的請求; 

最後,Server3的消息到達acceptor2和acceptor3,它們都接受過提議,但編號3的消息大於acceptor2已接受的2和acceptor3已接受的1,因此他們都接受該提議,並返回Server3 【3,null】; 

此時,Server2沒有收到過半的回覆,因此從新取得編號4,併發送給acceptor2和acceptor3,此時編號4大於它們已接受的提案編號3,因此接受該提案,並返回Server2 【4,null】。

接下來進入Accept階段,

Server3收到半數以上(2個)的回覆,而且返回的value爲null,因此,Server3提交了【3,server3】的提案。 

Server1在Prepare階段也收到過半回覆,返回的value爲null,因此Server1提交了【2,server1】的提案。 

Server2也收到過半回覆,返回的value爲null,因此Server2提交了【4,server2】的提案。 

Acceptor1和acceptor2接收到Server1的提案【2,server1】,acceptor1經過該請求,acceptor2承諾再也不接受編號小於4的提案,因此拒絕; 

Acceptor2和acceptor3接收到Server2的提案【4,server2】,都經過該提案; 

Acceptor2和acceptor3接收到Server3的提案【3,server3】,它們都承諾再也不接受編號小於4的提案,因此都拒絕。 

此時,過半的acceptor(acceptor2和acceptor3)都接受了提案【4,server2】,learner感知到提案的經過,learner開始學習提案,因此server2成爲最終的leader。

Multi-Paxos

剛纔我講了,Paxos還過於理論,沒法直接用到複製狀態機中,總的來講,有如下幾個緣由

  • Paxos只能肯定一個值,沒法用作Log的連續複製
  • 因爲有多個Proposer,可能會出現活鎖,如我在上面舉的例子中,Server2的一共提了兩次Propose才最終讓提案經過,極端狀況下,次數可能會更多
  • 提案的最終結果可能只有部分Acceptor知曉,無法達到複製狀態機每一個instance都必須有徹底一致log的需求。

那麼其實Multi-Paxos,其實就是爲了解決上述三個問題,使Paxos協議可以實際使用在狀態機中。解決第一個問題其實很簡單。爲Log Entry每一個index的值都是用一個獨立的Paxos instance。解決第二個問題也很簡答,讓一個Paxos group中不要有多個Proposer,在寫入時先用Paxos協議選出一個leader(如我上面的例子),而後以後只由這個leader作寫入,就能夠避免活鎖問題。而且,有了單一的leader以後,咱們還能夠省略掉大部分的prepare過程。只須要在leader當選後作一次prepare,全部Acceptor都沒有接受過其餘Leader的prepare請求,那每次寫入,均可以直接進行Accept,除非有Acceptor拒絕,這說明有新的leader在寫入。爲了解決第三個問題,Multi-Paxos給每一個Server引入了一個firstUnchosenIndex,讓leader可以向向每一個Acceptor同步被選中的值。解決這些問題以後Paxos就能夠用於實際工程了。

Paxos到目前已經有了不少的補充和變種,實際上,以後我要討論的ZAB也好,Raft也好,均可以看作是對Paxos的修改和變種,另外還有一句流傳甚廣的話,「世上只有一種一致性算法,那就是Paxos」。

ZAB

ZAB即Zookeeper Atomic BoardCast,是Zookeeper中使用的一致性協議。ZAB是Zookeeper的專用協議,與Zookeeper強綁定,並無抽離成獨立的庫,所以它的應用也不是很普遍,僅限於Zookeeper。但ZAB協議的論文中對ZAB協議進行了詳細的證實,證實ZAB協議是可以嚴格知足一致性要求的。

ZAB隨着Zookeeper誕生於2007年,此時Raft協議尚未發明,根據ZAB的論文,之因此Zookeeper沒有直接使用Paxos而是本身造輪子,是由於他們認爲Paxos並不能知足他們的要求。好比Paxos容許多個proposer,可能會形成客戶端提交的多個命令無法按照FIFO次序執行。同時在恢復過程當中,有一些follower的數據不全。這些斷論都是基於最原始的Paxos協議的,實際上後來一些Paxos的變種,好比Multi-Paxos已經解決了這些問題。固然咱們只能站在歷史的角度去看待這個問題,因爲當時的Paxos並不能很好的解決這些問題,所以Zookeeper的開發者創造了一個新的一致性協議ZAB。

ZAB其實和後來的Raft很是像,有選主過程,有恢復過程,寫入也是兩階段提交,先從leader發起一輪投票,得到超過半數贊成後,再發起一次commit。ZAB中每一個主的epoch number其實就至關於我接下來要講的Raft中的term。只不過ZAB中把這個epoch number和transition number組成了一個zxid存在了每一個entry中。

ZAB在作log複製時,兩階段提交時,一個階段是投票階段,只要收到過半數的贊成票就能夠,這個階段並不會真正把數據傳輸給follower,實際做用是保證當時有超過半數的機器是沒有掛掉,或者在同一個網絡分區裏的。第二個階段commit,纔會把數據傳輸給每一個follower,每一個follower(包括leader)再把數據追加到log裏,此次寫操做就算完成。若是第一個階段投票成功,第二個階段有follower掛掉,也沒有關係,重啓後leader也會保證follower數據和leader對其。若是commit階段leader掛掉,若是此次寫操做已經在至少一個follower上commit了,那這個follower必定會被選爲leader,由於他的zxid是最大的,那麼他選爲leader後,會讓全部follower都commit這條消息。若是leader掛時沒有follower commit這條消息,那麼這個寫入就當作沒寫完。

因爲只有在commit的時候才須要追加寫日誌,所以ZAB的log,只須要append-only的能力,就能夠了。

另外,ZAB支持在從replica裏作stale read,若是要作強一致的讀,能夠用sync read,原理也是先發起一次虛擬的寫操做,到不作任何寫入,等這個操做完成後,本地也commit了此次sync操做,再在本地replica上讀,可以保證讀到sync這個時間點前全部的正確數據,而Raft全部的讀和寫都是通過主節點的

Raft

Raft是斯坦福大學在2014年提出的一種新的一致性協議。做者表示之因此要設計一種全新的一致性協議,是由於Paxos實在太難理解,並且Paxos只是一個理論,離實際的工程實現還有很遠的路。所以做者狠狠地吐槽了Paxos一把:

  1. Paxos協議中,是不須要Leader的,每一個Proposer均可以提出一個propose。相比Raft這種一開始設計時就把選主和協議達成一致分開相比,Paxos等因而把選主和propose階段雜糅在了一塊兒,形成Paxos比較難以理解。
  2. 最原始的Paxos協議只是對單一的一次事件答成一致,一旦這個值被肯定,就沒法被更改,而在咱們的現實生活中,包括咱們數據庫的一致性,都須要連續地對log entry的值答成一致,因此單單理解Paxos協議自己是不夠的,咱們還須要對Paxos協議進行改進和補充,才能真正把Paxos協議應用到工程中。而對Paxos協議的補充自己又很是複雜,並且雖然Paxos協議被Lamport證實過,而添加了這些補充後,這些基於Paxos的改進算法,如Multi-Paxos,又是未經證實的。
  3. 第三個槽點是Paxos協議只提供了一個很是粗略的描述,致使後續每個對Paxos的改進,以及使用Paxos的工程,如Google的Chubby,都是本身實現了一套工程來解決Paxos中的一些具體問題。而像Chubby的實現細節其實並無公開。也就是說要想在本身的工程中使用Paxos,基本上每一個人都須要本身定製和實現一套適合本身的Paxos協議。

所以,Raft的做者在設計Raft的時候,有一個很是明確的目標,就是讓這個協議可以更好的理解,在設計Raft的過程當中,若是遇到有多種方案能夠選擇的,就選擇更加容易理解的那個。做者舉了一個例子。在Raft的選主階段,原本能夠給每一個server附上一個id,你們都去投id最大的那個server作leader,會更快地達成一致(相似ZAB協議),但這個方案又增長了一個serverid的概念,同時在高id的server掛掉時,低id的server要想成爲主必須有一個等待時間,影響可用性。所以Raft的選主使用了一個很是簡單的方案:每一個server都隨機sleep一段時間,最先醒過來的server來發起一次投票,獲取了大多數投票便可爲主。在一般的網絡環境下,最先發起投票的server也會最先收到其餘server的同意票,所以基本上只須要一輪投票就能夠決出leader。整個選主過程很是簡單明瞭。

除了選主,整個Raft協議的設計都很是簡單。leader和follower之間的交互(若是不考慮snapshot和改變成員數量)一共只有2個RPC call。其中一個仍是選主時才須要的RequestVote。也就是說全部的數據交互,都只由AppendEntries 這一個RPC完成。

理解Raft算法,首先要理解Term這個概念。每一個leader都有本身的Term,並且這個term會帶到log的每一個entry中去,來表明這個entry是哪一個leader term時期寫入的。另外Term至關於一個lease。若是在規定的時間內leader沒有發送心跳(心跳也是AppendEntries這個RPC call),Follower就會認爲leader已經掛掉,會把本身收到過的最高的Term加上1作爲新的term去發起一輪選舉。若是參選人的term還沒本身的高的話,follower會投反對票,保證選出來的新leader的term是最高的。若是在time out週期內沒人得到足夠的選票(這是有可能的),則follower會在term上再加上1去作新的投票請求,直到選出leader爲止。最初的raft是用c語言實現的,這個timeout時間能夠設置的很是短,一般在幾十ms,所以在raft協議中,leader掛掉以後基本在幾十ms就可以被檢測發現,故障恢復時間能夠作到很是短。而像用Java實現的Raft庫,如Ratis,考慮到GC時間,我估計這個超時時間無法設置這麼短。

在Leader作寫入時也是一個兩階段提交的過程。首先leader會把在本身的log中找到第一個空位index寫入,並經過AppendEntries這個RPC把這個entry的值發給每一個follower,若是收到半數以上的follower(包括本身)回覆true,則再下一個AppendEntries中,leader會把committedIndex加1,表明寫入的這個entry已經被提交。如在下圖中,leader將x=4寫入index=8的這個entry中,並把他發送給了全部follower,在收到第一臺(本身),第三臺,第五臺(圖中沒有畫index=8的entry,但由於這臺服務器以前全部的entry都和leader保持了一致,所以它必定會投贊成),那麼leader就得到了多數票,再下一個rpc中,會將Committed index往前挪一位,表明index<=8的全部entry都已經提交。至於第二臺和第四臺服務器,log內容已經明顯落後,這要麼是由於前幾回rpc沒有成功。leader會無限重試直到這些follower和leader的日誌追平。另一個多是這兩臺服務器重啓過,處於恢復狀態。那麼這兩臺服務器在收到寫入index=8的RPC時,follower也會把上一個entry的term和index發給他們。也就是說prevLogIndex=7,prevLogTerm=3這個信息會發給第二臺服務器,那麼對於第二臺服務器,index=7的entry是空的,也就是log和leader不一致,他會返回一個false給leader,leader會不停地從後往前遍歷,直到找到一個entry與第二臺服務器一致的,從這個點開始從新把leader的log內容發送給該follower,便可完成恢復。raft協議保證了全部成員的replicated log中每一個index位置,若是他們的term一致,內容也必定一致。若是不一致,leader必定會把這個index的內容改寫成和leader一致。

其實通過剛纔個人一些描述,基本上就已經把Raft的選主,寫入流程和恢復基本上都講完了。從這裏,咱們能夠看出Raft一些很是有意思的地方。

第一個有意思的地方是Raft的log的entry是可能被修改的,好比一個follower接收了一個leader的prepare請求,把值寫入了一個index,而這個leader掛掉,新選出的leader可能會從新使用這個index,那麼這個follower的相應index的內容,會被改寫成新的內容。這樣就形成了兩個問題,首先第一個,raft的log沒法在append-only的文件或者文件系統上去實現,而像ZAB,Paxos協議,log只會追加,只要求文件系統有append的能力便可,不須要隨機訪問修改能力。

第二個有意思的地方是,爲了簡單,Raft中只維護了一個Committed index,也就是任何小於等於這個committedIndex的entry,都是被認爲是commit過的。這樣就會形成在寫入過程當中,在leader得到大多數選票以前掛掉(或者leader在寫完本身的log以後還沒來得及通知到任何follower就掛掉),重啓後若是這個server繼續被選爲leader,這個值仍然會被commit永久生效。由於leader的log中有這個值,leader必定會保證全部的follower的log都和本身保持一致。然後續的寫入在增加committedIndex後,這個值也默認被commit了。

舉例來講,如今有5臺服務器,其中S2爲leader,可是當他在爲index=1的entry執行寫入時,先寫到了本身的log中,還沒來得及通知其餘server append entry就宕機了。 

當S2重啓後,任然有可能被從新當選leader,當S2從新當選leader後,仍然會把index=1的這個entry複製給每臺服務器(可是不會往前移動Committed index)

此時S2又發生一次寫入,此次寫入完成後,會把Committed index移動到2的位置,所以index=1的entry也被認爲已經commit了。

這個行爲有點奇怪,由於這樣等於raft會讓一個沒有得到大多數人贊成的值最終commit。這個行爲取決於leader,若是上面的例子中S2重啓後沒有被選爲leader,index=1的entry內容會被新leader的內容覆蓋,從而不會提交未通過表決的內容。

雖說這個行爲是有點奇怪,可是不會形成任何問題,由於leader和follower仍是會保持一致,並且在寫入過程當中leader掛掉,對客戶端來講是原本就是一個未決語義,raft論文中也指出,若是用戶想要exactly once的語義,能夠在寫入的時候加入一個相似uuid的東西,在寫入以前leader查下這個uuid是否已經寫入。那麼在必定程度上,能夠保證exactly once的語義。

Raft的論文中也比較了ZAB的算法,文中說ZAB協議的一個缺點是在恢復階段須要leader和follower來回交換數據,這裏我沒太明白,據我理解,ZAB在從新選主的過程當中,會選擇Zxid最大的那個從成爲主,而其餘follower會從leader這裏補全數據,並不會出現leader從follower節點補數據這一說。

後話

目前,通過改進的Paxos協議已經用在了許多分佈式產品中,好比說Chubby,PaxosStore,阿里雲的X-DB,以及螞蟻的OceanBase,都選用了Paxos協議,但他們都多多少少作了一些補充和改進。而像Raft協議,廣泛認爲Raft協議只能順序commit entry,性能沒有Paxos好,可是TiKV中使用了Raft,其公開的文章宣傳對Raft作了很是多的優化,使Raft的性能變的很是可觀。阿里的另一個數據庫PolarDB,也是使用了改進版的Parallel-Raft,使Raft實現了並行提交的能力。相信將來會有更多的基於Paxos/Raft的產品會面世,同時也對Raft/Paxos也會有更多的改進。

參考文獻

  1. 《Time, clocks, and the ordering of events in a distributed system》
  2. 《Implementing fault-tolerant services using the state machine approach- A tutorial》
  3. 《Paxos Made Simple》
  4. 《Paxos made live- An engineering perspective》
  5. 《Multi-Paxos》(Standford大學的一個ppt)
  6. 《Zab- High-performance broadcast for primary-backup systems》
  7. 《In search of an understandable consensus algorithm》(raft)



本文做者:正研

閱讀原文

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索