閱讀464 返回首頁    go 阿裏雲 go 技術社區[雲棲]


如何基於Raft繞過​分布式算法一致性的那些痛?

本文根據DCOS聯盟第6期線上分享整理而成。

 

講師介紹  20170105104557455.jpg

姚雲

數人雲研發工程師

 

 

  • 目前負責Go語言分布式係統的相關研發,Linux社區成員,擅長Python,熟悉C#開發。

 

主題大綱:

1、點撥分布式係統和Raft算法要點

2、深入剖析Raft實現原理

3、幹貨:基於Raft的分布式係統實戰經驗

 

最近我們開源了一個運行在Mesos上的分布式調度器swan(https://github.com/Dataman-Cloud/swan),在研發過程中發現由於目前的分布式存儲並不能滿足我們所有需求,所以自己動手將Raft嵌入到swan中,以保證多節點之間的數據同步。在這個過程中我們積累了一些Raft相關的經驗和教訓,在這裏與大家分享。

 

一、關於分布式係統

 

分布式係統具有可擴展性和高可用性強等特點,被越來越多的應用到各個應用場景中,但是在分布式係統中,各個服務器之間數據的一致性一直是無法繞過的難題。

 

所謂一致性,它是指分布式係統中,多個服務器的狀態達成一致。但由於各種意外可能,某個服務器發生崩潰或變得不可靠,它就不能與其他服務器達成一致性狀態。這樣就需要一種Consensus協議,這個協議是為了確保容錯性,也就是即使係統中有一兩個服務器宕機,也不會影響其他的服務器正常提供服務。

 

在過去Paxos一直是分布式協議的標準,但是Paxos難於理解,更難以實現。較於Paxos,來自Stanford的新的分布式協議Raft更好用一些,它是一個為真實世界應用建立的協議,主要注重協議的可理解性和落地。

 

二、Raft如何在多個服務器之間實現數據的一致性

 

下麵簡單介紹一下Raft如何在多個服務器之間保證數據的一致性。為了達成一致性這個目標,首先Raft需要進行選舉,在Raft中,任何時候一個服務器可以扮演下麵角色之一:

 

  1. Leader:處理所有客戶端交互,日誌複製等,一般一次隻有一個Leader;

  2. Follower:類似選民,完全被動;

  3. Candidate候選人:類似Proposer律師,可以被選為一個新的領導人。參選者需要說服大多數選民(服務器)投票給他。

 

選舉的過程大概分為以下幾步:

  • 任何一個服務器都可以成為一個候選者Candidate,它向其他服務器Follower發出要求選舉自己的請求;

  • 其他服務器同意了,發出OK。注意如果在這個過程中,有一個Follower宕機,沒有收到請求選舉的要求,候選者可以自己選自己,隻要達到N/2 + 1 的大多數票,候選人還是可以成為Leader的;

  • 因此該候選者就成為了Leader領導人,它可以向選民也就是Follower們發出指令,比如進行日誌複製;

  • 如果一旦Leader宕機崩潰了,那麼Follower中會有一個成為候選者,發出選舉邀請;

  • Follower同意後,其成為Leader;

 

選出leader之後,所有的操作都需要在leader上進行,leader把所有的操作下發到集群中的其他服務器(follower),follower收到消息,完成操作commit之後需要向leader匯報狀態;如果leader處於不可用的狀態,則需要重新進行選舉。

 

為了與容錯方式達成一致性,Raft不要求所有的服務器100%達成一致,隻要超過半數的服務器達成一致就可以了,假設有N台服務器,N/2+1超過半數,也就是說一個3節點的Raft集群允許一個節點宕機,一個5節點的Raft集群可以允許2個節點宕機,所以為了更有效的利用服務器,一般Raft集群裏服務器的數量都是奇數,建議配置運行3或5節點的Raft集群, 最大限度的提高可用性, 而且不會犧牲很多性能。

 

三、實踐:如何自己動手寫一個內置Raft集群的分布式服務

 

下麵結合swan中的具體代碼,介紹如何啟動RaftNode,以及處理數據和有關Raft狀態的相關實踐,swan的分布式存儲的設計草圖如下:

 

20170105104606267.jpg

 

  • 如何啟動一個RaftNode

 

先分享下如何自己動手寫一個內置Raft集群的分布式服務,由於是使用Go語言開發,所以選用etcd/raft。

 

在Raft集群中最重要的概念就是一個RaftNode,多個互相連通的RaftNode組成了一個RaftCluster,可以通過以下代碼快速啟動/重啟一個RaftNode。

 

20170105105208345.jpg

 

該方法是在RaftNode啟動時已經知道集群未來的規模,將集群的其他節點ID寫入到配置中,如果要實現節點的Auto Join,則需要在Start的Peers參數處傳入空值;另外,如果服務之前已經運行了一段時間,在啟動服務時就需要從WAL中讀取上一次服務停止時的狀態和數據,然後在這些數據的基礎上繼續運行。

RaftNode啟動成功後,各個節點之間的通信需要借助一個Transport,同樣還是使用etcd提供的httptransport ,當然,也可以基於grpc實現所有的通信方法(參考swarmkit的實現),以下是啟動transport的代碼實現。

 

20170105105223813.jpg

 

代碼中先構造出一個transport實例,然後將集群中其餘節點的訪問地址添加到transport中,需通過地址來進行節點之間的互相通信,最後啟動一個TCPListener來接收和處理消息。

 

至此,一個簡單的RaftNode就算啟動起來了,依據同樣的方式再啟動其他兩個節點,3節點的RaftCluster也運行起來了,不過,隻是服務啟動成功還遠遠不夠。

 

  • 數據提交及事件處理

 

1、 將數據提交到RaftNode,使用Propose方法將數據提交到leader節點

注:隻能講數據Propose到leader節點, 因為隻有leader才有能力讓follower複製自己的操作。

 

2、leader節點接收到數據會根據集群的狀態判斷是否已經能接受數據的提交,leader確定能接收數據後會負責將數據發送到follower。

 

注:如果是集群節點的改動需要調用ProposeConfChange方法。

 

3、RaftNode節點數據提交是一個異步的過程,通過Propose方法往RaftNode中提交數據,而RaftNode則在經過一係列的狀態判斷從另一個線程中通過 Ready()方法通知,此外,集群狀態的變化也會通過這個channel來通知,所以當Propose數據之後不知道數據是否提交成功,如果服務的數據有高可用性的要求,這裏可能需要進行額外的處理,將異步的提交變成同步的(可以參考swarmkit ProposeValue)。

 

注:由於數據提交隻有一個Propose接口,所以需要對不同的數據進行不同的操作,提前定義好對哪些數據(比如app, cluster)進行什麼樣的操作(比如Add, Update, Delete),這種情況下就要先在Propose的對象裏加上數據和操作之後再進行序列化。
 

著重注意從 Ready()中收到數據之後的處理,先看代碼:

 

20170105105230928.jpg

 

以上代碼可以看到最主要的三個處理:

 

  1. 檢查RaftCluster的狀態是否已經改變,如果該節點已經從follower升級成為leader,需要通知外部的服務這個變化,以便外部服務做出相應的調整

  2. publishEntries,其實就是講rd。CommittedEntries持久化或者存到相應的地方,可以認為這些CommittedEntries就是已經被RaftCluster接收了的可靠消息。

 

關於節點狀態的變化,需要在外部服務中監聽RaftNode的leadershipChange event,由於RaftNode隻有在leader上才能Propose數據(相當於寫操作),所以cluster中的所有節點地位並不是對等的,比如有的提交數據的功能可能需要等RaftCluster leader election完成後再leader上啟動;至於其他的follower節點如果對外想提供和leader一樣的服務,則需要自己實現一個proxy,將請求proxy到leader節點, 或者通過grpc來遠程調用leader上相應的接口,相關的代碼如下所示:

 

20170105105236357.jpg

 

3、以上關於接受Ready()消息的處理代碼中,除了之前提到的兩點外,剩下的就是關於WAL(Write Ahead Logging 預寫式日誌多用於實現數據庫原子性事務)和snapshot的相關處理了。

 

通過代碼看到Ready()裏收到的每一條消息都會先調用wal。Save,由wal。Save將相關的信息保存到WAL中,當操作積累到一定的數量時,則會通過saveSnapshot將目前的全量數據(包括狀態和已經接受到的所有數據)保存到snapshot中, 然後調用wal。ReleaseLockTo釋放掉已經存入snapshot中的操作。

 

這點與許多數據庫實現的WAL原理( WAL機製的原理,是修改並不寫入到數據庫文件中,而是寫入到另外一個稱為WAL的文件中;如果事務失敗,WAL中的記錄會被忽略,撤銷修改;如果事務成功,它將在隨後的某個時間被寫回到數據庫文件中,提交修改)是一樣的。

 

注:最後別忘了調用RaftNode。Advance()。

 

  • 關於日誌壓縮

 

之前提到WAL會將RaftNode的每次操作記錄下來,而且在RaftCluster中leader不刪除日誌,僅追加日誌,因此隨著係統的持續運行,WAL中內容越來越多,導致日誌重放時間增長,係統可用性下降。快照(Snapshot)是用於“日誌壓縮”最常見的手段,Raft也不例外。

 

具體做法如下所示(圖片來自網絡):

 

20170105105242348.jpg

 

與Raft其它操作Leader-Based不同,snapshot是由各個節點獨立生成的。除了日誌壓縮的功能,snapshot還可以用於同步狀態。

 

四、總結

 

以上僅僅隻是跑起來一個簡單的RaftCluster,關於服務怎麼和嵌入的RaftCluster結合,以及leader切換,節點的增刪等等還是有不少的問題等著我們一起去探討和解決。有關Raft的更多資料可以參考:

 

  • Raft動畫演示 

    https://thesecretlivesofdata.com/raft/ 

  • Raft論文

    https://ramcloud.stanford.edu/wiki/download/attachments/11370504/raft.pdf

 

Q&A  

 

Q1:拋磚引玉問一下,swankit 的典型部署是跨數據中心部署嗎?如果是的話,raft 的心跳時間你們的經驗值是? 在網絡抖動的情況下,如何避免 follower 頻繁發起新 Term 的選舉?

A1:由於目前我們的項目還沒有大規模的應用到生產所以心跳和選舉的間隔參考的swarmkit和etcd分別設置的1秒和10秒。目前我們的實現方式是不支持跨數據中心的部署的。至於如何避免follower頻繁發起新的term選舉目前還隻有設置選舉間隔這一條了。

 

Q2:Raft和Multi-Paxos的區別?

A2:Raft可以說是從Paxos 中發展出來的,答:簡單來說就是 Raft 在理解和實現都要比 Multi Paxos 要簡單,主要體現在兩階段的限製。Paxos 出現的太早了,嚴格的證明,非常的學術,有興趣的可以去看 Lamport 老的論文,包括最早那篇和 make simple,make live。我也不敢說理解透徹,隻是了解到穀歌的 Chubby 是基於Multi Paxos 實現的。至於實踐上的區別可以參考https://bingotree.cn/?p=614。

 

Q3:第一張圖裏,顯示了3個raft節點,一個leader,兩個follower,那律師節點是隨機的麼?另外,圖中顯示,心跳是兩兩節點完成的,而不是用連到一個交換機,這種方式會不會有什麼問題?

A3:由於在Raft節點中任何一個節點都可以成為候選者向其他節點請求投票所以需要 兩兩之間互相通信, 所以如果不是連到同一交換機導致節點之間無法互相通信是有問題的。

 

Q4:請問這次采用了哪個raft的開源實現?還是自己實現了raft協議?

A4:用的是 coreos 的 etcd/raft 庫 https://github.com/coreos/etcd/tree/master/raft。

原文發布時間為:2017-01-05

本文來自雲棲社區合作夥伴DBAplus

最後更新:2017-05-13 08:43:33

  上一篇:go  facebook\微博 like場景 數據庫設計與性能壓測
  下一篇:go  騰訊互娛架構師談遊戲服務器緩存係統怎麼造