Paxos算法和Uniform Consensus

昨天我們介紹了拜占庭將軍問題和FLP Impossibility, 我們知道了在異步網絡中的total correct的consensus算法是不存在的, 但是如果我們放松

liveness的要求, 我們在工程和實際應用當中, 我們是否可以解決一致性問題呢? 今天我們會給大家介紹一個工程中最常用的一個consensus算法 – Paxos算法.

大名鼎鼎的Paxos算法

Consensus主要目的是屏蔽掉故障節點的噪音讓整個系統正常運行下去, 比如選舉過程和狀態機復制. 所以Consensus問題對于agreement條件做了放松, 它接受不一致是常態的事實, 既然我無法知道某些節點是掛了還是暫時聯系不到, 那我只要關心正確響應的節點, 只要表決能過半即可, 過半表決意味著雖然沒有完全一致, 但是”投票結果”被過半成員繼承下來了, 這是因為任何兩個quorum一定會存在交集(想象一下有A/B/C三個節點, 兩個quorum比如AB和AC一定會有A是交集), 所以不管有多少個quorum存在, 我們能確保他們一定會有交集, 所以他們一定能信息互通而最終達成一致, 其他沒有達成一致的成員將來在網絡恢復后也可以和這部分交集內的節點傳播出去的”真理”達成一致.

大名鼎鼎的Paxos算法誕生了, Paxos是第一個正確實現適用于分布式系統的Consensus算法。1990年Lamport提出了這個算法, 但有趣的是ACM TOCS的評審委員們沒看懂他的論文, 主編建議他不要拿古希臘神話什么長老被砸健忘了的故事寫論文, 要他用數學語言寫簡潔點, Lamport也是個人才, 他拒絕修改論文, 并在一次會議上公開質疑”為什么搞基礎理論的人一點幽默感都沒有呢?”

6年光陰過去, 另外一個超級牛人, 圖靈獎獲得者Butler Lampson看到這篇論文, 而且…… 他看懂了! Lampson覺得這個算法很重要并呼吁大家重新審視這篇重要論文, 后來提出FLP理論的三人組其中的Nacy Lynch重寫了篇文章闡述這篇論文, 后來終于大家都看懂了, 最終1998年ACM TOCS終于發表了這篇論文 [The Part-Time Parliament. ACM Transactions on Computer Systems 16, 2 (May 1998), 133-169], 至此將近9年了. 最爆笑得是1998發表的時候, 負責編輯的Keith配合Lamport的幽默寫的注解, 這里我給八卦一下省的你去翻論文了:

本文最近剛被從一個文件柜里發現, 盡管這篇論文是很久之前提交的但是主編認為還是值得發表的(不是我們ACM TOCS過去沒看懂, 是忘記發表了). 但是由于作者目前在希臘小島上考古, ACM TOCS聯系不上這位考古學家, 所以任命我來發表這篇論文(其實是Lamport拒絕修改論文, 不鳥ACM TOCS了). 作者貌似是個對計算機科學稍微有點興趣的考古學家(赤裸裸的吐槽), 盡管他描述的Paxon島上民主制度的故事對計算機科學家沒啥興趣, 但是但是這套制度對于在異步網絡中實現分布式系統到時很好的模型(這句總算客觀了). 建議閱讀的時候直接看第四節(跳過前三節神話故事), 或者最好先別看(你可能會看不懂), 最好先去看看Lampson或者De Prisco對這篇論文的解釋.

看到這你笑噴了沒?

我們的”考古學家” Lamport在Paxos算法定義了三個角色, 其中proposer是提出建議值的進程, acceptor是決定是否接受建議的進程, learner是不會提議但是也要了解結果的進程, 在一個系統中一個進程經常同時扮演這三個角色. 算法分兩個階段 (以下是最基礎的算法, 其中沒有learners):

