翻譯自Eli Bendersky的系列博客,已得到原做者受權。git
本文是系列文章中的序言,本系列文章旨在介紹Raft分佈式一致性協議及其Go語言實現。文章的完整列表以下:github
Raft是一個相對較新的算法(2014),可是已經在業界取到了普遍的應用。最知名的案例應該就是Kubernetes,其中的分佈式鍵值存儲組件etcd就依賴了Raft協議。算法
本系列文章的寫做目的,在於描述Raft協議的一個功能完備且通過嚴格測試的實現方式,並提供一些Raft工做方式的直觀理解。這並非您學習Raft協議的惟一途徑。我假定您至少讀過Raft論文; 此外,也強烈建議您花時間仔細研究Raft網站上的資源——觀看創做者的一兩次演講,鼓搗一下算法的可視化工具,瀏覽Ongaro的博士論文以學習更多細節,等等。數據庫
不要期望能用一天時間就徹底掌握Raft協議。儘管Raft設計得比Paxos更易於理解,但Raft算法仍然是至關複雜的。 它要解決的問題(分佈式一致性)是一個難題,所以解決方案天然也不會太簡單。編程
分佈式一致性算法能夠認爲是在解決跨服務器複製一個肯定性狀態機的問題。這裏的狀態機一詞能夠用來表示任意服務。 畢竟,狀態機是計算機科學的基礎之一,並且一切事物均可以用狀態機來表示。 數據庫、文件服務器、鎖服務器等均可以被看做是複雜的狀態機。緩存
考慮用狀態機來表明一些服務,有多個客戶端能夠鏈接到它,這些客戶端會發出請求,並指望獲得響應: 安全
只要運行狀態機的服務器是穩定可靠的,這個系統就能夠正常工做。若是服務器崩潰的話,咱們的服務也就不可用了,而這是不能接受的。一般,咱們系統的可靠性取決於其運行的服務器。服務器
提升服務可靠性的一種經常使用方法就是複製。 咱們能夠在不一樣的服務器上運行服務的多個實例。 這樣就建立了一個服務器集羣,這些服務器協同工做以提供服務,而且其中任何一臺服務器的崩潰都不會致使服務中斷。 服務器間相互隔離[1]能夠摒除一些同時影響多臺服務器的常見故障,從而進一步提升系統可靠性。網絡
客戶端不會再鏈接單個提供服務的機器,而是會鏈接整個集羣。此外,構成該集羣的服務副本之間必須相互通訊,以期正確地複製狀態:併發
上圖中的每一個狀態機都是服務的一個副本。其思想就是,全部的狀態機同步運行,從客戶端請求中獲取相同的輸入,並執行相同的狀態轉換。這樣就保證即便集羣有一些服務器出現故障,也會返回相同的結果給客戶端。Raft就是實現這個目的的一種算法。
如今正好澄清一些後文中會頻繁使用的術語:
如今咱們來看一下上圖展現的其中一個狀態機。Raft做爲一個通用的算法,並不關心服務是如何根據狀態機實現的。它的目標是可靠、準確地記錄並重現狀態機接受的輸入序列(Raft術語中也稱爲指令),給定初始狀態和全部輸入,就能夠徹底準確地重放狀態機。能夠換個角度理解:若是咱們有相同狀態機的兩個獨立的副本,而且從相同的起始狀態開始向其發出一樣的輸入序列,那麼兩個副本最終會停留在相同的狀態,而且會產生相同的輸出。
這裏是使用Raft的通常服務的結構:
這些組件的詳細描述以下:
Raft使用的是強領導模型,其中集羣中的一個副本做爲領導者,其它副本都做爲追隨者。領導者負責接受客戶的請求,複製指令給追隨者,並返回響應給客戶端。
正常操做狀況下,追隨者的目的就是簡單地複製領導者的日誌。一旦領導者出現故障或者網絡隔斷,會有一個追隨者接管領導權,所以服務仍然是可用的。
這個模型是有利有弊的。一個重要的優勢就是簡單,數據老是vong領導者流向追隨者,並且只有領導者響應客戶端的請求。這個設計使得Raft協議更容易被分析、測試和調試。缺點就是性能——由於集羣中只有一個服務器與客戶端進行交互,當客戶端請求激增時這會變成系統的瓶頸。對於這個問題,答案一般是:Raft協議不適用於大流量服務。Raft協議更適用於那些以犧牲可用性爲代價來保證一致性的低流量服務——咱們在容錯部分會從新討論這一點。
前面寫過,「客戶端不會再鏈接單個提供服務的機器,而是會鏈接整個集羣」,這句話是什麼含義呢?集羣就是一組經過網絡互連的服務器,因此你如何鏈接「整個集羣」呢?
答案很簡單:
第三點中提到的優化在多數狀況下都不是必要的。一般來講,在Raft環境中區分「正常運行」和「異常狀況」是頗有用的。一個服務一般有99.9%的時間都是「正常運行」的,此時,客戶端知道領導者是哪個,由於它們在第一次鏈接服務的時候就緩存了這些信息。故障場景下確定會形成混亂(下一節會討論更多細節),可是也只是很短的時間。咱們在下一篇文章中也會詳細介紹,Raft集羣可以很快地從機器臨時故障或網絡分區問題中恢復——大多數狀況下恢復間隔不到1秒鐘。當新的領導者聲明領導權以及客戶端查找具體的領導者副本時,可能會出現短暫的不可用狀態,可是以後集羣會恢復到「正常運行模式」。
咱們來看一下三個Raft副本的示意圖,此次不須要鏈接客戶端:
在這個集羣中,咱們能夠預見什麼類型的故障呢?
現代計算機中的每一個組件均可能會出現故障,可是爲了方便討論,咱們把Raft實例中運行的服務器看做一個原子單元。這樣的話,咱們會面臨兩大類的故障:
從服務器A的角度來講,其與服務器B之間相互通訊,對於服務器B的故障與A、B間的網絡分區是沒法區分的。這兩種狀況的表現是相同的——A接受不到任何B的信息及響應。可是,從系統的角度來講,網絡分區的影響更大,由於它們會同時影響多臺服務器。在本系列的下一部分,咱們會討論網絡分區致使的一些複雜場景。
爲了優雅地應對任意網絡分區和服務器故障問題,Raft要求集羣中的大多數服務器是正常啓動的,並且在任意指定時刻均可覺得領導者所用。若是有3臺服務器,Raft能夠容許1臺機器故障,對於5臺服務器的集羣,能夠容許2臺機器故障; 對於2N+1
臺服務器,能夠容許N
臺服務器出現故障。
這就引出了CAP理論,其實際結論就是,當存在網絡分區(實際應用中難以免的一部分)時,咱們必須仔細權衡可用性和一致性。
在這個權衡中,Raft堅決地站在一致性陣營。其設計理念就是防止集羣可能達到不一致狀態的狀況,在這種狀況下,不一樣的客戶端可能會獲得不一樣的響應。爲此Raft犧牲了部分可用性。
我前面也簡單提過,Raft不是爲高吞吐量、細粒度的服務設計的。客戶端的每個請求都會觸發一系列工做——Raft副本間通訊,以期把指令複製到大多數服務並持久化;這些都發生在客戶端獲得迴應以前。
舉例來講,你確定不會設計一個全部客戶端請求都通過Raft的複製數據庫,這樣太慢了。Raft更適合於粗粒度的分佈式原語——如實現鎖服務器,爲更高級別的協議選舉領導者,在分佈式系統中複製關鍵配置數據,等等。
本系列中介紹的Raft實現是用Go語言編寫的。在我看來,Go語言有三大優點,也是本系列及通用的網絡服務選擇Go做爲實現語言的緣由:
net/rpc
,這是一個足以應對此類任務的解決方案,能夠快速使用並且不須要引入 (依賴)。感謝您能讀到這裏!若是您以爲有哪些地方我能夠寫得更好的,請告訴我。儘管Raft在概念上可能看起來很簡單,可是一旦咱們編碼實現,仍是會遇到不少問題。本系列的後續部分將介紹關於Raft算法不一樣方面的更多細節。
如今你應該已經準備好進入第1部分,咱們開始實現Raft吧。
本系列文章經過使用Golang實現Raft協議,不只直觀解釋了Raft協議中的一些難點,對於Go語言併發編程的學習也有很大的幫助。
本人在讀完原博客以後以爲受益不淺,在徵求做者贊成以後,將本系列博客翻譯爲中文並分享給你們,但願對Go或者Raft有興趣的同窗都可以有所收穫。
強烈建議讀者在看完一篇文章以後,能夠執行做者代碼中的測試用例,對照測試輸出日誌鞏固一下對Raft協議的理解。
我在學習過程當中,fork了原做者的代碼,在原基礎上添加了中文註釋,也添加了測試用例的輸出結果。對於不方便執行測試的讀者,能夠直接在其中查看測試輸出日誌。
需者自取,Github地址:github.com/GuoYaxiang/…
舉例來講,能夠將它們放在不一樣的機架中,或鏈接到不一樣的電源,甚至放置在不一樣的建築物中。 大型公司提供的真正重要的服務一般是在全球範圍內複製的,副本會分佈在不一樣的區域。 ↩︎