raft演算法一致性的研究

程式碼的壞味道發表於2022-01-06

Raft 演算法在史丹佛 Diego Ongaro 和 John Ousterhout 於 2013 年發表的《In Search of an Understandable Consensus Algorithm》中提出。相較於 Paxos,Raft 透過邏輯分離使其更容易理解和實現,目前,已經有十多種語言的 Raft 演算法實現框架,較為出名的有 etcd、Consul.

RAFT 演算法 - 分散式共識演算法,Redis 的 Sentinel 就利用該演算法。

  • 服務發現框架:consul、etcd
  • 日誌:RocketMQ
  • 資料儲存:Tidb、k8s

動態演示
www.kailing.pub/raft/index.html#ele...

一個 Raft 叢集包含若干節點,Raft 把這些節點分為三種狀態:Leader、 Follower、Candidate,每種狀態負責的任務也是不一樣的。正常情況下,叢集中的節點只存在 Leader 與 Follower 兩種狀態。

Leader(領導者) :負責日誌的同步管理,處理來自客戶端的請求,與Follower保持heartBeat的聯絡;

Follower(追隨者) :響應 Leader 的日誌同步請求,響應Candidate的邀票請求,以及把客戶端請求到Follower的事務轉發(重定向)給Leader;

Candidate(候選者) :負責選舉投票,叢集剛啟動或者Leader當機時,狀態為Follower的節點將轉為Candidate併發起選舉,選舉勝出(獲得超過半數節點的投票)後,從Candidate轉為Leader狀態。

1.3 Raft 三個子問題

通常,Raft 叢集中只有一個 Leader,其它節點都是 Follower。Follower 都是被動的,不會傳送任何請求,只是簡單地響應來自 Leader 或者 Candidate 的請求。Leader 負責處理所有的客戶端請求(如果一個客戶端和 Follower 聯絡,那麼 Follower 會把請求重定向給 Leader)。

為簡化邏輯和實現,Raft 將一致性問題分解成了三個相對獨立的子問題。

選舉(Leader Election) :當 Leader 當機或者叢集初創時,一個新的 Leader 需要被選舉出來;

日誌複製(Log Replication) :Leader 接收來自客戶端的請求並將其以日誌條目的形式複製到叢集中的其它節點,並且強制要求其它節點的日誌和自己保持一致;

安全性(Safety) :如果有任何的伺服器節點已經應用了一個確定的日誌條目到它的狀態機中,那麼其它伺服器節點不能在同一個日誌索引位置應用一個不同的指令。

2. Raft 演算法之 Leader Election 原理

根據 Raft 協議,一個應用 Raft 協議的叢集在剛啟動時,所有節點的狀態都是 Follower。由於沒有 Leader,Followers 無法與 Leader 保持心跳(Heart Beat),因此,Followers 會認為 Leader 已經下線,進而轉為 Candidate 狀態。然後,Candidate 將向叢集中其它節點請求投票,同意自己升級為 Leader。如果 Candidate 收到超過半數節點的投票(N/2 + 1),它將獲勝成為 Leader。

第一階段:所有節點都是 Follower。

上面提到,一個應用 Raft 協議的叢集在剛啟動(或 Leader 當機)時,所有節點的狀態都是 Follower,初始 Term(任期)為 0。同時啟動選舉定時器,每個節點的選舉定時器超時時間都在 100~500 毫秒之間且並不一致(避免同時發起選舉)。

raft演算法一致性的研究

所有節點都是 Follower

第二階段:Follower 轉為 Candidate 併發起投票。

沒有 Leader,Followers 無法與 Leader 保持心跳(Heart Beat),節點啟動後在一個選舉定時器週期內未收到心跳和投票請求,則狀態轉為候選者 Candidate 狀態,且 Term 自增,並向叢集中所有節點傳送投票請求並且重置選舉定時器。

注意,由於每個節點的選舉定時器超時時間都在 100-500 毫秒之間,且彼此不一樣,以避免所有 Follower 同時轉為 Candidate 並同時發起投票請求。換言之,最先轉為 Candidate 併發起投票請求的節點將具有成為 Leader 的“先發優勢”。

raft演算法一致性的研究

Follower 轉為 Candidate 併發起投票

第三階段:投票策略。

節點收到投票請求後會根據以下情況決定是否接受投票請求(每個 follower 剛成為 Candidate 的時候會將票投給自己):