第一階段: 一個proposer選擇一個全局唯一的序號n發給至少過半的acceptors. 如果一個acceptor收到的請求中的序號n大于之前收到的建議, 那么這個acceptor返回proposer一個確認消息表示它不會再接受任何小于這個n的建議.

第二階段: 如果proposer收到過半的acceptor的確認消息表示他們不會接受n以下的建議, 那么這個proposer給這些acceptor返回附帶他的建議值v的確認消息. 如果一個acceptor收到這樣的確認消息{n, v}, acceptor就會接受它. 除非在此期間acceptor又收到了一個更高的n的建議.

對證明有興趣的讀者可以去看看Paxos Made Simple. 簡單來講, 算法的正確性有兩個方面。

提議的safety是由sequence number N決定的, 如果N是全序集, 唯一而且一定有先后, 并且在每個proposer上都單調遞增, 那么acceptor選擇的結果就是安全的. 實際應用中, 這個N經常是timestamp, 進程id, 還有進程的本體counter的組合, 比如: timestamp + node id + counter, 或者像twitter的snowflake基于timestamp和網卡mac地址的算法. 這樣可以保證事件順序盡量接近物理時間的順序, 同時保證事件number的唯一性.

過半表決可以保證網絡出現多個分區的時候, 任何兩個能夠過半的分區必然存在交集, 而交集內的進程就可以保證正確性被繼承, 以后被傳播出去.

Paxos是一個非?;A的算法, 更多的時候你需要在Paxos的基礎之上實現你的算法.

Paxos的過半表決有一定局限性. 這牽扯到分區可用性. 如果網絡分區的時候, 沒有形成多數派, 比如一個網絡內被均勻的分成了三個小區, 那么整個系統都不能正常工作了. 如果分成一個大區, 一個小區, 那么小區是無法工作的, 如果大區和小區非常接近, 比如是501 vs 499, 這意味著系統的處理能力可能會下降一半. 所有這些都影響到了Paxos的可用性. 舉個例子, Zookeeper的ZAB協議的選舉和廣播部分很類似Paxos, Zookeeper允許讀取過期數據來獲得更好的性能, 所以一般情況下是Sequential Consistency. 但是他有一個Sync命令, 當你每次都Sync+Read的時候, 雖然性能大打折扣, 但是Zookeeper就是和Paxos一樣能保證Linearizability了. 當zookeeper在發生網絡分區的時候, 如果leader在quorum side (大區), 那么quorum side的讀寫都正常, 但是non-quorum side因為無法從小區選出leader, 所有連接到non-quorum side的客戶端的所有的讀寫都會失敗! (也可以不發Sync命令降低到SC級別, 通過過期的緩存讓讀操作能繼續下去) 如果把client也考慮在內, 假設如下圖所示, 99%的節點連同1%的客戶端處于一個分區, 那么會導致99%的客戶端都無法正常工作, 盡管這個時候集群是99%的節點都是好的. (通常我們的網絡拓撲結構不會發生這么極端不平衡的情況).

有人會覺得網絡分區在同一個數據中心內發生的概率非常低吧?

其實不然, 舉一個例子, github在2012年有一次升級一對聚合交換機的時候, 發現了一些問題決定降級, 降級過程中其中要關閉一臺交換機的agent來收集故障信息, 導致90秒鐘的網絡分區, 精彩的開始了. 首先, github的文件服務器是基于DRBD的, 因為每一對文件服務器只能有一個活動節點, 另外一個作為熱備, 當主節點出現故障后, 熱備節點在接替故障的活動節點之前會發一個指令去關閉活動節點. 如果網絡沒有分區, 活動節點的硬件發生了故障導致響應超時, 那么這樣可以避免brain-split. 但是在網絡分區的情況下, 活動節點沒有收到關閉指令, 熱備節點就把自己作為新的活動節點了, 當網絡恢復之后, 就有了兩個活動節點同時服務, 導致了數據不一致不說, 還會發生互相嘗試殺死對方的情況, 因為兩邊都認為對方是有故障的, 需要殺死對方, 結果有些文件服務器真的把對方都殺死了.

