實現Raft協議:Part 0 - 介紹

翻譯自Eli Bendersky系列博客,已得到原做者受權。git

本文是系列文章中的序言,本系列文章旨在介紹Raft分佈式一致性協議及其Go語言實現。文章的完整列表以下:github

Raft是一個相對較新的算法(2014),可是已經在業界取到了普遍的應用。最知名的案例應該就是Kubernetes,其中的分佈式鍵值存儲組件etcd就依賴了Raft協議。算法

本系列文章的寫做目的,在於描述Raft協議的一個功能完備且通過嚴格測試的實現方式,並提供一些Raft工做方式的直觀理解。這並非您學習Raft協議的惟一途徑。我假定您至少讀過Raft論文; 此外,也強烈建議您花時間仔細研究Raft網站上的資源——觀看創做者的一兩次演講,鼓搗一下算法的可視化工具,瀏覽Ongaro的博士論文以學習更多細節,等等。數據庫

不要期望能用一天時間就徹底掌握Raft協議。儘管Raft設計得比Paxos更易於理解,但Raft算法仍然是至關複雜的。 它要解決的問題(分佈式一致性)是一個難題,所以解決方案天然也不會太簡單。編程

複製狀態機

分佈式一致性算法能夠認爲是在解決跨服務器複製一個肯定性狀態機的問題。這裏的狀態機一詞能夠用來表示任意服務。 畢竟,狀態機是計算機科學的基礎之一,並且一切事物均可以用狀態機來表示。 數據庫、文件服務器、鎖服務器等均可以被看做是複雜的狀態機。緩存

考慮用狀態機來表明一些服務,有多個客戶端能夠鏈接到它,這些客戶端會發出請求,並指望獲得響應: 安全

Single state machine with two clients

只要運行狀態機的服務器是穩定可靠的,這個系統就能夠正常工做。若是服務器崩潰的話,咱們的服務也就不可用了,而這是不能接受的。一般,咱們系統的可靠性取決於其運行的服務器。服務器

提升服務可靠性的一種經常使用方法就是複製。 咱們能夠在不一樣的服務器上運行服務的多個實例。 這樣就建立了一個服務器集羣,這些服務器協同工做以提供服務,而且其中任何一臺服務器的崩潰都不會致使服務中斷。 服務器間相互隔離[1]能夠摒除一些同時影響多臺服務器的常見故障,從而進一步提升系統可靠性。網絡

客戶端不會再鏈接單個提供服務的機器,而是會鏈接整個集羣。此外,構成該集羣的服務副本之間必須相互通訊,以期正確地複製狀態:併發

Replicated state machine with two clients

上圖中的每一個狀態機都是服務的一個副本。其思想就是,全部的狀態機同步運行,從客戶端請求中獲取相同的輸入,並執行相同的狀態轉換。這樣就保證即便集羣有一些服務器出現故障,也會返回相同的結果給客戶端。Raft就是實現這個目的的一種算法。

如今正好澄清一些後文中會頻繁使用的術語:

  • 服務(Service):是咱們將實現的分佈式系統中的邏輯任務,好比說,一個鍵值數據庫。
  • 服務器(Server)副本(Replica):在隔離的機器上運行的一個Raft服務實例,能夠經過網絡鏈接其它副本或者客戶端。
  • 集羣(Cluster):一組協做實現分佈式服務的Raft服務器,典型的集羣規模是3或5。

一致性模塊和Raft日誌

如今咱們來看一下上圖展現的其中一個狀態機。Raft做爲一個通用的算法,並不關心服務是如何根據狀態機實現的。它的目標是可靠、準確地記錄並重現狀態機接受的輸入序列(Raft術語中也稱爲指令),給定初始狀態和全部輸入,就能夠徹底準確地重放狀態機。能夠換個角度理解:若是咱們有相同狀態機的兩個獨立的副本,而且從相同的起始狀態開始向其發出一樣的輸入序列,那麼兩個副本最終會停留在相同的狀態,而且會產生相同的輸出。

這裏是使用Raft的通常服務的結構:

Raft consensus module and log connected to state machine

