隨著大型網站的各種高并發訪問、海量數據處理等場景越來越多,如何實現網站的高可用、易伸縮、可擴展、安全等目標就顯得越來越重要。為了解決這樣一系列問題,大型網站的架構也在不斷發展。提高大型網站的高可用架構,不得不提的就是分布式。在關于分布式事務、兩階段提交協議、三階提交協議一文中主要用于解決分布式一致性問題的集中協議,那么這篇文章主要講解業內公認的比較難的也是最行之有效的paxos算法。
我認為對paxos算法講解的最清楚的就是維基百科了。但是要看懂維基百科中的介紹需要很強的數學思維(paxos畢竟是一個算法),而且有很多關于定理的推論、證明等過程。那么本篇文章主要站在程序的角度,通俗的,循序漸進的講解到底什么是paxos算法。
背景
google Chubby的作者Mike Burrows說過, there is only one consensus protocol, and that’s Paxos” – all other Approaches are just broken versions of Paxos. 意即世上只有一種一致性算法,那就是Paxos,所有其它一致性算法都是Paxos算法的不完整版。
Paxos算法是萊斯利·蘭伯特(Leslie Lamport,就是 LaTeX 中的”La”,此人現在在微軟研究院)于1990年提出的一種基于消息傳遞的一致性算法。為描述 Paxos 算法,Lamport 講述了這樣一個故事:
在古希臘有一個島嶼叫做Paxos,這個島嶼通過議會的形式修訂法律。執法者(legislators,后面稱為牧師priest)在議會大廳(chamber)中表決通過法律,并通過服務員傳遞紙條的方式交流信息,每個執法者會將通過的法律記錄在自己的賬目(ledger)上。問題在于執法者和服務員都不可靠,他們隨時會因為各種事情離開議會大廳、服務員也有可能重復傳遞消息(或者直接徹底離開),并隨時可能有新的執法者(或者是剛暫時離開的)回到議會大廳進行法律表決,因此,議會協議要求保證上述情況下可以能夠正確的修訂法律并且不會產生沖突。
什么是paxos算法
Paxos 算法是分布式一致性算法用來解決一個分布式系統如何就某個值(決議)達成一致的問題。
人們在理解paxos算法是會遇到一些困境,那么接下來,我們帶著以下幾個問題來學習paxos算法:
1、paxos到底在解決什么問題?
2、paxos到底如何在分布式存儲系統中應用?
3、paxos的核心思想是什么?
paxos解決了什么問題
在關于分布式一致性的探究中我們提到過,分布式的一致性問題其實主要是指分布式系統中的數據一致性問題。所以,為了保證分布式系統的一致性,就要保證分布式系統中的數據是一致的。
在一個分布式數據庫系統中,如果各節點的初始狀態一致,每個節點都執行相同的操作序列,那么他們最后能得到一個一致的狀態。為保證每個節點執行相同的命令序列,需要在每一條指令上執行一個“一致性算法”以保證每個節點看到的指令一致。
所以,paxos算法主要解決的問題就是如何保證分布式系統中各個節點都能執行一個相同的操作序列。

