Go 實現 Raft 第一篇:介紹

CrazyZard發表於2020-03-20

本篇文章為Raft系列文章中的第一篇,Raft的介紹。整個系列文章描述了Raft分散式共識演算法及其在Go中的完整實現。

Raft是一種相對較新的演算法(2014),但是它在業界已經被大量使用。最為大家所熟知的當屬K8s,它依賴於Raft通過etcd分散式鍵值儲存。

本系列文章的目的是描述Raft的功能齊全且經過嚴格測試的實現,並捎帶介紹Raft的工作方式。我們假設讀者至少了解過Raft相關文章。

不要指望在一天內完全掌握Raft。儘管它的設計比Paxos更易於理解,但Raft仍然相當複雜。它要解決的問題-分散式共識-是一個難題,因此解決方案的複雜性自然有一個下限。

分散式共識演算法可以看作是解決跨多個伺服器複製確定性狀態機的問題。狀態機一詞用來表示任意服務;畢竟,狀態機是電腦科學的基礎之一,並且一切都可以用它們來表示。資料庫,檔案伺服器,鎖伺服器等都可以被認為是複雜的狀態機。

考慮一些由狀態機表示服務。多個客戶端可以連線到它併發出請求,並期望得到響應:

Go實現Raft第一篇:介紹

只要執行狀態機的伺服器是可靠的,系統就可以正常工作。如果伺服器崩潰,我們的服務將不可用,這可能是不可接受的。通常,我們系統的可靠性取決於執行它的單個伺服器。

提高服務可靠性的一種常見方法是通過複製。我們可以在不同的伺服器上執行服務的多個例項。這樣就建立了一個叢集,這些伺服器可以協同工作以提供服務,並且任何一臺伺服器崩潰都不應導致該服務中斷。通過消除會同時影響多臺伺服器的常見故障模式,將伺服器彼此隔離進一步提高了可靠性。

客戶端將跟整個叢集請求服務,而不是單個伺服器來執行服務。此外,組成叢集的服務副本必須在它們之間進行通訊以正確複製狀態:

Go實現Raft第一篇:介紹

圖中的每個狀態機都是服務的副本。其思想是所有狀態機都以鎖步的方式執行,從客戶端請求中獲取相同的輸入並執行相同的狀態轉換。這樣可以確保即使某些伺服器出現故障,它們也可以將相同的結果返回給客戶端。Raft就是實現此目的的演算法。

介紹一些相關名詞:

  • 服務:是我們正在實現的分散式系統的邏輯任務。例如,鍵值資料庫。

  • 伺服器或副本:一個啟用raft的服務例項,它執行在一臺與其他副本和客戶端有網路連線的隔離機器上。

  • 叢集:一組Raft伺服器進行協作以實現分散式服務。典型的群集大小為3或5。

作為一種通用演算法,Raft並沒有規定如何使用狀態機實現服務。它旨在實現的功能是可靠,確定性地記錄和再現狀態機的輸入序列(在Raft中也稱為命令)。給定初始狀態和所有輸入,就可以完全精確地重放狀態機。另一種思考方法:如果我們從同一狀態機獲取兩個單獨的副本,並從相同的初始狀態開始為它們提供相同的輸入序列,則狀態機將以相同的狀態結束併產生相同的輸出。

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

Go實現Raft第一篇:介紹

關於該元件的更多細節:

  • 狀態機與我們上面看到的相同。它表示任意服務;在介紹Raft時,鍵值儲存是一個常見例子。

  • 日誌是儲存客戶端發出的所有命令(輸入)的地方。命令不直接應用於狀態機;相反,當它們已成功複製到大多數伺服器時,Raft將應用它們。而且,該日誌是永續性的——它儲存在穩定的儲存中,可以在崩潰後倖免,並且可以用於在崩潰後回放狀態機。

  • 共識模組是Raft演算法的核心。它接受來自客戶端的命令,確保將它們儲存在日誌中,與叢集中的其他Raft副本一起復制它們(與上圖中的綠色箭頭相同),並在確信安全時將它們提交給狀態機。提交到狀態機後會將實際更改通知客戶。

Raft使用了一個強大的領導模型,其中叢集中的一個副本充當領導者,其他副本充當追隨者。領導者負責根據客戶的請求採取行動,將命令複製到追隨者,並將響應返回給客戶。

在正常操作期間,追隨者的目標是簡單地複製領導者的日誌。如果領導者發生故障或網路分割槽,則一個追隨者可以接管領導權,因此該服務仍然可用。

該模型有其優缺點。一個重要的優點是簡單。資料總是從領導者流向跟隨者,只有領導者才能響應客戶請求。這使得Raft叢集更容易分析、測試和除錯。一個缺點是效能——因為叢集中只有一臺伺服器與客戶機通訊,這可能成為客戶機活動激增時的瓶頸。答案通常是:Raft不應該用於高流量的服務。它更適合於一致性非常重要的低流量場景,但可能會犧牲可用性——我們將在容錯一節中介紹。

前面說過:客戶端將和整個叢集通訊,而不是和單個伺服器通訊來執行服務。什麼意思呢?叢集只是通過網路連線的一組伺服器,那麼將如何和整個叢集通訊?