這些組件的詳細描述以下:

  • 狀態機與咱們前面所說的相同。它表明任意一種服務:在介紹Raft時經常使用的例子就是鍵值存儲。
  • 日誌(Log)是存儲客戶端發出的全部指令(輸入)的位置。這些指令不會直接應用於狀態機,相反,只有當它們被成功複製到大多數服務器時,Raft算法纔會提交這些指令。此外。日誌是持久的——它保存在抗系統崩潰的穩定存儲中,而且在系統崩潰後能夠用於重放狀態機。
  • 一致性模塊是Raft算法的核心。它會接受客戶端的指令,確保它們保存在日誌中,將指令複製到集羣中的其它Raft副本中(上圖中的綠色箭頭),而且在肯定安全的時候將指令提交到狀態機中。提交到狀態機會將實際修改通知到客戶端。

領導者和追隨者

Raft使用的是強領導模型,其中集羣中的一個副本做爲領導者,其它副本都做爲追隨者。領導者負責接受客戶的請求,複製指令給追隨者,並返回響應給客戶端。

正常操做狀況下,追隨者的目的就是簡單地複製領導者的日誌。一旦領導者出現故障或者網絡隔斷,會有一個追隨者接管領導權,所以服務仍然是可用的。

這個模型是有利有弊的。一個重要的優勢就是簡單,數據老是vong領導者流向追隨者,並且只有領導者響應客戶端的請求。這個設計使得Raft協議更容易被分析、測試和調試。缺點就是性能——由於集羣中只有一個服務器與客戶端進行交互,當客戶端請求激增時這會變成系統的瓶頸。對於這個問題,答案一般是:Raft協議不適用於大流量服務。Raft協議更適用於那些以犧牲可用性爲代價來保證一致性的低流量服務——咱們在容錯部分會從新討論這一點。

客戶端交互

前面寫過,「客戶端不會再鏈接單個提供服務的機器,而是會鏈接整個集羣」,這句話是什麼含義呢?集羣就是一組經過網絡互連的服務器,因此你如何鏈接「整個集羣」呢?

答案很簡單:

  • 在訪問Raft集羣時,客戶端知道集羣中副本的網絡地址。至於它如何知道(例如經過某種服務發現機制)不在本文的討論範圍以內。
  • 客戶端一開始會發請求到任意副本,若是這個副本是領導者,它會當即接受請求,並且客戶端也會等待完整的響應,此後,客戶端會記住這個副本是領導者,之後就沒必要再次搜索領導者(除非遇到某些故障,如領導者崩潰)。
  • 若是副本表示本身不是領導者,客戶端會嘗試鏈接另外一個副本。這裏能夠進行優化,由追隨者副本直接告訴客戶端哪個副本是領導者。由於副本間是一直在相互通訊的,因此一般知道正確答案,這樣能夠節省客戶端的猜想時間。
  • 還有一種狀況下客戶端會意識到本身鏈接的不是領導者,那就是它的請求在一段超時時間內沒有提交成功。這可能意味着它鏈接的副本實際上不是領導者(即便它認爲本身是)——它可能跟其它Raft服務器間出現了分割。當超時時間耗盡後,客戶端會從新搜索其它的領導者。

第三點中提到的優化在多數狀況下都不是必要的。一般來講,在Raft環境中區分「正常運行」和「異常狀況」是頗有用的。一個服務一般有99.9%的時間都是「正常運行」的,此時,客戶端知道領導者是哪個,由於它們在第一次鏈接服務的時候就緩存了這些信息。故障場景下確定會形成混亂(下一節會討論更多細節),可是也只是很短的時間。咱們在下一篇文章中也會詳細介紹,Raft集羣可以很快地從機器臨時故障或網絡分區問題中恢復——大多數狀況下恢復間隔不到1秒鐘。當新的領導者聲明領導權以及客戶端查找具體的領導者副本時,可能會出現短暫的不可用狀態,可是以後集羣會恢復到「正常運行模式」。

Raft容錯機制和CAP理論

咱們來看一下三個Raft副本的示意圖,此次不須要鏈接客戶端:

Replicated state machine not showing clients

在這個集羣中,咱們能夠預見什麼類型的故障呢?

現代計算機中的每一個組件均可能會出現故障,可是爲了方便討論,咱們把Raft實例中運行的服務器看做一個原子單元。這樣的話,咱們會面臨兩大類的故障:

  1. 服務器崩潰,其中一個服務器在一段時間內中止響應全部的網絡請求。崩潰的服務器一般會被重啓,並在短暫的中斷後從新上線。
  2. 網絡分區,因爲網絡設備或傳輸介質問題,致使一個或多個服務器與其它服務器和/或客戶端斷開鏈接。

