這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
在理論電腦科學中,CAP定理(CAP theorem),又被稱作布魯爾定理(Brewer’s theorem),它指出對於一個分散式運算系統來說,不可能同時滿足以下三點:
- 一致性(Consistence),等同於所有節點訪問同一份最新的資料副本;
- 可用性(Availability),每次請求都能擷取到非錯的響應——但是不保證擷取的資料為最新資料;
- 分區容錯性(Network partitioning),以實際效果而言,分區相當於對通訊的時限要求。系統如果不能在時限內達成資料一致性,就意味著發生了分區的情況,必須就當前操作在 C 和 A 之間做出選擇。
上車
今天說說一致性,分布式系統中的節點通訊存在兩種模型:共用記憶體(Shared memory)和訊息傳遞(Messages passing)。
基於訊息傳遞通訊模型的分布式系統,不可避免的會發生以下錯誤:進程可能會慢、被殺死或者重啟,訊息可能會延遲、丟失、重複。在基礎 Paxos 情境中,先不考慮可能出現訊息篡改即拜占庭錯誤的情況。Paxos 演算法解決的問題是在一個可能發生上述異常的分布式系統中如何就某個值達成一致,保證不論發生以上任何異常,都不會破壞決議的一致性。一個典型的情境是,在一個分散式資料庫系統中,如果各節點的初始狀態一致,每個節點都執行相同的操作序列,那麼他們最後能得到一個一致的狀態。為保證每個節點執行相同的命令序列,需要在每一條指令上執行一個“一致性演算法”以保證每個節點看到的指令一致。一個通用的一致性演算法可以應用在許多情境中,是分散式運算中的重要問題,因此從 20 世紀 80 年代起對於一致性演算法的研究就沒有停止過。
發車 (Paxos 演算法)
Paxos 演算法就是通過兩個階段確定一個決議:
- Phase1:確定誰的編號最高,只有編號最高者才有權利提交 Proposal(提議:給定的具體值);
- Phase2:編號最高者提交 Proposal,如果沒有其他節點提出更高編號的 Proposal,則該提案會被順利通過,否則整個過程就會重來。
結論就是這個結論,至於整個過程的推導,就不在這裡展開細說了。但是有一點需要注意的是,在過程第一階段,可能會出現活鎖。你編號高,我比你更高,反覆如此,演算法永遠無法結束。可使用一個“Leader”來解決問題,這個 Leader 並非我們刻意去選出來一個,而是自然形成出來的。同樣再次也不展開討論了,本篇主要是以 Code 為主的哈!
Phase1
func (px *Paxos)Prepare(args *PrepareArgs, reply *PrepareReply) error {px.mu.Lock()defer px.mu.Unlock()round, exist := px.rounds[args.Seq]if !exist {//new seq of commit,so need newpx.rounds[args.Seq] = px.newInstance()round, _ = px.rounds[args.Seq]reply.Err = OK}else {if args.PNum > round.proposeNumber {reply.Err = OK}else {reply.Err = Reject}}if reply.Err == OK {reply.AcceptPnum = round.acceptorNumberreply.AcceptValue = round.acceptValuepx.rounds[args.Seq].proposeNumber = args.PNum}else {//reject}return nil}
在 Prepare 階段,主要是通過 RPC 調用,詢問每一台機器,當前的這個提議能不能通過,判斷的條件就是,當前提交的編號大於之前的其他機器 Prepare 的編號,代碼 if args.PNum > round.proposeNumber
的判斷。還有一個就是,如果之前一台機器都沒有通過,即使當前是第一個提交 Prepare 的機器,那就直接同意通過了。程式碼片段:
round, exist := px.rounds[args.Seq]if !exist {// new seq of commit,so need newpx.rounds[args.Seq] = px.newInstance()round, _ = px.rounds[args.Seq]reply.Err = OK}
在完成邏輯判斷過後,如果本次提議是通過的,那麼還需返回給提議者,已經通過提議和確定的值。程式碼片段:
if reply.Err == OK {reply.AcceptPnum = round.acceptorNumberreply.AcceptValue = round.acceptValuepx.rounds[args.Seq].proposeNumber = args.PNum}
Phase2
func (px Paxos)Accept(args *AcceptArgs, reply *AcceptReply) error {px.mu.Lock()defer px.mu.Unlock()round, exist := px.rounds[args.Seq]if !exist {px.rounds[args.Seq] = px.newInstance()reply.Err = OK}else {if args.PNum >= round.proposeNumber {reply.Err = OK}else {reply.Err = Reject}}if reply.Err == OK {px.rounds[args.Seq].acceptorNumber = args.PNumpx.rounds[args.Seq].proposeNumber = args.PNumpx.rounds[args.Seq].acceptValue = args.Value}else {//reject}return nil}
在 Accept 階段基本和 Prepare 階段如出一轍咯。判斷當前的提議是否存在,如果不純在表明是新的,那就直接返回 OK 咯!
round, exist := px.rounds[args.Seq]if !exist {px.rounds[args.Seq] = px.newInstance()reply.Err = OK}
然後同樣判斷提議號是否大於等於當前的提議編號,如果是,那同樣也返回 OK 咯,否者就拒絕。
if args.PNum >= round.proposeNumber {reply.Err = OK}else {reply.Err = Reject}
與此重要的一點就是,如果提議通過,那麼就需設定當輪的提議編號和提議的值。
if reply.Err == OK {px.rounds[args.Seq].acceptorNumber = args.PNumpx.rounds[args.Seq].proposeNumber = args.PNumpx.rounds[args.Seq].acceptValue = args.Value}
整個使用過程中使用了 Map 和數組來儲存一些輔助資訊,Map 主要儲存的是,每一輪的投票被確定的結果,Key 表示每一輪的投票編號,Round 表示儲存已經接受的值。Completes 數組主要是用於儲存在使用的過程中,已經確定完成了的最小的編號。
rounds map[int]*Round //cache each round paxos result key is seq value is valuecompletes [] int //maintain peer min seq of completedfunc (px *Paxos)Decide(args *DecideArgs, reply *DecideReply) error {px.mu.Lock()defer px.mu.Unlock()_, exist := px.rounds[args.Seq]if !exist {px.rounds[args.Seq] = px.newInstance()}px.rounds[args.Seq].acceptorNumber = args.PNumpx.rounds[args.Seq].acceptValue = args.Valuepx.rounds[args.Seq].proposeNumber = args.PNumpx.rounds[args.Seq].state = Decidedpx.completes[args.Me] = args.Donereturn nil}
同時 Decide 方法,用於提議者來確定某個值,這個映射到分布式裡面的狀態機器的應用。
客戶段通過提交指令給伺服器,伺服器通過 Paxos 演算法是現在多台機器上面,所有的伺服器按照順序執行相同的指令,然後狀態機器對指令進行執行最後每台機器的結果都是一樣的。
到站
在分布式環境之中,網路故障宕機屬於正常現象。如果一台機器宕機了,過了一段時間又恢複了,那麼他宕機的時間之中,怎麼將之前的指令恢複回來?當他提交一個 jmp 指令的時候,索引 1、2 都是已經確定了的指令,所以可以直接從索引 3 開始,當他提交 Propser(jmp)的時候,會收到 s1、s3 的傳回值(cmp),根據 Paxos 演算法後者認同前者的原則,所以他會在 Phase2 階段提交一個值為 cmp accept 的請求,最後索引為 3 的就變成了 cmp,如果說在這個階段沒有傳回值,那麼就選擇用戶端的傳回值就可以了,最後就達成了一致。
源於 MIT,然後用於自己學習,源碼注釋地址。