網絡分區是真實存在的, 而且在跨多個數據中心的情況下, 網絡分區發生的概率更高. 所以使用zookeeper的時候你一定要理解他的一致性模型在處理網絡分區的情況時的局限性. 對于map reduce來講, 結果是correct or nothing, 寧可犧牲可用性也要保證一致性, safety更重要. 但是一個分布式系統的服務發現組件就不同了, 對于發現服務而言, having something wrong is better than having nothing, liveness更重要. 所以Netflix的黑幫們才自己造了個輪子Eureka, 因為如果你的微服務都是無狀態的, 大多數情況下發現服務只要能達到Eventual Consistency就可以了, 而高可用性是必須的. 后面介紹CAP定理和Eventual Consistency的時候會介紹一下側重可用性的算法.

除了網絡分區, Paxos的另外一個缺點是延遲比較高. 因為Paxos放松了liveness, 它有時候可能會多個回合才能決定結果, 錯誤的實現甚至會導致live lock. 比如proposer A提出n1, proposer B提出n2, 如果n1 > n2, acceptor先接受了n1, B收到拒絕之后重新發起n3, 如果n3先于A的確認消息到達acceptor, 而且n3 > n1, 那么acceptor會拒絕A, 接受B, A可能會重復B的行為, 然后無限循環下去.

FLP理論描述了Consensus可能會進入無限循環的情況, 但是實際應用中這個概率非常低, 大家都知道計算機科學是應用科學, 不是離散數學那樣非正即誤, 如果錯誤或者偏離的概率非常低, 工程中就會采用. 比如費馬小定理(Fermat’s Little Theorem) 由于Carmichael Number的存在并不能用來嚴格判斷大素數, 但是由于Carmichael Number實在是太少, 1024 bit的范圍里概率為10^-88, 所以RSA算法還是會用費馬小定理. 同樣, 根據FLP理論, 異步網絡中Paxos可能會進入無限循環. 真實世界中Paxos的如果兩個節點不斷的互相否定, 那么就會出現無限循環, 但是要永遠持續下去的概率非常非常低, 實際中我們經常讓某個Proposer獲取一定期限的lease, 在此期限之內只有一個proposer接受客戶端請求并提出 proposal. 或者隨機改變n的增長節奏和proposer的發送節奏等, 來降低livelock的概率. 當然, 單從純粹FLP理論來看, 超過一個proposer的時候Paxos是不保證liveness的.

Paxos在實現中, 每個進程其實一般都是身兼三職, 然后成功提議的那個proposer所在的進程就是 distinguished proposer, 也是 distinguished learner, 我們稱之為 leader.

上面提到的Paxos算法是最原始的形式, 這個過程中有很多可以優化的地方, 比如如果accetpr拒絕了可以順便返回當前最高的n, 減少算法重試的回合, 但這不影響這個算法的正確性. 比如Multi-Paxos可以假設leader沒有掛掉或者過期之前不用每次都發出prepare請求, 直接發accept請求, 再比如Fast Paxos等等.

Paxos設計的時候把異步網絡的不確定性考慮在內, 放松了liveness的要求, 算法按照crash-recovery的思想設計, 所以Paxos才可以成為這樣實際廣泛應用而且成功的算法. 但是Paxos也不能容忍拜占庭式故障節點, 要容忍拜占庭式故障實在是太困難了 (比如, Paxos中兩個quorum中的交集如果都是叛徒怎么辦?).

