一、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