從服務器A的角度來講,其與服務器B之間相互通訊,對於服務器B的故障與A、B間的網絡分區是沒法區分的。這兩種狀況的表現是相同的——A接受不到任何B的信息及響應。可是,從系統的角度來講,網絡分區的影響更大,由於它們會同時影響多臺服務器。在本系列的下一部分,咱們會討論網絡分區致使的一些複雜場景。

爲了優雅地應對任意網絡分區和服務器故障問題,Raft要求集羣中的大多數服務器是正常啓動的,並且在任意指定時刻均可覺得領導者所用。若是有3臺服務器,Raft能夠容許1臺機器故障,對於5臺服務器的集羣,能夠容許2臺機器故障; 對於2N+1臺服務器,能夠容許N臺服務器出現故障。

這就引出了CAP理論,其實際結論就是,當存在網絡分區(實際應用中難以免的一部分)時,咱們必須仔細權衡可用性一致性

在這個權衡中,Raft堅決地站在一致性陣營。其設計理念就是防止集羣可能達到不一致狀態的狀況,在這種狀況下,不一樣的客戶端可能會獲得不一樣的響應。爲此Raft犧牲了部分可用性。

我前面也簡單提過,Raft不是爲高吞吐量、細粒度的服務設計的。客戶端的每個請求都會觸發一系列工做——Raft副本間通訊,以期把指令複製到大多數服務並持久化;這些都發生在客戶端獲得迴應以前。

舉例來講,你確定不會設計一個全部客戶端請求都通過Raft的複製數據庫,這樣太慢了。Raft更適合於粗粒度的分佈式原語——如實現鎖服務器,爲更高級別的協議選舉領導者,在分佈式系統中複製關鍵配置數據,等等。

爲何選Go

本系列中介紹的Raft實現是用Go語言編寫的。在我看來,Go語言有三大優點,也是本系列及通用的網絡服務選擇Go做爲實現語言的緣由:

  1. 併發 :Raft這類算法在本質上是徹底並行的,每一個副本要執行持續不斷的操做(指令),爲定時事件運行定時器,還必須響應其它副本和客戶端的請求。我以前寫過爲何我認爲Go是編寫這類代碼的理想語言
  2. 標準庫:Go語言擁有一個強大的工業級標準庫,能夠輕鬆編寫複雜的網絡服務器,而不須要導入和學習任何第三方庫。特別是在Raft中,須要面對的第一個問題就是「如何在副本之間發生消息?」,不少人會陷入設計協議和序列化的困境中,或者使用繁重的第三方庫。Go語言中有net/rpc,這是一個足以應對此類任務的解決方案,能夠快速使用並且不須要引入 (依賴)。
  3. 簡單:即便不考慮編程語言,實現分佈式一致性就已經足夠複雜了。使用任何語言均可以寫出清晰、簡單的代碼,可是在Go語言中,這是默認的習慣寫法,這門語言在每一個可能的層面上都反對代碼的複雜性。

下一步

感謝您能讀到這裏!若是您以爲有哪些地方我能夠寫得更好的,請告訴我。儘管Raft在概念上可能看起來很簡單,可是一旦咱們編碼實現,仍是會遇到不少問題。本系列的後續部分將介紹關於Raft算法不一樣方面的更多細節。

如今你應該已經準備好進入第1部分,咱們開始實現Raft吧。

譯者注

本系列文章經過使用Golang實現Raft協議,不只直觀解釋了Raft協議中的一些難點,對於Go語言併發編程的學習也有很大的幫助。

本人在讀完原博客以後以爲受益不淺,在徵求做者贊成以後,將本系列博客翻譯爲中文並分享給你們,但願對Go或者Raft有興趣的同窗都可以有所收穫。

強烈建議讀者在看完一篇文章以後,能夠執行做者代碼中的測試用例,對照測試輸出日誌鞏固一下對Raft協議的理解。

我在學習過程當中,fork了原做者的代碼,在原基礎上添加了中文註釋,也添加了測試用例的輸出結果。對於不方便執行測試的讀者,能夠直接在其中查看測試輸出日誌。

需者自取,Github地址:github.com/GuoYaxiang/…


  1. 舉例來講,能夠將它們放在不一樣的機架中,或鏈接到不一樣的電源,甚至放置在不一樣的建築物中。 大型公司提供的真正重要的服務一般是在全球範圍內複製的,副本會分佈在不一樣的區域。 ↩︎

相關文章
相關標籤/搜索