答案很簡單:

  • 在使用Raft叢集時,客戶端知道叢集副本的網路地址。

  • 客戶端最初向任意副本傳送請求。如果該副本是領導者,它將立即接受請求,並且客戶端將等待完整的響應。此後,客戶端會記住該副本是領導者,而不必再次搜尋它(直到出現某些故障,例如領導者崩潰)。

  • 如果副本表示不是領導者,則客戶端將嘗試另一個副本。此處可能的優化是,跟隨者副本可以告訴客戶端哪個其他副本是領導者。由於副本之間不斷進行通訊,因此通常知道正確的答案。這樣可以為客戶端節省一些猜測的時間。在另一種情況下,客戶端可能意識到與其通訊的副本不是領導者,如果在一定的超時時間內未提交其請求。這可能意味著它通訊的副本實際上不是領導者(即使它仍然認為是副本)——可能已經從其他Raft伺服器中被分隔出來了。超時結束後,客戶端將繼續尋找其他領導者。

在多數情況下,第三點中提到的優化是不必要的。通常,在Raft中區分“正常執行”和“故障情況”很有用。很典型的服務將花費其99.9%的時間用於“正常執行”,該情況下,客戶知道領導者是誰,因為首次跟該服務通訊時就快取了此資訊。故障場景——我們將在下一節中進行詳細討論——肯定會造成混亂,但只是一小段時間。正如我們將在下一篇文章中詳細瞭解的那樣,一個Raft叢集將很快地從伺服器的臨時故障或網路分割槽中恢復——在大多數情況下,恢復間隔只有一秒鐘。當新的領導者宣告其領導權並且客戶找到它是哪臺伺服器時,將會出現短暫的不可用狀態,但是之後它將返回到“正常操作模式”。

讓我們回顧一下這次沒有連線客戶端的三個Raft副本的示意圖:

Go實現Raft第一篇:介紹

在叢集中,我們可以預料到哪些故障?

現代計算機中的每個元件都可能發生故障,但是為了使討論更加容易,我們將執行Raft例項的伺服器視為原子單元。這給我們帶來了兩種主要的失敗型別:

  1. 伺服器崩潰,其中一臺伺服器在一段時間內停止響應所有網路流量。崩潰的伺服器通常會重新啟動,並可能在短暫中斷後恢復聯機。

  2. 一種網路分割槽,其中一個或多個伺服器由於網路裝置或傳輸介質的問題而與其他伺服器和/或客戶端斷開連線。

從伺服器A與伺服器B進行通訊的角度來看,B崩潰與A和B之間的網路分割槽是無法區分的。它們都以相同的方式表現出來——A停止接收來自B的任何訊息或響應。在系統級看來,網路分割槽要隱蔽得多,因為它們會同時影響多臺伺服器。在本系列的下一部分中,我們將介紹一些由於分割槽而引起的棘手的情況。

為了能夠優雅地處理任意網路分割槽和伺服器崩潰,Raft要求群集中的大多數伺服器都可以啟動,並且領導者可以在任何給定的時刻使用它來取得進展。對於3臺伺服器,Raft可以容忍單個伺服器故障。如果有5臺伺服器,它將容忍2臺;對於2N + 1臺伺服器,它將容忍N個故障。

這就引出了CAP定理,它的實際結果是,在存在網路分割槽的情況下,我們必須權衡可用性和一致性。

在權衡中,Raft處於一致性陣營中。其不變數旨在防止群集可能達到不一致狀態的情況,在這種情況下,不同的客戶端將獲得不同的答案。為此,Raft犧牲了可用性。

正如前面所說,Raft並不是為高吞吐量,細粒度的服務而設計的。每個客戶端請求都會觸發大量工作——Raft副本之間的通訊,以將其複製到大多數副本並持久化;在客戶得到迴應之前。

因此,例如,我們不會設計一個所有客戶端請求都通過Raft進行復制的資料庫。那就太慢了。Raft更適合粗粒度的分散式原語——例如實現鎖伺服器,選舉高層協議的領導者,在分散式系統中複製關鍵配置資料等等。

本系列中介紹的Raft實現是用Go編寫的。從作者角度來看,Go具有三個強大的優勢,這使其成為本系列以及一般網路服務的有希望的實現語言:

  1. 併發性:像Raft這樣的演算法,本質上是深度併發的。每個副本執行正在進行的操作,執行定時事件的計時器,並且必須響應來自其他副本和客戶端的非同步請求。

  2. 標準庫:Go具有功能強大的標準庫,可以輕鬆編寫複雜的網路伺服器,而無需匯入和學習任何第三方庫。特別是在Raft的情況下,第一個必須回答的問題是“如何在副本之間傳送訊息?”,許多人陷入設計協議和某些序列化或使用繁重的第三方庫的細節中。Go僅具有net/rpc,它是用於此類任務的足夠好的解決方案,它的建立速度非常快,並且不需要匯入。

  3. 簡便性:即使在我們開始考慮實現語言之前,實現分散式共識也已經足夠複雜。可以用任何一種語言編寫清晰,簡單的程式碼,但是在Go語言中,這是預設的習慣用法,並且該語言在每個可能的級別上都反對複雜性。

平臺開發 [360雲端計算]

本作品採用《CC 協議》,轉載必須註明作者和本文連結

快樂就是解決一個又一個的問題!

相關文章