請求節點的 Term 大於自己的 Term,且自己尚未投票給其它節點,則接受請求,把票投給它;

請求節點的 Term 小於自己的 Term,且自己尚未投票,則拒絕請求,將票投給自己。

raft演算法一致性的研究

投票策略

第四階段:Candidate 轉為 Leader。

一輪選舉過後,正常情況下,會有一個 Candidate 收到超過半數節點(N/2 + 1)的投票,它將勝出並升級為 Leader。然後定時傳送心跳給其它的節點,其它節點會轉為 Follower 並與 Leader 保持同步,到此,本輪選舉結束。

注意:有可能一輪選舉中,沒有 Candidate 收到超過半數節點投票,那麼將進行下一輪選舉。

raft演算法一致性的研究

Candidate 轉為 Leader

3. Raft 演算法之 Log Replication 原理

在一個 Raft 叢集中,只有 Leader 節點能夠處理客戶端的請求(如果客戶端的請求發到了 Follower,Follower 將會把請求重定向到 Leader),客戶端的每一個請求都包含一條被複制狀態機執行的指令。Leader 把這條指令作為一條新的日誌條目(Entry)附加到日誌中去,然後並行得將附加條目傳送給 Followers,讓它們複製這條日誌條目。

當這條日誌條目被 Followers 安全複製,Leader 會將這條日誌條目應用到它的狀態機中,然後把執行的結果返回給客戶端。如果 Follower 崩潰或者執行緩慢,再或者網路丟包,Leader 會不斷得重複嘗試附加日誌條目(儘管已經回覆了客戶端)直到所有的 Follower 都最終儲存了所有的日誌條目,確保強一致性。

第一階段:客戶端請求提交到 Leader。

如下圖所示,Leader 收到客戶端的請求,比如儲存資料 5。Leader 在收到請求後,會將它作為日誌條目(Entry)寫入本地日誌中。需要注意的是,此時該 Entry 的狀態是未提交(Uncommitted),Leader 並不會更新本地資料,因此它是不可讀的。

raft演算法一致性的研究

客戶端請求提交到 Leader

第二階段:Leader 將 Entry 傳送到其它 Follower

Leader 與 Followers 之間保持著心跳聯絡,隨心跳 Leader 將追加的 Entry(AppendEntries)並行地傳送給其它的 Follower,並讓它們複製這條日誌條目,這一過程稱為複製(Replicate)。

有幾點需要注意:

1. 為什麼 Leader 向 Follower 傳送的 Entry 是 AppendEntries 呢?

因為 Leader 與 Follower 的心跳是週期性的,而一個週期間 Leader 可能接收到多條客戶端的請求,因此,隨心跳向 Followers 傳送的大機率是多個 Entry,即 AppendEntries。當然,在本例中,我們假設只有一條請求,自然也就是一個Entry了。

2. Leader 向 Followers 傳送的不僅僅是追加的 Entry(AppendEntries)。

在傳送追加日誌條目的時候,Leader 會把新的日誌條目緊接著之前條目的索引位置(prevLogIndex), Leader 任期號(Term)也包含在其中。如果 Follower 在它的日誌中找不到包含相同索引位置和任期號的條目,那麼它就會拒絕接收新的日誌條目,因為出現這種情況說明 Follower 和 Leader 不一致。

3. 如何解決 Leader 與 Follower 不一致的問題?

在正常情況下,Leader 和 Follower 的日誌保持一致,所以追加日誌的一致性檢查從來不會失敗。然而,Leader 和 Follower 一系列崩潰的情況會使它們的日誌處於不一致狀態。Follower可能會丟失一些在新的 Leader 中有的日誌條目,它也可能擁有一些 Leader 沒有的日誌條目,或者兩者都發生。丟失或者多出日誌條目可能會持續多個任期。

要使 Follower 的日誌與 Leader 恢復一致,Leader 必須找到最後兩者達成一致的地方(說白了就是回溯,找到兩者最近的一致點),然後刪除從那個點之後的所有日誌條目,傳送自己的日誌給 Follower。所有的這些操作都在進行附加日誌的一致性檢查時完成。

