以前介紹了viewservice,可是不能解決單點的問題。換句話說,若是viewserver crash, 那麼整個系統就癱瘓了。git
爲了解決這個問題,一個能夠想到的方法就是將單個的viewserver變爲多個,而多個server保持同步。而多個server之間的同步問題,涉及到分佈式系統中的一致性算法。本文介紹Paxos的概念及實現。github
關於Paxos算法,網上相關的文章不少,不過仍是建議看lamport的原文。原文經過不斷增強約束,一步步的獲得最後的結論,即:算法
For any v and n, if a proposal with value v and number n is issued,then there is a set S consisting of a majority of acceptors such that either (a) no acceptor in S has accepted any proposal numbered less than n, or (b) v is the value of the highest-numbered proposal among all proposals numbered less than n accepted by the acceptors in S .數據庫
咱們以一個分佈式的KV數據庫爲例,分析Paxos的應用場景。首先,假設數據庫對外提供3種操做架構
Put負載均衡
PutAppendless
Get分佈式
在這樣的一個架構下,經過多臺server組成集羣來避免單點問題。可是,如圖所示的3臺server必須保持同步。也就是說,若是Client向集羣發送請求 Put("a", 1)
併成功,那麼整個集羣中任意一臺server必須都含有數據("a", 1).函數
這裏的Put("a", 1)
就是lamport論文中proposer提出的value
,這個稍後解釋。對於數據庫集羣而言,全部的操做都是串行的,就像如今的數據庫都提供日誌功能同樣,每一個操做都會記錄在日誌當中,並而是有前後順序的。若是咱們的集羣前後收到3個操做請求,分別爲spa
Put("a", 1)
Put("b", 2)
Put("c", 3)
那麼在對於數據庫集羣而言,應該有這麼一個表,相似爲
+---+-------------+ |1 |Put("a", 1) | +---+-------------+ |2 |Put("b", 2) | +---+-------------+ |3 |Put("c", 3) | +---+-------------+
咱們暫時稱它爲狀態表
。
這裏的標號1,2,3就對應論文中的Sequence。
下面咱們就假設數據庫集羣是空的,即沒有任何數據。客戶端發送了上面3個請求,固然,這中間可能存在多個客戶端同時發送請求的狀況。咱們看看整個流程是怎麼樣的。
首先,假設Client1向集羣發送了Put("a", 1)
, 這個請求雖然是發給集羣的,但實際上最後確定會落實到某一個具體的server,這個過程可能經過負載均衡等方法獲得,並非咱們關心的。咱們就假設這個請求發到了server1。這時server1是空的,因此他查詢本身的狀態表
,發現Seq1沒有對應的操做,按理說他能夠將今天的第一個操做肯定爲Put("a", 1)
,但因爲它是集羣中的一份子,必需要協商一下。這個協商的過程,就是Paxos。他須要協商的問題是:
第1個操做是否能夠爲
Put("a", 1)
這裏的協商過程咱們能夠理解爲一個函數, 函數的參數爲操做的序號和操做,返回值爲一個操做。
Op doPaxos(int seq, Op v){...}
這裏因爲只有一個Client提出的操做的請求,不存在競爭,因此協商很順利,當server1調用這個協商函數的時候,返回的值就是它傳入的值,此時咱們就能夠認爲集羣中的3臺server達成了一個共識,即集羣的第一個操做爲Put("a", 1)
。因而server1將本身的狀態表改
+---+-------------+ |1 |Put("a", 1) | +---+-------------+
須要注意的是,在協商的過程當中,server2與server3也將本身的狀態表改成上面的樣子。這個狀態表同步的過程就是論文中的Learning
階段。
可見,Paxos做爲一致性算法,在這裏例子當中保證的一致性其實就是狀態表的一致性。
有沒有協商不順利的狀況呢? 固然, 有!
這裏咱們假設有出現了兩個客戶端,Client2和Client3。接着用上面的例子,Client2向集羣發送了請求Put("b", 2)
,假設這個請求最終到了server1上。同時, Client3向集羣發送了請求Put("c", 3)
, 假設這個求情發到了server2上。
此時server1和server2的狀態表是同樣的,這一點是由Paxos保證的。他們的不一樣之處在於:
server1的數據和server2不一樣,即server1裏面包含一條數據,就是剛剛Client1請求的Put("a", 1)
, 可是server2是空的。
server1的sequence爲2, 可是server2的sequence仍然爲1。緣由是server1已經作了一次Put操做,可是server2什麼都沒有作。
因爲server1和server2的不一樣,所以他們處理各自請求的方式也不一樣。
先來看server2。server2從1開始遍歷本身的狀態表,發現1號操做非空,說明這是他遺漏的操做,因而server2也執行了操做Put("a", 1)
, 此時server2與server1的狀態徹底同樣了。在此以後,server2會向上面server1同樣,調用doPaxos函數進行協商,即詢問集羣中的全部server我們的第2個操做可否爲Put("c", 3)
doPaxos(2, v);
其中v爲 Put("c", 3)
。
但就在此時,server1也調用了doPaxos進行協商,詢問集羣中的全部server第2個操做可否爲Put("b", 2)
。
這裏集羣中兩個server提出了不一樣的議題(lamport的論文中也使用了這種說法)
,Paxos保證了最終會選出一個你們都贊成的議題。咱們這裏假設server1的議題最終被經過了。因而,集羣中全部server的狀態表都更新爲
+---+-------------+ |1 |Put("a", 1) | +---+-------------+ |2 |Put("b", 2) | +---+-------------+
而且server1向本身的database中插入("b", 2),並將本身的sequence更新爲3.
而此時server2發現本身的狀態表中sequence2有了對應的操做,但很惋惜不是本身提出的那個操做。這條信心代表集羣其實已經作了2個操做了,本身想要提出的這個操做Put("c", 3)
已經不能做爲集羣的第2個操做了。因而server2對本身的database進行Put("b", 2)
操做, 而後將本身的sequence更新爲3。再次調用doPaxos進行協商,試圖將本身的操做做爲集羣的第3個操做,因而doPaxos的參數爲
doPaxos(3, {Put("c", 3)})
此時因爲只有一個提議,因此協商很順利。此時集羣中全部server的狀態表都爲
+---+-------------+ |1 |Put("a", 1) | +---+-------------+ |2 |Put("b", 2) | +---+-------------+ |3 |Put("c", 3) | +---+-------------+
其中,server1包含2條數據,server2包含3條數據,server3爲空。
咱們假設此時Client1向集羣發送了Get("a")
請求,並最終發到了server3。因爲server3的sequence爲, 因此他會從1開始,將本身狀態表中的1,2,3號操做就執行一遍,直到到了sequence4時,發現狀態表爲空,因而進行查詢操做,將結果返回Client。
因爲Get
是隻讀操做,所以不必協商,也不必將其寫入狀態表。
示例代碼:https://github.com/qc1iu/haze-kv/tree/master/src/kvpaxos
原文來自 qc1iu的簡書