盡管Paxos有很多缺點, 但是Paxos仍然是分布式系統中最重要的一個算法, 比如它的一個重要用途就是 State Machine Replication. 一個Deterministic State Machine對于固定的輸入序列一定會產生固定的結果, 不論重復執行多少次, 或者再另外一臺一樣的機器上執行, 結果都是一樣的. 分布式系統中最常見的一個需求就是通過某種路由算法讓客戶端請求去一組狀態一致的服務器, 數據在服務器上的分布要有replica來保證高可用性, 但是replica之間要一致. 狀態復制就成為了分布式系統的一個核心問題. Paxos的狀態復制可以保證整個系統的所有狀態機接受同樣的輸入序列 (Atomic Broadcast), 如果查詢也使用過半表決, 那么你一定會得到正確的結果.

這種做法相對于Master-Slave的的狀態復制一致性好很多. 比如一旦Master節點掛了, 還沒來得及復制的結果會導致Slave之間的狀態有可能是不一致的, 如果客戶端能夠訪問到延遲的Slave節點, 那么用戶展現的數據將會不一致. 如果你可以犧牲性能來換得更高的一致性, 那么你可以通過Paxos表決查詢來屏蔽掉延遲或者有故障的節點. 只要系統中存在一個quorum, 那么狀態的一致性就可以保留下去. 比如google的chubby, 只要故障節點不超過一半, 網絡沒有發生分區, chubby就可以通過Paxos狀態機為其他服務器提供分布式鎖. 因為chubby分別部署在每個數據中心, 他們沒有跨數據中心的通信, 所以網絡分區的故障頻率不像跨數據中心的情況那么多高, chubby犧牲部分性能和網絡分區下的可用性, 換來了一致性.

因為Paxos家族的算法寫性能都不是很好但是一致性又很重要, 所以實現中我們經常做一些取舍, 讓Paxos只處理最關鍵的信息. 比如Kafaka的每個Partition都有多個replicas, 日志的復制本身沒有經過Paxos這樣高延遲的算法, 但是為了保證負責接受producer請求和跟蹤ISR的leader只有一個, Kafaka依賴于Zookeeper的ZAB算法來選舉leader. 在實際應用中, 狀態機復制不太適合很高的吞吐量, 一般都是用于不太頻繁寫入的重要信息. Zookeeper不能被當做一個OLTP級的數據庫用, 它不是分布式系統協同的萬能鑰匙。

Uniform Consensus

對于Consensus問題, 我們只關注非故障節點的一致性和整體系統的正常, 而Uniform Consensus中要求無論是非故障節點還是故障節點, 他們都要一致, 比如在分布式事務的前后, 各個節點必須一致, 故障節點恢復后也要一致, 任何時刻都不應該有兩個參與者一個決定提交, 一個決定回滾.。

很長一段時間內大多數人沒有把Uniform Consensus單獨分類, 直到2001年才有人提出Uniform Consensus更難. [Uniform Consensus is Harder Than Consensus, Charron-Bost, Schiper, Journal of Algorithms 51 2004 15-37]

一般的Consensus問題通常用來解決狀態機復制時容錯處理的問題, 比如Paxos, 而Uniform Consensus所處理的是分布式事務這樣的問題, 在Uniform Consensus中我們要求所有節點在故障恢復后都要達成一致. 所以Uniform Consensus的定義在agreement上更加嚴格:

termination: 所有進程最終會在有限步數中結束并選取一個值, 算法不會無盡執行下去.

agreement: 所有進程(包含故障節點恢復后)必須同意同一個值. (假設系統沒有拜占庭故障)

validity: 最終達成一致的值必須是V1到Vn其中一個, 如果所有初始值都是vx, 那么最終結果也必須是vx.

總結

至此, 通過對Concensus問題的介紹, 我們對Linearizability和Sequential Consistency的應用應該有了更深入的理解, 在本系列下一篇文章中我們將會介紹性能更好但是一致性要求更低的Causal Consistency, PRAM以及不需要硬件自動同步的一致性模型, 比如Weak Consistency。

本文作者:Daniel吳強(點融黑幫),現任點融網首席社交平臺架構師,前盛大架構師, 專注分布式系統和移動應用, 有十三年的開發經驗, 目前在點融做最愛的兩件事情: 寫代碼和重構代碼。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容