Leader 為每一個 Follower 維護一個 nextIndex,它表示下一個需要傳送給 Follower 的日誌條目的索引地址。當一個 Leader 剛獲得權力的時候,它初始化所有的 nextIndex 值,為自己的最後一條日誌的 index 加 1。如果一個 Follower 的日誌和 Leader 不一致,那麼在下一次附加日誌時一致性檢查就會失敗。在被 Follower 拒絕之後,Leader 就會減小該 Follower 對應的 nextIndex 值並進行重試。最終 nextIndex 會在某個位置使得 Leader 和 Follower 的日誌達成一致。當這種情況發生,附加日誌就會成功,這時就會把 Follower 衝突的日誌條目全部刪除並且加上 Leader 的日誌。一旦附加日誌成功,那麼 Follower 的日誌就會和 Leader 保持一致,並且在接下來的任期繼續保持一致。

raft演算法一致性的研究

如何解決 Leader 與 Follower 不一致的問題

第三階段:Leader 等待 Followers 回應。

Followers 接收到 Leader 發來的複製請求後,有兩種可能的回應:

寫入本地日誌中,返回 Success;

一致性檢查失敗,拒絕寫入,返回 False,原因和解決辦法上面已做了詳細說明。

需要注意的是,此時該 Entry 的狀態也是未提交(Uncommitted)。完成上述步驟後,Followers 會向 Leader 發出 Success 的回應,當 Leader 收到大多數 Followers 的回應後,會將第一階段寫入的 Entry 標記為提交狀態(Committed),並把這條日誌條目應用到它的狀態機中。

raft演算法一致性的研究

Leader 等待 Followers 回應

第四階段:Leader 回應客戶端。

完成前三個階段後,Leader會向客戶端回應 OK,表示寫操作成功。

raft演算法一致性的研究

Leader 回應客戶端

第五階段,Leader 通知 Followers Entry 已提交

Leader 回應客戶端後,將隨著下一個心跳通知 Followers,Followers 收到通知後也會將 Entry 標記為提交狀態。至此,Raft 叢集超過半數節點已經達到一致狀態,可以確保強一致性。

需要注意的是,由於網路、效能、故障等各種原因導致“反應慢”、“不一致”等問題的節點,最終也會與 Leader 達成一致。

raft演算法一致性的研究

Leader 通知 Followers Entry 已提交

4. Raft 演算法之安全性

前面描述了 Raft 演算法是如何選舉 Leader 和複製日誌的。然而,到目前為止描述的機制並不能充分地保證每一個狀態機會按照相同的順序執行相同的指令。例如,一個 Follower 可能處於不可用狀態,同時 Leader 已經提交了若干的日誌條目;然後這個 Follower 恢復(尚未與 Leader 達成一致)而 Leader 故障;如果該 Follower 被選舉為 Leader 並且覆蓋這些日誌條目,就會出現問題,即不同的狀態機執行不同的指令序列。

鑑於此,在 Leader 選舉的時候需增加一些限制來完善 Raft 演算法。這些限制可保證任何的 Leader 對於給定的任期號(Term),都擁有之前任期的所有被提交的日誌條目(所謂 Leader 的完整特性)。關於這一選舉時的限制,下文將詳細說明。

4.1 選舉限制

在所有基於 Leader 機制的一致性演算法中,Leader 都必須儲存所有已經提交的日誌條目。為了保障這一點,Raft 使用了一種簡單而有效的方法,以保證所有之前的任期號中已經提交的日誌條目在選舉的時候都會出現在新的 Leader 中。換言之,日誌條目的傳送是單向的,只從 Leader 傳給 Follower,並且 Leader 從不會覆蓋自身本地日誌中已經存在的條目。

Raft 使用投票的方式來阻止一個 Candidate 贏得選舉,除非這個 Candidate 包含了所有已經提交的日誌條目。Candidate 為了贏得選舉必須聯絡叢集中的大部分節點。這意味著每一個已經提交的日誌條目肯定存在於至少一個伺服器節點上。如果 Candidate 的日誌至少和大多數的伺服器節點一樣新(這個新的定義會在下面討論),那麼它一定持有了所有已經提交的日誌條目(多數派的思想)。投票請求的限制中請求中包含了 Candidate 的日誌資訊,然後投票人會拒絕那些日誌沒有自己新的投票請求。

Raft 透過比較兩份日誌中最後一條日誌條目的索引值和任期號,確定誰的日誌比較新。如果兩份日誌最後條目的任期號不同,那麼任期號大的日誌更加新。如果兩份日誌最後的條目任期號相同,那麼日誌比較長的那個就更加新。

4.2 提交之前任期內的日誌條目

