一、前言
在上一篇理解了Paxos算法的理論基礎后,接下來看看Paxos算法在工程中的應用。
二、Chubby
Chubby是一個面向松耦合分布式系統的鎖服務,GFS(Google File System)和Big Table等大型系統都是用它來解決分布式協作、元數據存儲和Master選舉等一些列與分布式鎖服務相關的問題。Chubby的底層一致性實現就是以Paxos算法為基礎,Chubby提供了粗粒度的分布式鎖服務,開發人員直接調用Chubby的鎖服務接口即可實現分布式系統中多個進程之間粗粒度的同控制,從而保證分布式數據的一致性。
2.1 設計目標
Chubby被設計成為一個需要訪問中心化的分布式鎖服務。
① 對上層應用程序的侵入性更小,使用一個分布式鎖服務的接口方式對上層應用程序的侵入性更小,應用程序只需調用相應的接口即可使用分布式一致性特性,并且更易于保持系統已有的程序結構和網絡通信模式。
② 便于提供數據的發布與訂閱,在Chubby進行Master選舉時,需要使用一種廣播結果的機制來向所有客戶端公布當前Master服務器,這意味著Chubby應該允許其客戶端在服務器上進行少量數據的存儲和讀取(存儲主Master地址等信息),也就是對小文件的讀寫操作。數據的發布與訂閱功能和鎖服務在分布式一致性特性上是相通的。
③ 開發人員對基于鎖的接口更為熟悉,Chubby提供了一套近乎和單機鎖機制一直的分布式鎖服務接口。
④ 更便捷地構建更可靠的服務,Chubby中通常使用5臺服務器來組成一個集群單元(Cell),根據Quorum機制(在一個由若干個機器組成的集群中,在一個數據項值的選定過程中,要求集群中過半的機器達成一致),只要整個集群中有3臺服務器是正常運行的,那么整個集群就可以對外提供正常的服務。
⑤ 提供一個完整的、獨立的分布式鎖服務,Chubby對于上層應用程序的侵入性特別低,對于Master選舉同時將Master信息等級并廣播的場景,應用程序只需要向Chubby請求一個鎖,并且在獲得鎖之后向相應的鎖文件寫入Master信息即可,其余的客戶端就可以通過讀取這個鎖文件來獲取Master信息。
⑥ 提供粗粒度的鎖服務,Chubby針對的應用場景是客戶端獲得鎖之后會進行長時間持有(數小時或數天),而非用于短暫獲取鎖的場景。當鎖服務短暫失效時(服務器宕機),Chubby需要保持所有鎖的持有狀態,以避免持有鎖的客戶端出現問題。而細粒度鎖通常設計為為鎖服務一旦失效就釋放所有鎖,因為其持有時間很短,所以其放棄鎖帶來的代價較小。
⑦ 高可用、高可靠,對于一個由5太機器組成的集群而言,只要保證3臺正常運行的機器,整個集群對外服務就能保持可用,另外,由于Chubby支持通過小文件讀寫服務的方式來進行Master選舉結果的發布與訂閱,因此在Chubby的實際應用中,必須能夠支撐成百上千個Chubby客戶端對同一個文件進行監控和讀取。
⑧ 提供時間通知機制,Chubby客戶端需要實時地感知到Master的變化情況,這可以通過讓你客戶端反復輪詢來實現,但是在客戶端規模不斷增大的情況下,客戶端主動輪詢的實時性效果并不理想,且對服務器性能和網絡帶寬壓力都非常大,因此,Chubby需要有能力將服務端的數據變化情況以時間的形式通知到所有訂閱的客戶端。
2.2 技術架構
Chubby的整個系統結構主要由服務端和客戶端兩部分組成,客戶端通過RPC調用和服務端進行通信,如下圖所示。
一個典型的Chubby集群(Chubby Cell),通常由5臺服務器組成,這些副本服務器采用Paxos協議,通過投票的方式來選舉產生一個獲得過半投票的服務器作為Master,一旦成為Master,Chubby就會保證在一段時間內不會再有其他服務器成為Master,這段時期稱為Master租期(Master Lease),在運行過程中,Master服務器會通過不斷續租的方式來延長Master租期,而如果Master服務器出現故障,那么余下的服務器會進行新一輪的Master選舉,最終產生新的Master服務器,開始新的Master租期。
集群中的每個服務器都維護著一份服務端數據庫的副本,但在實際運行過程中,只有Master服務器才能對數據庫進行寫操作,而其他服務器都是使用Paxos協議從Master服務器上同步數據庫數據的更新。
Chubby客戶端通過向記錄有Chubby服務端機器列表的DNS來請求獲取所有的Chubby服務器列表,然后逐一發起請求詢問該服務器是否是Master,在這個詢問過程中,那些非Master的服務器,則會將當前Master所在的服務器標志反饋給客戶端,這樣客戶端就能很快速的定位到Master服務器了。
只要該Master服務器正常運行,那么客戶端就會將所有的請求都發送到該Master服務器上,針對寫請求,Master會采用一致性協議將其廣播給集群中所有的副本服務器,并且在過半的服務器接受了該寫請求后,再響應給客戶端正確的應答,而對于讀請求,則不需要在集群內部進行廣播處理,直接由Master服務器單獨處理即可。
若該Master服務器發生故障,那么集群中的其他服務器會在Master租期到期后,重新開啟新的一輪Master選舉,通常一次Master選舉大概花費幾秒鐘的時間,而如果其他副本服務器崩潰,那么整個集群繼續工作,該崩潰的服務器會在恢復之后自動加入到Chubby集群中去,新加入的服務器首先需要同步Chubby最新的數據庫數據,完成數據庫同步之后,新的服務器就可以加入到正常的Paxos運作流程中與其他服務器副本一起協同工作。若崩潰后幾小時后仍無法恢復工作,那么需要加入新的機器,同時更新DNS列表(新IP代替舊IP),Master服務器會周期性地輪詢DNS列表,因此會很快感知服務器地址的變更,然后Master就會將集群數據庫中的地址列表做相應的變更,集群內部的其他副本服務器通過復制方式就可以獲取到最新的服務器地址列表了。
2.3 目錄與文件
Chubby對外提供了一套與Unix文件系統非常相近但是更簡單的訪問接口。Chubby的數據結構可以看作是一個由文件和目錄組成的樹,其中每一個節點都可以表示為一個使用斜杠分割的字符串,典型的節點路徑表示如下:
/ls/foo/wombat/pouch
其中,ls是所有Chubby節點所共有的前綴,代表著鎖服務,是Lock Service的縮寫;foo則指定了Chubby集群的名字,從DNS可以查詢到由一個或多個服務器組成該Chubby集群;剩余部分的路徑/wombat/pouch則是一個真正包含業務含義的節點名字,由Chubby服務器內部解析并定位到數據節點。
Chubby的命名空間,包括文件和目錄,我們稱之為節點(Nodes,我們以數據節點來泛指Chubby的文件或目錄)。在同一個Chubby集群數據庫中,每一個節點都是全局唯一的。和Unix系統一樣,每個目錄都可以包含一系列的子文件和子目錄列表,而每個文件中則會包含文件內容。Chubby沒有符號鏈接和硬連接的概念。
Chubby上的每個數據節點都分為持久節點和臨時節點兩大類,其中持久節點需要顯式地調用接口API來進行刪除,而臨時節點則會在其對應的客戶端會話失效后被自動刪除。也就是說,臨時節點的生命周期和客戶端會話綁定,如果該臨時節點對應的文件沒有被任何客戶端打開的話,那么它就會被刪除掉。因此,臨時節點通常可以用來進行客戶端會話有效性的判斷依據。
Chubby的每個數據節點都包含了少量的元數據信息,其中包括用于權限控制的訪問控制列表(ACL)信息,同時每個節點的元數據還包括4個單調遞增的64位標號:
① 實例標號,用于標識創建該數據節點的順序,節點的創建順序不同,其實例編號也不相同,可以通過實例編號來識別兩個名字相同的數據節點是否是同一個數據節點,因為創建時間晚的數據節點,其實例編號必定大于任意先前創建的同名節點。
② 文件內容編號(針對文件),用于標識文件內容的變化情況,該編號會在文件內容被寫入時增加。
③ 鎖編號,用于標識節點鎖狀態的變更情況,該編號會在節點鎖從自由狀態轉化到被持有狀態時增加。
④ ACL編號,用于標識節點的ACL信息變更情況,該編號會在節點的ACL配置信息被寫入時增加。
2.4 鎖和鎖序列器
分布式環境中鎖機制十分復雜,消息的延遲或是亂序都有可能引起鎖的失效,如客戶端C1獲得互斥鎖L后發出請求R,但請求R遲遲沒有到達服務端(網絡延遲或消息重發等),這時應用程序會認為該客戶端進程已經失敗,于是為另一個客戶端C2分配鎖L,然后在發送請求R,并成功應用到了服務器上。然而,之前的請求R經過一波三折后也到達了服務器端,此時,它可能不瘦任何鎖控制的情況下被服務端處理,而覆蓋了客戶端C2的操作,也是導致了數據不一致問題。
在Chubby中,任意一個數據節點均可被當做一個讀寫鎖來使用:一種是單個客戶端排他(寫)模式持有這個鎖,另一種則是任意數目的客戶端以共享(讀)模式持有這個鎖,Chubby放棄了嚴格的強制鎖,客戶端可以在沒有獲取任何鎖的情況下訪問Chubby的文件。
Chubby采用了鎖延遲和鎖序列器兩種策略來解決上述由于消息延遲和重排序引起的分布式鎖問題,對于鎖延遲而言,如果一個客戶端以正常的方式主動釋放了一個鎖,那么Chubby服務端將會允許其他客戶端能夠立即獲取到該鎖;如果鎖是以異常情況被釋放的話,那么Chubby服務器會為該鎖保留一定的時間,這稱之為鎖延遲,這段時間內,其他客戶端無法獲取該鎖,其可以很好的防止一些客戶端由于網絡閃斷的原因而與服務器暫時斷開的場景。對于鎖序列器而言,其需要Chubby的上層應用配合在代碼中加入相應的修改邏輯,任何時候,鎖的持有者都可以向Chubby請求一個鎖序列器,其包括鎖的名字、鎖模式(排他或共享)、鎖序號,當客戶端應用程序在進行一些需要鎖機制保護的操作時,可以將該鎖序列器一并發送給服務端,服務端收到該請求后,會首先檢測該序列器是否有效,以及檢查客戶端是否處于恰當的鎖模式,如果沒有通過檢查,那么服務端就會拒絕該客戶端的請求。
2.5 事件通知機制
為了避免大量客戶端輪詢Chubby服務端狀態所帶來的壓力,Chubby提供了事件通知機制,客戶端可以向服務端注冊事件通知,當觸發這些事件時,服務端就會向客戶端發送對應的時間通知,消息通知都是通過異步的方式發送給客戶端的。常見的事件如下
① 文件內容變更 ② 節點刪除 ③ 子節點新增、刪除 ④ Master服務器轉移
2.6 緩存
其是為了減少客戶端與服務端之間的頻繁的讀請求對服務端的壓力設計的,會在客戶端對文件內容和元數據信息進行緩存,雖然緩存提高了系統的整體性能,但是其也帶來了一定復雜性,如如何保證緩存的一致性。其通過租期機制來保證緩存的一致性。
緩存的生命周期和Master租期機制緊密相關,Master會維護每個客戶端的數據緩存情況,并通過向客戶端發送過期信息的方式來保證客戶端數據的一致性,在此機制下,Chubby能夠保證客戶端要么能夠從緩存中訪問到一致的數據,要么訪問出錯,而一定不會訪問到不一致的數據。具體的講,每個客戶端的緩存都有一個租期,一旦該租期到期,客戶端就需要向服務端續訂租期以繼續維持緩存的有效性,當文件數據或元數據被修改時,Chubby服務端首先會阻塞該修改操作,然后由Master向所有可能緩存了該數據的客戶端發送緩存過期信號,使其緩存失效,等到Master在接收到所有相關客戶端針對該過期信號的應答(客戶端明確要求更新緩存或客戶端允許緩存租期過期)后,再進行之前的修改操作。
2.7 會話
Chubby客戶端和服務端之間通過創建一個TCP連接來進行所有的網絡通信操作,這稱之為會話,會話存在生命周期,存在超時時間,在超時時間內,客戶端和服務端之間可以通過心跳檢測來保持會話的活性,以使會話周期得到延續,這個過程稱為KeepAlive(會話激活),如果能夠成功地通過KeepAlive過程將Chubby會話一直延續下去,那么客戶端創建的句柄、鎖、緩存數據等將仍然有效。
2.8 KeepAlive請求
Master服務端在收到客戶端的KeepAlive請求時,首先會將該請求阻塞住,并等到該客戶端的當前會話租期即將過期時,才為其續租該客戶端的會話租期,之后再向客戶端響應這個KeepAlive請求,并同時將最新的會話租期超時時間反饋給客戶端,在正常情況下,每個客戶端總是會有一個KeepAlive請求阻塞在Master服務器上。
2.9 會話超時
客戶端也會維持一個和Master端近似相同(由于KeepAlive響應在網絡傳輸過程中會花費一定的時間、Master服務端和客戶端存在時鐘不一致的現象)的會話租期。客戶端在運行過程中,按照本地的會話租期超時時間,檢測到其會話租期已經過期卻尚未收到Master的KeepAlive響應,此時,它將無法確定Master服務端是否已經中止了當前會話,這個時候客戶端處于危險狀態,此時,客戶端會清空其本地緩存并將其標記為不可用,同時客戶端還會等待一個被稱作寬限期的時間周期,默認為45秒,若在寬限期到期前,客戶端與服務端之間成功地進行了KeepAlive,那么客戶端就會再次開啟本地緩存,否則,客戶端就會認為當前會話已經過期了,從而終止本次會話。
在客戶端進入危險狀態時,客戶端會通過一個“jeopardy”事件來通知上層應用程序,如果恢復正常,客戶端同樣會以一個“safe”事件來通知應用程序可以繼續正常運行,但如果客戶端最終沒能從危險狀態中恢復過來,那么客戶端會以一個“expired”事件來通知應用程序當前Chubby會話已經超時。
2.10 Master故障恢復
Master服務端會運行著會話租期計時器,用來管理所有的會話的生命周期,如果Master在運行過程中出現故障,那么該計時器就會停止,直到新的Master選舉產生后,計時器才會繼續計時,即從舊的Master崩潰到新的Master選舉產生所花費的時間將不計入會話超時的計算中,這就等價于延長了客戶端的會話租期,如果Master在短時間內就選舉產生了,那么客戶端就可以在本地會話租期過期前與其創建連接,而如果Master的選舉花費了較長的時間,就會導致客戶端只能清空本地的緩存而進入寬限期進行等待,由于寬限期的存在,使得會話能夠很好地在服務端Master轉化的過程中得到維持,整個Master的故障恢復過程中服務端和客戶端的交互情況如下
如上圖所示,一開始在舊的Master服務器上維持了一個會話租期lease M1,在客戶端上維持對應的lease C1,同時客戶端的KeepAlive請求1一直被Master服務器阻塞著。一段時間后,Master向客戶端反饋了KeepAlive響應2,同時開始了新的會話租期lease M2,而客戶端在接收到該KeepAlive響應后,立即發送了新的KeepAlive請求3,并同時也開始了新的會話租期lease C2。至此,客戶端和服務端的交互都是正常的,隨后,Master發生了故障,從而無法反饋客戶端的KeepAlive請求3,在此過程中,客戶端檢測到會話租期lease C2已經過期,它會清空本地緩存并進入寬限期,這段時間內,客戶端無法確定Master上的會話周期是否也已經過期,因此它不會銷毀它的本地會話,而是將所有應用程序對它的API調用都阻塞住,以避免出現數據不一致的現象。同時,在客戶端寬限期開始時,客戶端會向上層應用程序發送一個“jeopardy”事件,一段時間后,服務器開始選舉產生新的Master,并為該客戶端初始化了新的會話租期lease M3,當客戶端向新的Master發送KeepAlive請求4時,Master檢測到該客戶端的Master周期號已經過期,因此會在KeepAlive響應5中拒絕這個客戶端請求,并將最新的Master周期號發送給客戶端,之后,客戶端會攜帶上最新的Master周期號,再次發送KeepAlive請求6給Master,最終,整個客戶端和服務端之間的會話就會再次回復正常。
在Master轉化的這段時間內,只要客戶端的寬限足夠長,那么客戶端應用程序就可以在沒有任何察覺的情況下,實現Chubby的故障恢復,但如果客戶端的寬限期設置得比較短,那么Chubby客戶端就會丟棄當前會話,并將這個異常情況通知給上層應用程序。
一旦客戶端與新的Master建立上連接之后,客戶端和Master之間會通過互相配合來實現對故障的平滑恢復,新的Master會設法將上一個Master服務器的內存狀態構建出來,同時,由于本地數據庫記錄了每個客戶端的會話信息,以及持有的鎖和臨時文件等信息,因此Chubby會通過讀取本地磁盤上的數據來恢復一部分狀態。一個新的Master服務器選舉之后,會進行如下處理。
① 確定Master周期。Master周期用來唯一標識一個Chubby集群的Master統治輪次,以便區分不同的Master,只要發生Master重新選舉,就一定會產生新的Master周期,即使選舉前后Master是同一臺機器。
② 新的Master能夠對客戶端的Master尋址請求進行響應,但是不會立即開始處理客戶端會話相關的請求操作。
③ Master根據本地數據庫中存儲的會話和鎖信息來構建服務器的內存狀態。
④ 至此,Master已經能夠處理客戶端的KeepAlive請求,但仍然無法處理其他會話相關的操作。
⑤ Master會發送一個Master故障切換事件給每一個會話,客戶端接收到這個事件后,會清空它的本地緩存,并警告上層應用程序可能已經丟失了別的事件,之后再向Master反饋應答。
⑥ 此時,Master會一直等待客戶端的應答,直到每一個會話都應答了這個切換事件。
⑦ Master接收了所有客戶端的應答之后,就能夠開始處理所有的請求操作。
⑧若客戶端使用了一個故障切換之間創建的句柄,Master會重新為其創建這個句柄的內存對象,并執行調用,如果該句柄在之前的Master周期中就已經被關閉了,那么它就不能在這個Master周期內再次被重建了,這一機制就確保了由于網絡原因使得Master接收到那些延遲或重發的網絡數據包也不會錯誤的重建一個已經關閉的句柄。
三、Paxos協議實現
Chubby服務端的基本架構大致分為三層
① 最底層是容錯日志系統(Fault-Tolerant Log),通過Paxos算法能夠保證集群所有機器上的日志完全一致,同時具備較好的容錯性。
② 日志層之上是Key-Value類型的容錯數據庫(Fault-Tolerant DB),其通過下層的日志來保證一致性和容錯性。
③ 存儲層之上的就是Chubby對外提供的分布式鎖服務和小文件存儲服務。
Paxos算法用于保證集群內各個副本節點的日志能夠保持一致,Chubby事務日志(Transaction Log)中的每一個Value對應Paxos算法中的一個Instance(對應Proposer),由于Chubby需要對外提供不斷的服務,因此事務日志會無限增長,于是在整個Chubby運行過程中,會存在多個Paxos Instance,同時,Chubby會為每個Paxos Instance都按序分配一個全局唯一的Instance編號,并將其順序寫入到事務日志中去。
在多Paxos Instance的模式下,為了提升算法執行的性能,就必須選舉出一個副本作為Paxos算法的主節點,以避免因為每一個Paxos Instance都提出提議而陷入多個Paxos Round并存的情況,同時,Paxos會保證在Master重啟或出現故障而進行切換的時候,允許出現短暫的多個Master共存卻不影響副本之間的一致性。
在Paxos中,每一個Paxos Instance都需要進行一輪或多輪的Prepare->Promise->Propose->Accept這樣完整的二階段請求過程來完成對一個提議值的選定,為了保證正確性的前提下盡可能地提高算法運行性能,可以讓多個Instance共用一套序號分配機制,并將Prepare->Promise合并為一個階段。具體做法如下:
① 當某個副本節點通過選舉成為Master后,就會使用新分配的編號N來廣播一個Prepare消息,該Prepare消息會被所有未達成一致的Instance和目前還未開始的Instance共用。
② 當Acceptor接收到Prepare消息后,必須對多個Instance同時做出回應,這通常可以通過將反饋信息封裝在一個數據包中來實現,假設最多允許K個Instance同時進行提議值的選定,那么:
-當前之多存在K個未達成一致的Instance,將這些未決的Instance各自最后接受的提議值封裝進一個數據包,并作為Promise消息返回。
-同時,判斷N是否大于當前Acceptor的highestPromisedNum值(當前已經接受的最大的提議編號值),如果大于,那么就標記這些未決Instance和所有未來的Instance的highestPromisedNum的值為N,這樣,這些未決Instance和所有未來Instance都不能再接受任何編號小于N的提議。
③ Master對所有未決Instance和所有未來Instance分別執行Propose->Accept階段的處理,如果Master能夠一直穩定運行的話,那么在接下來的算法運行過程中,就不再需要進行Prepare->Promise處理了。但是,一旦Master發現Acceptor返回了一個Reject消息,說明集群中存在另一個Master并且試圖使用更大的提議編號發送了Prepare消息,此時,當前Master就需要重新分配新的提議編號并再次進行Prepare->Promise階段的處理。
利用上述改進的Paxos算法,在Master穩定運行的情況下,只需要使用同一個編號來依次執行每一個Instance的Promise->Accept階段處理。
在集群的某臺機器在宕機重啟后,為了恢復機器的狀態,最簡單的辦法就是將已記錄的所有日志重新執行一遍,但是如果機器上的日志已經很多,則耗時長,因此需要定期對狀態機數據做一個數據快照并將其存入磁盤,然后就可以將數據快照點之前的事務日志清除。
在恢復過程中,會出現磁盤未損壞和損壞兩種情況,若未損壞,則通過磁盤上保存的數據庫快照和事務日志就可以恢復到之前的某個時間點的狀態,之后再向集群中其他正常運行的副本節點索取宕機后缺失的部分數據變更記錄即可;若磁盤損壞,就笑從其他副本節點索取全部的狀態數據。
四、總結
本部分詳細介紹了Chubby系統,并且對Paxos在Chubby中的使用也做了較為詳細的介紹,后面我們將會介紹開源分布式系統Zookeeper與Paxos的聯系。本部分理論知識較強(文字較多),需要慢慢研究才能理解其中的含義,謝謝各位園友的觀看~