上圖中,C1是一個客戶端,N1、N2、N3是分布式部署的三個服務器,初始狀態下N1、N2、N3三個服務器中某個數據的狀態都是S0。當客戶端要向服務器請求處理操作序列:op1op2op3時(op表示operation)(這里把客戶端的寫操作簡化成向所有服務器發送相同的請操作序列,實際上可能通過Master/Slave模式處理)。如果想保證在處理完客戶端的請求之后,N1、N2、N3三個服務器中的數據狀態都能從S0變成S1并且一致的話(或者沒有執行成功,還是S0狀態),就要保證N1、N2、N3在接收并處理操作序列op1op2op3時,嚴格按照規定的順序正確執行opi,要么全部執行成功,要不就全部都不執行。
所以,針對上面的場景,paxos解決的問題就是如何依次確定不可變操作opi的取值,也就是確定第i個操作什么,在確定了opi的內容之后,就可以讓各個副本執行opi操作。
Paxos算法詳解
Paxos是一個十分巧妙的一致性算法,但是他也十分難以理解,就連他的作者Lamport都被迫對他做過多種講解。我認為對paxos算法講解的最清楚的就是維基百科了。但是要看懂維基百科中的介紹需要很強的數學思維(paxos畢竟是一個算法),而且有很多關于定理的推論、證明等過程。那么本篇文章主要站在程序的角度,通俗的,循序漸進的講解到底什么是paxos算法。
我們先把前面的場景簡化,把我們現在要解決的問題簡化為如何確定一個不可變變量的取值(每一個不可變變量可以標識一個操作序列中的某個操作,當確保每個操作都正確之后,就可以按照順序執行這些操作來保證數據能夠準確無誤的從一個狀態轉變成另外一個狀態了)。
接下來,請跟我一步一步的學習paxos算法。
要學習paxos算法,我們就要從他要解決的問題出發,假如沒有paxos算法,當我們面對如何確定一個不可變變量的取值這樣一個問問題的時候,我們應該如何解決呢?
這里暫不介紹paxos中的角色的概念,讀者可以自行從維基百科中了解。不了解的話也可以直接往下看,看著看著就了解了。
問題抽象
我們把確定一個不可變變量的取值問題定義成:
設計一個系統,來存儲名稱為var的變量。
var的取值可以是任意二進制數
系統內部由多個Accepter組成,負責管理和存儲var變量。
系統對外提供api,用來設置var變量的值propose(var,V) => <ok,f> or <error>
將var的值設置為V,系統會返回ok和系統中已經確定的取值f,或者返回error。
外部有多個Proposer機器任意請求系統,調用系統API(propose(var,V) => <ok,f> or <error>)來設置var變量的值。
如果系統成功的將var設置成了V,那么返回的f應該就是V的值。否則,系統返回的f就是其他的Proposer設置的值。
>
系統需要保證var的取值滿足一致性
如果var沒有被設置過,那么它的初始值為null
一旦var的值被設置成功,則不可被更改,并且可以一直都能獲取到這個值
系統需要滿足容錯特性
可以容忍任意proposer出現故障可以容忍少數acceptor故障(半數以下)
暫時忽略網絡分化問題和acceptor故障導致var丟失的問題。
到這里,問題已經抽象完成了,讀者可以再仔細看看上面的系統描述。如果這樣設置一個系統,是不是就可以保證變量var的不可變性了呢?
這里還是再簡單講解一下,上面的系統確實可以保證變量var的不可變性。
因為var的初始值為null,當有proposer請求接口propose(var,v)設置var的值的時候,系統會將var設置為v,并返回f(f==v)。
var變量被初始化以后,再有proposer請求propose(var,v)設置var的值的時候,系統會直接返回系統中已有的var的值f,而放棄proposer提供的v。
系統難點
要設計以上系統存在以下難點:
1、管理多個proposer并發執行
2、容忍var變量的不可變性
3、容忍任意Proposer的故障
4、容忍半數以下的acceptor的故障
解決方案一
先考慮整個系統由單個acceptor組成。通過類似互斥鎖的方式來管理并發的proposer的請求。
proposer向acceptor申請acceptor的互斥訪問權,當取得互斥訪問權之后才能調用api給var變量賦值。accepter向proposer發放互斥訪問權,誰取得了互斥訪問權,acceptor就接收誰的請求。這樣通過互斥訪問的機制,proposer就要按照獲取互斥訪問權的順序來請求系統。一旦acceptor接收到一個proposer請求,并成功給var變量賦值之后,就不再允許其他的proposer設置var變量的值。每當再有proposer來請求設置var變量的值的時候,acceptor就會將var里面現有的值返回給他。
基于互斥訪問權的acceptor的實現
acceptor會保存變量var的值和一個互斥鎖Lock。
提供接口prepare()
加互斥鎖,給予var的互斥訪問權,并返回當前var的取值
提供接口release()
用于釋放互斥訪問權
提供接口accept(var, v)
如果已經加鎖,并且當前var沒有值,則將var的值設置成v,并釋放鎖。
proposer采用兩階段來實現
Step1、通過調用prepare接口來獲取互斥性訪問權和當前var的取值
如果無法獲取到互斥性訪問權,則返回,并不能進入到下一個階段,因為其他proposer獲取到了互斥性訪問權。
Step2、根據當前var的取值f選擇執行
1、如果f的取值為null,說明沒有被設置過值,則調用接口accept(var ,v)來將var的取值設置成v,并釋放掉互斥性訪問權。2、如果f的取值不為null,說明var已經被其他proposer設置過值,則調用release接口釋放掉互斥性訪問權。
總結:方案一通過互斥訪問的方式來保證所有的proposer能夠串行的訪問acceptor,這樣其實并沒有解決多個proposer并發執行的問題。只是想辦法繞開了并發執行。雖然可以在一定程度上保證var變量的取值是確定的。但是一旦獲取到互斥訪問權的proposer在執行過程中出現故障,那么就會導致所有其他proposer無法再獲取到互斥訪問權,就會發生死鎖。。所以,方案一不僅效率低、而且還會產生死鎖問題,不能容忍任意Proposer出現故障。在之前提到的四個系統難點中,方案一可以解決難點1和難點2,但是無法解決難點3和難點4。
解決方案二
通過引入搶占式訪問權來取代互斥訪問權。acceptor有權讓任意proposer的訪問權失效,然后將訪問權發放給其他的proposer。
在方案二中,proposer向acceptor發出的每次請求都要帶一個編號(epoch),且編號間要存在全序關系。一旦acceptor接收到proposer的請求中包含一個更大的epoch的時候,馬上讓舊的epoch失效,不再接受他們提交的取值。然后給新的epoch發放訪問權,讓它可以設置var變量的值。
為了保證var變量取值的不變性,不同epoch的proposer之前遵守后者認同前者的原則:
在確保舊的epoch已經失效后,并且舊的epoch沒有設置var變量的值,新的epoch會提交自己的值。當舊的epoch已經設置過var變量的取值,那么新的epoch應該認同舊的epoch設置過的值,并不再提交新的值。
基于搶占式訪問權的acceptor的實現
原文鏈接:
http://www.hollischuang.com/archives/693