如同 4.1 節介紹的那樣,Leader 知道一條當前任期內的日誌記錄是可以被提交的,只要它被複制到了大多數的 Follower 上(多數派的思想)。如果一個 Leader 在提交日誌條目之前崩潰了,繼任的 Leader 會繼續嘗試複製這條日誌記錄。然而,一個 Leader 並不能斷定被儲存到大多數 Follower 上的一個之前任期裡的日誌條目 就一定已經提交了。這很明顯,從日誌複製的過程可以看出。

鑑於上述情況,Raft 演算法不會透過計算副本數目的方式去提交一個之前任期內的日誌條目。只有 Leader 當前任期裡的日誌條目透過計算副本數目可以被提交;一旦當前任期的日誌條目以這種方式被提交,那麼由於日誌匹配特性,之前的日誌條目也都會被間接的提交。在某些情況下,Leader 可以安全地知道一個老的日誌條目是否已經被提交(只需判斷該條目是否儲存到所有節點上),但是 Raft 為了簡化問題使用了一種更加保守的方法。

當 Leader 複製之前任期裡的日誌時,Raft 會為所有日誌保留原始的任期號,這在提交規則上產生了額外的複雜性。但是,這種策略更加容易辨別出日誌,即使隨著時間和日誌的變化,日誌仍維護著同一個任期編號。此外,該策略使得新 Leader 只需要傳送較少日誌條目。

選舉約束

前面所講的選舉機制,還存在一個問題。例如,當一個跟隨者不可用時,領導者提交了幾個日誌(注:只要有大多數機器存活,raft演算法就能正常運轉並且保證資料一致性)。然後,這個跟隨者從故障中恢復並且被選舉為領導者。此時,領導者的日誌跟其他跟隨者的日誌存在衝突。根據前面的日誌複製章節,對於這種情況,領導者會強制跟隨者複製自己的日誌來解決不一致的問題。但是,現在這樣做會使得不同伺服器的狀態機沒有以相同的順序執行相同命令。

所以,對於這種情況,簡單的做法就是:不允許這樣的跟隨者或者候選者稱為領導者。
也就是說,新的領導者必須包含所有已提交的日誌
也就是說,一個候選者必須包含所有已提交的日誌,才能贏得選舉
也就是說,RequestVote訊息中應該包含候選人的日誌資訊。如果投票的人發現它的日誌比候選人的日誌新,它可以拒絕投票

那怎麼判斷兩個日誌哪個是最新的呢?透過日誌中最後一個日誌項的任期和索引來比較。比較過程為:

  • 如果任期不同,那麼擁有更大編號任期的日誌是最新的。
  • 如果任期相同,那麼擁有更大索引的日誌是最新的。

基於這個問題,我們對選舉過程提出了一個約束:

領導者的完整性:
如果一個日誌項在某個任期已經被提交了,那麼這個條目一定會出現在更大編號任期內的領導者的日誌中。

提交先前任期的日誌項

raft中,新的領導者不會提交先前任期的日誌(來自先前領導者)。一旦一個來自當前任期的日誌項被提交後,(在這個日誌項)之前的所有日誌項都會被間接提交,因為日誌複製章節的日誌匹配特性。

相比其他一致性演算法,raft演算法保持了先前任期日誌項的任期,

  • 由於日誌項在不同時間、不同日誌之間保持相同任期,使得我們更容易推理日誌項,使得系統行為可預測。
  • 不需要傳送多餘的日誌項來重新編號先前任期的日誌項,減少了網路通訊。

安全性證明

  1. 證明領導者完整性。
  2. 由領導者完整性證明狀態機安全性。
  3. 狀態機安全性 + 按日誌索引應用日誌 –> 所有狀態機以相同的順序執行相同的命令。

領導者完整性特性

領導者完整性特性:如果某個任期內的某個日誌項已提交,那麼後面任期的領導者的日誌中都會包含這個日誌項。

