通俗易懂關於Paxos的直觀解釋

一、Paxos是什麼

在分佈式系統中保證多副本數據強一致性算法。

沒有paxos的一堆機器, 叫做分佈式

有paxos協同的一堆機器, 叫分佈式系統

這個世界上只有一種一致性算法,那就是Paxos … - Google Chubby的作者Mike Burrows

其他一致性算法都可以看做Paxos在實現中的變體和擴展,比如raft。

二、先從複製算法說起

防止數據丟失,所以需要數據進行復製備份

2.1 主從異步複製





 

主節點接到寫請求,主節點寫本磁盤,主節點應答OK,主節點複製數據到從節點

如果數據在數據複製到從節點之前損壞,數據丟失。

2.2 主從同步複製





 

主節點接到寫請求,主節點複製日誌到所有從節點,從節點可能會阻塞,客戶端一直等待應答,直到所有從節點返回

一個節點失聯導致整個系統不可用,整個可用性的可用性比較低

2.3 主從半同步複製





 

主接到寫請求,主複製日誌到從庫,從庫可能阻塞,如果1~N個從庫返回OK,客戶端返回OK

可靠性和可用性得到了保障,但是可能任何從庫都沒有完整數據

2.4 多數派寫讀

往一個主接節點寫入貌似都會出現問題,那我們嘗試一下往多個節點寫入,捨棄主節點。



 

客戶端寫入 W >= N / 2 + 1 個節點, 讀需要 W + R > N, R >= N / 2 + 1,可以容忍 (N - 1)/ 2 個節點損壞

最後一次寫入覆蓋先前寫入,需要一個全局有序時間戳。

多數派寫可能會出現什麼問題?怎麼解決這些問題呢?

三、從多數派到Paxos的推導

假想一個存儲系統,滿足以下條件:

1. 有三個存儲節點 2. 使用多數派寫策略 3. 假定只存儲一個變量i 4. 變量i的每次更新對應多個版本,i1,i2, i3..... 5. 該存儲系統支持三個命令: 1. get 命令,讀取最新的變量i,對應多數派讀 2. set <n> 命令,設置下版本的變量i的值<n>,直接對應的多數派寫 3. inc <n> 命令, 對變量i增加<n>,也生成一個新版本,簡單的事務型操作

3.1 inc的實現描述





 

1. 從多數中讀取變量i,i當前版本1

2. 進行計算,i2 = i1 + n,i變更,版本+1

3. 多數派寫i2,寫入i,當前版本2

4. 獲取i,最新版本是2

這種實現方式存在一下問題:

如果2個併發的客戶端同時進行inc操作,必然會產生Y客戶端覆蓋X客戶端的問題,從而產生數據更新丟失





 

假設X,Y兩個客戶端,X客戶端執行命令inc 1,Y客戶端執行inc 2,我們期望最終變量i會加3

但是實際上會出現併發衝突

1. X客戶端讀取到變量i版本1的值是2

2. 同時客戶端Y讀取到變量i版本1的值也是2

3. X客戶端執行i1 + 1 = 3,Y客戶端執行i1 + 2 = 4

4. X執行多數派寫,變量i版本2的值是2,進行寫入(假定X客戶端先執行)

5. Y執行多數派寫,變量i版本2的值是4,進行寫入(如果Y成功,會把X寫入的值覆蓋掉)

所以Y寫入操作必須失敗,不能讓X寫入的值丟失但是該怎麼去做呢?

3.2 解決多數派寫入衝突

我們發現,客戶端X,Y寫入的都是變量i的版本2,那我們是不是可以增加一個約束:

整個系統對變量i的某個版本,只能有一次寫入成功。

也就是說,一個值(一個變量的一個版本)被確定(客戶端接到OK後)就不允許被修改了。

怎麼確定一個值被寫入了呢?在X或者Y寫之前先做一次多數派讀,以便確認是否有其他客戶端進程在寫了,如果有,則放棄。



 

客戶端X在執行多數派寫之前,先執行一個多數派讀,在要寫入的節點上標識一下客戶端X準備進行寫入,這樣其他客戶端執行的時候看到有X進行寫入就要放棄。

但是忽略了一個問題,就是客戶端Y寫之前也會執行多數派讀,那麼就會演變成X,Y都執行多數派讀的時候當時沒有客戶端在寫,然後在相應節點打上自己要寫的標識,這樣也會出現數據覆蓋。





 