假設 leaderT 在它的任期T內提交了一個日誌項,但是這個日誌項沒有被一些未來任期的領導者儲存。假設大於任期 T 的所有任期中,存在一個最小任期 U,它的領導者 leaderU 沒有儲存這個日誌項。

  1. 在選舉 leaderU 的時候,它的日誌中沒有這個已提交的日誌項。因為領導者不能刪除覆蓋或刪除自己的日誌項。
  2. leaderT 把這個日誌項複製給了叢集中的大多數伺服器,而 leaderU 接收到了叢集中大多數伺服器投的贊同票。因此,至少有一個伺服器即接受了 leaderT 的日誌項,也為 leaderU 投了票。這個投票者是達到矛盾的關鍵點。
  3. 這個投票者一定是先接受了 leaderT 的日誌項,再為 leaderU 投了票。如果不是這樣的話,這個投票者會拒絕儲存 leaderT 的日誌項,因為它的當前任期比 leaderT 的當前任期大。相比步驟2,步驟3強調了投票者這兩個操作的順序性
  4. 這個投票者為 leaderU 投票時,它的日誌中仍然儲存了 leaderT 提交的日誌項。因為每個在 [T,U) 的領導者不會移除日誌項,跟隨者只會移除它們跟領導者衝突的日誌。相比步驟3,步驟4強調了投票者投票時的日誌內容
  5. 投票者把它的票投給了 leaderU,說明 leaderT 的日誌至少是跟投票者是一樣新的,也就是說,投票者的日誌內容要麼跟 leaderT 的日誌內容完全一樣,要麼是 leaderT 日誌內容的子集。這個導致了兩個矛盾點中的一個。
  6. 首先,如果投票者和 leaderU 的最後一個日誌項的任期相同(加上步驟5這個大前提),那麼 leaderU 的日誌至少跟投票者的日誌一樣長,所以 leaderU 的日誌包含了投票者日誌中的每一項。這是個矛盾,因為我們已經假設了leaderU 沒有儲存這個日誌項。
  7. 否則,leaderU 的最後一個日誌項的任期大於投票者的。這個日誌項的任期是大於 T 的,因為投票者的最後一個日誌項的任期至少是 T(從步驟2知道,它包含了任期T內已提交的日誌項)。建立 leaderU 最後一個日誌項的領導者一定已經包含了任期 T 內已提交的日誌項(從假設得知)。然後,根據日誌匹配特性,leaderU 的日誌一定也包含了這個已提交的日誌項,跟假設衝突了。
  8. 因此,所有任期大於 T 的領導者一定包含了任期 T 提交的所有日誌項。
  9. 日誌匹配特性保證了未來的領導者也將包含那些間接提交的日誌項,即大多數伺服器儲存了這個日誌條目,還沒有應用到狀態機。那麼,新的領導者一定是從這些伺服器產生,並且他們提交自己任期的日誌條目時,也會根據日誌匹配特性,一併提交先前任期的日誌條目。

狀態機安全性

狀態機安全性:如果一個伺服器已經把某個給定索引的日誌項應用到狀態機了,那麼沒有其它伺服器會為相同的索引應用不同的日誌項。

如果一個伺服器應用了某個給定索引的日誌項,那麼這個日誌項一定是已經被提交了的,並且在這個日誌項之前,包括這個日誌項,伺服器的日誌和領導者的日誌是保持一致的。

根據日誌完整性特性,後面任期的領導者也儲存了相同的日誌項,所以,在後面任期應用這個索引的伺服器也會應用同樣的值。

5、線性化語義

raft 的讀寫都在 leader 節點中進行,它保證了讀的都是最新的值,它是符合強一致性的(線性一致性),raft 除了這個還在【客戶端互動】那塊也做了一些保證,詳情可以參考論文。但是 zookeeper 不同,zookeeper 寫在 leader,讀可以在 follower 進行,可能會讀到了舊值,它不符合強一致性(只考慮寫一致性,不考慮讀一致性),但是 zookeeper 去 follower 讀可以有效提升讀取的效率。

6、後記

對比於 zab、raft,我們發現他們選舉、setData 都是需要過半機制才行,所以他們針對網路分割槽的處理方法都是一樣的。

一個叢集的節點經過網路分割槽後,如一共有 A、B、C、D、E 5個節點,如果 A 是 leader,網路分割槽為 A、B、C 和 D、E,在A、B、C分割槽還是能正常提供服務的,而在 D、E 分割槽因為不能得到大多數成員確認(雖然分割槽了,但是因為配置的原因他們還是能知道所有的成員數量,比如 zk 叢集啟動前需要配置所有成員地址,raft 也一樣),是不能進行選舉的,所以保證只會有一個 leader。

如果分割槽為 A、B 和 C、D、E ,A、B 分割槽雖然 A 還是 leader,但是卻不能提供事務服務(setData),C、D、E 分割槽能重新選出 leader,還是能正常向外提供服務。

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

相關文章