3.3 逐步發現的真相

既然讓客戶端自己標識會出現數據丟失問題,那我們可以讓節點來記住最後一個做過寫前讀取的進程,並且只允許最後一個完成寫前讀取的進程進行後續寫入,拒絕之前做過寫前讀取進行的寫入權限。





 

X,Y同時進行寫前讀取的時候,節點記錄最後執行一個執行的客戶端,然後只允許最後一個客戶端進行寫入操作。

使用這個策略變量i的每個版本可以被安全的存儲。

然後Leslie Lamport寫了一篇論文,並且獲得了圖靈獎。

四、重新描述一下Paxos的過程(Classic Paxos

使用2輪RPC來確定一個值,一個值確定之後不能被修改,算法中角色描述:

•Proposer 客戶端

•Acceptor 可以理解爲存儲節點

•Quorum 在99%的場景裏都是指多數派, 也就是半數以上的Acceptor

•Round 用來標識一次paxos算法實例, 每個round是2次多數派讀寫: 算法描述裏分別用phase-1和phase-2標識. 同時爲了簡單和明確, 算法中也規定了每個Proposer都必須生成全局單調遞增的round, 這樣round既能用來區分先後也能用來區分不同的Proposer(客戶端).

4.1 Proposer請求使用的請求

// 階段一 請求
class RequestPhase1 {
    int rnd; // 遞增的全局唯一的編號,可以區分Proposer
}
// 階段二 請求
class RequestPhase2 {
     int rnd;    // 遞增的全局唯一的編號,可以區分Proposer
     Object v;   // 要寫入的值
 }

4.2 Acceptor 存儲使用的應答

// 階段一 應答 
class ResponsePhase1 { 
    int last_rnd; // Acceptor 記住的最後一次寫前讀取的Proposer,以此來決定那個Proposer可以寫入
    Object v;     // 最後被寫入的值
    int vrnd;     // 跟v是一對,記錄了在哪個rnd中v被寫入了
}
// 階段二 應答
class ResponsePhase2 { 
    boolean ok;
}

4.3 步驟描述

階段一





 

當Acceptor收到phase-1的請求時:

● 如果請求中rnd比Acceptor的last_rnd小,則拒絕請求

● 將請求中的rnd保存到本地的last_rnd. 從此這個Acceptor只接受帶有這個last_rnd的phase-2請求。

● 返回應答,帶上自己之前的last_rnd和之前已接受的v.

當Proposer收到Acceptor發回的應答:

● 如果應答中的last_rnd大於發出的rnd: 退出.

● 從所有應答中選擇vrnd最大的v: 不能改變(可能)已經確定的值,需要把其他節點進行一次補償

● 如果所有應答的v都是空或者所有節點返回v和vrnd是一致的,可以選擇自己要寫入v.

● 如果應答不夠多數派,退出

階段二:





 

Proposer:

發送phase-2,帶上rnd和上一步決定的v

Acceptor:

● 拒絕rnd不等於Acceptor的last_rnd的請求

● 將phase-2請求中的v寫入本地,記此v爲‘已接受的值’

● last_rnd==rnd 保證沒有其他Proposer在此過程中寫入 過其他值

4.4 例子

無衝突:





 

有衝突的情況,不會改變寫入的值





 

客戶端X寫入失敗,因此重新進行2輪PRC進行重新寫入,相當於做了一次補償,從而使系統最終一致



 

五、問題及改進

活鎖(Livelock): 多個Proposer併發對1個值運行paxos的時候,可能會互 相覆蓋對方的rnd,然後提升自己的rnd再次嘗試,然後再次產生衝突,一直無法完成

然後後續演化各種優化:

multi-paxos:用一次rpc爲多個paxos實例運行phase-1

fast-paxos增加quorum的數量來達到一次rpc就能達成一致的目的. 如果fast-paxos沒能在一次rpc達成一致, 則要退化到classic paxos.

raft: leader, term,index等等..

六、參考

1. 文章主要來自博客:https://blog.openacid.com/algo/paxos/

2. 一個基於Paxos的KV存儲的實現:https://github.com/openacid/paxoskv

3. Paxos論文:https://lamport.azurewebsites.net/pubs/paxos-simple.pdf

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章