談談Raft

CodeBear發表於2022-01-09

本文主要參考
極客時間-etcd 實戰課
GitChat-分散式鎖的最佳實踐之:基於 Etcd 的分散式鎖
談到分散式協調元件,我們第一個想到的應該是大名鼎鼎的Zookeeper,像我們常用的Kafka(最新版本的Kafka已經拋棄了Zookeeper),Hadoop都用到了Zookeeper,而另外一個分散式協調元件etcd隨著k8s的出現,也映入了我們的眼簾。談到etcd,不得不說說etcd的基石—Raft。

遠古時代-單點系統

在遠古時代,我們資料都只存在於一個節點,不管是讀資料也好,寫資料也罷,都在一個節點上進行,不存在資料一致性問題,非常簡單。

但是慢慢的,單點的問題就顯現了——無法高可用,因為我們的資料是單點的,只要這個節點出現問題,我們的系統就不可用了,我們就得提桶跑路了:
image.png

作為有追求的軟體開發者,肯定不允許這樣的情況,所以就引入了“多副本”的概念,也就是說一份資料,同時在N個節點儲存,這樣做的好處也顯而易見:

  1. 高可用,避免單點故障,哪怕有個別節點掛了,其他節點還可以繼續提供服務。
  2. 高效能:
    2.1 原本讀寫資料都在一個節點,節點壓力比較大,現在把讀寫請求分散在不同的節點,節點壓力就下降了,效能也就獲得了提升。
    2.2 原本讀寫資料都在一個節點,比如說資料節點部署在了廣東機房,應用部署在了內蒙古機房,位於內蒙古的應用操作位於廣東的資料節點,想想就不怎麼“高效能”,現在由於“多副本”,可以把資料節點同時部署在內蒙古機房、廣東機房,如果是位於內蒙古的應用來運算元據節點,就可以訪問內蒙古的資料節點,如果是位於廣東的應用來運算元據節點,就可以訪問廣東的資料節點,大幅度減少訪問延遲,效能也就獲得了提升。

多副本複製方案

引入了“多副本”後,帶來的第一個問題就是多節點資料如何複製,有兩個大方向:

  1. 主從複製,一個節點是主節點,其他節點都是從節點,當主節點收到寫請求後,再把資料分發給從節點。
  2. 去中心化複製,任意節點都可以接收寫請求,再把資料分發給其他節點,這種方案聽起來就比較頭疼——如何處理各種衝突。

大部分系統都是採用的主從複製,主從複製也有不同的實現方案:

  1. 同步複製,主節點收到寫請求後,把資料分發給所有的從節點,從節點接收到資料後,給主節點一個響應,直到所有的從節點都響應了主節點,主節點才能響應客戶端。這種方案確保了資料的一致性,但是可用性卻降低了,只要有一個節點出現故障,整個系統就會不可用。
  2. 非同步複製,主節點收到寫請求後,立刻響應客戶端,同時後臺非同步的將資料分發給從節點,如果從節點還沒有收到資料,主節點或者從節點或者主節點和從節點間的網路出現故障了,那資料就不一致了,但是可用性卻是最高的。
  3. 半同步複製,介於同步複製和非同步複製之間,主節點收到寫請求後,把資料分發給所有的從節點,從節點接收到資料後,給主節點一個響應,直到主節點收到了N個從節點的響應,主節點才能響應客戶端。在一致性,可用性上進行了平衡和取捨。

注意,同步複製是主節點收到了所有從節點的響應,才能響應客戶端,而半同步複製是主節點收到了N個從節點的響應,就能響應客戶端,N可以是如下的情況:

  • 可以是1,也可以是2。
  • 可以是所有從節點的數量,這樣就接近於同步複製了。
  • 可以是0,這樣就接近於非同步複製了。
  • 比較好的方案,N=所有從節點的數量/2+1。

Raft的由來

上面我們瞭解了單點系統的問題——無法高可用,引入“多副本”的意義,介紹了多副本資料複製的方案,其中主從複製是用的比較廣泛的,又分析了三種主從複製方案的優缺點。

既然是主從複製,那麼問題就來了,who is master?who is follower?誰是主節點,誰是從節點?資料複製細節是怎樣的?異常情況如何處理?Paxos便出現了,Paxos是解決這類問題的“祖師爺”,它是一種共識演算法,非常複雜,實現起來難度也非常高,所以一般來說,實現的時候都會進行一定的簡化,像我們比較熟悉的Zookeeper採用的ZAB就是基於Paxos實現的,還有今天要分享的Raft也是基於Paxos實現的。

好了,餐前小麵包吃完了,現在進入正餐環節。

Raft角色

Raft定義了三種角色:Leader、 Follower 、Candidate,一個執行良好的Raft叢集,只會存在Leader、 Follower兩種角色。下面,我們來看看這三種角色的職責。

  1. Leader:領導者,一個Raft叢集,只會有一個Leader
    1.1 處理來自客戶端的讀寫請求;
    1.2 接收到寫請求後,會把資料分發給Follower;
    1.3 與Follower保持心跳,穩固自己Leader的地位。
  2. Follower:追隨者
    2.1 處理來自客戶端的讀寫請求,如果是寫請求,會轉發給Leader;
    2.2 接收來自Leader分發的資料;
  3. Candidate:候選者,負責投票選舉Leader,選舉勝出後,Candidate轉為Leader。

Raft概述

一個應用Raft的叢集只會有一個Leader,其他節點都是Follower:

  • Follower只是被動的接收來自Leader、客戶端的請求,並且響應,不會主動發起請求,如果接收到了來自客戶端的寫請求,會把請求轉發給Leader。
  • Leader會處理來自客戶端的讀寫請求,如果接收到了寫請求,還會將資料分發給Follower,讓Follower的資料和自己保持同步。

為了簡化邏輯,Raft將一致性問題拆分成了三個子問題:

  • 選舉:叢集剛啟動,或者Leader當機,就需要選舉出新的Leader。
  • 日誌複製:Leader處理來自客戶端的寫請求,然後把日誌(資料)分發給Follower強制Follower的資料和自己保持一致。
  • 安全性:由Leader只附加原則、Leader完全特性、日誌匹配三個特性保證。

下面我們將圍繞這三個子問題,進行較為詳細的介紹,不過在這之前,需要再介紹幾個專業名詞:

  • Term:屆數、任期,叢集剛啟動Term為0,有新的Leader產生,Term就會+1(自增),在ZAB協議中,用Epoch表示,概念是類似的。
  • Index:索引,每個日誌(資料)都對應了一個索引。
  • 日誌:資料,這裡的日誌並不是指的我們在開發中,列印出來,幫助我們分析、排查問題的日誌,也不是使用者的操作日誌,而是資料的概念。

瞭解了這三個專業名詞之後,我們就要開始介紹選舉、日誌複製、安全性三個子問題了:
image.png

選舉

Raft叢集啟動——沒有Leader,或者Leader當機——沒有Leader,Follwer就接收不到來自Leader的心跳,持續一段時間後,Follwer就會轉為Candidate,進入投票流程,如果Candidate收到大多數Candidate同意自己成為Leader的投票,就會升級為Leader,此時Term就會+1。

Leader當機,又會進入新一輪的選舉。

從這裡看出,Follwer和Candidate是可以相互轉換的,Follwer是無法直接轉為Leader的,但是Leader可以直接轉為Follwer(Leader轉為Follower的時機,後面會說到):

image.png

下面我們就來看看一個應用Raft的叢集啟動,選舉過程中的細節:

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

一個應用Raft的叢集剛啟動,所有節點都是Follower,此時Term為0,由於接收不到來自Leader的心跳(Leader還沒有產生,肯定接收不到來自Leader的心跳),並持續一段時間,Follower轉為Candidate,Term自增。

第二階段:所有節點都是Candidate

第一階段後,所有節點都從Follower轉為了Candidate,這個時候,有一個新的概念:選舉定時器。每個節點都有一個選舉定時器,選舉定時器的時間是隨機的,且很大概率上,每個節點的選舉定時器的時間都不同。節點的選舉定時器達到一定時間後,此節點會向所有其他節點發起“毛遂自薦”式的投票。

第三階段:Candidate判定

節點(假設是B)收到其他節點(假設是A)的“毛遂自薦”式的投票後,會有兩種可能:

  1. A的日誌完整度至少和自己一樣高,且B節點沒有同意其他節點成為Leader,B節點才會同意A節點成為Leader(當B節點同意A節點成為Leader後,就沒辦法同意其他節點成為Leader了,每個Candidate只有一張選票)。
  2. A的日誌完整度沒有自己高,且A節點沒有同意其他節點成為Leader,B節點就會拒絕A成為Leader,並且將票投給自己。

第四階段:Candidate轉為Leader

正常情況下,經過一輪的選舉,會有一個Candidate可以獲得半數以上節點的投票,此節點就成為了Leader,Leader會告知其他節點,其他節點就會從Candidate轉為Follower。
如果一輪的選舉後,沒有Candidate獲得半數以上節點的投票,就會再次進行選舉。

選舉定時器的作用

讓我們想想這個選舉定時器有什麼作用,假設現在有3個節點:Follwer A、Follwer B、Leader C,由於某些原因,Leader C當機了,A、B就會從Follwer轉為Candidate,進入投票流程,選出新的Leader。Candidate A、Candidate B兩個節點同時發起“毛遂自薦”式的投票,極有可能出現以下的情況:

  • A節點收到了B“毛遂自薦”式的投票後,發現自己已經投了自己,就會拒絕B成為Leader
  • B節點收到了A“毛遂自薦”式的投票後,發現自己已經投了自己,就會拒絕A成為Leader

然後就尷尬了:一個叢集中有三個節點,Candidate要成為Leader,至少要獲得兩個節點的同意,現在並不滿足這個條件,就需要重新進行選舉,正是引入了選舉定時器,所以一般不會發生這種情況。

Follower認為Leader掛了的時機

在前面,我們說到Follwer就接收不到來自Leader的心跳,持續一段時間後,Follwer就會轉為Candidate。那麼就產生了兩個問題,Leader與Follower心跳間隔的時間是多少,到多長時間還接收不到Leader的心跳 ,Follower才認為Leader掛了。

在etcd中,這兩個引數是可以配置的,etcd的Leader與Follower預設心跳間隔是100ms,預設最大容忍時間是1000ms,這個預設最大容忍時間實在是太小了,需要進行適當的增大,否則很容易觸發選舉,影響叢集的穩定性,當然也不能增加的很大,不然Leader真的掛了,需要過好久,才能觸發選舉,也影響叢集的穩定性。

Leader轉為Folllower、無效選舉、etcd如何避免

為了方便大家閱讀,避免往上翻,我把Raft角色轉換的圖片再複製下:
image.png
可以看到Follower無法直接轉為Leader,但是Leader可以直接轉為Follower,那麼在什麼情況下,Leader可以直接轉為Follower呢?

假設,現在有3個節點:Follwer A、Follwer B、Leader C,Leader C當機了,A、B就會從Follwer轉為Candidate,進入投票流程,選出新的Leader,新的Leader會從A、B中誕生。Leader C復活後,發現現在已經有新的Term了,現在的天下已經不是自己的了,就會發出這樣的感嘆:
image.png

曾經的Leader C就會默默的轉為Follower,假設網路原因,C突然無法與A、B進行聯通,它就會不斷的自增Term,發起投票,但是這是無效的,因為無法與A、B進行聯通。

網路問題修復後,新的Leader收到了大於自己的Term,Leader就會陷入自我懷疑,也會發出這樣的感嘆:
image.png
Leader就會默默的轉為Follower。

由於此時叢集中沒有Leader,就會進入選舉。節點C的資料是很舊的,所以C肯定在選舉中落敗,這個選舉是毫無意義的,且會影響叢集的穩定性。

為了避免問題,3.4版本的etcd新增了一個引數:PreVote。開啟PreVote後,Follower在轉為Candidate前,會進入PreCandidate,不自增Term,發起預投票,如果多數節點認為此節點有成為Leader的資格,才能轉為Candidate,進入選舉。

不過,PreVote預設是關閉的,如果有需要,可以開啟。

看到預投票、投票,不知道大家有沒有想到2PC,這應該就是2PC的一個應用吧。

日誌複製

在一個Raft叢集中,只有Leader才能真正處理來自客戶端的寫請求,Leader接收到寫請求後,需要把資料再分發給Follower,當半數以上的Follower響應Leader,Leader才會響應客戶端。如果有部分Follower執行緩慢,或者網路丟包,Leader會不斷嘗試,直到所有Follower都響應了客戶端,保證資料的最終一致性。

從這裡可以看出,Raft是最終一致性,那麼應用Raft的etcd也應該是最終一致性(從儲存資料的角度來說),但是etcd很巧妙的解決了這個問題,實現了強一致性(從讀取資料的角度來說)。Zookeeper處理寫請求,從巨集觀上來講,和Raft是比較類似的,所以Zookeeper本身並不是強一致性的(更準確的來說,從Zookeeper服務端的角度來說,Zookeeper並不是強一致性的,但是客戶端提供了API,可以實現強一致性),很多地方都說Zookeeper是強一致性的,其實這是錯誤的,最起碼,我們呼叫普通API的時候,Zookeeper並不是強一致性的。

讓我們來看看日誌複製過程中的細節。

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

如果客戶端把寫請求提交給了Follower,Follower會把請求轉給Leader,由Leader真正處理寫請求。

第二階段:Leader預寫日誌

Leader收到寫請求後,會預寫日誌,日誌為不可讀,這就是傳說中的WAL。

第三階段:Leader將日誌傳送給Follower

Leader與Follower保持心跳聯絡,會把日誌分發給Follower,這裡的日誌可能會存在多個,因為在一個心跳時間間隔內,Leader可能收到了來自客戶端的多個寫請求。Leader同步給Follower的日誌,並不是僅僅只有當前的日誌,還會包含上一個日誌的index,term,因為Follower要進行一致性檢查。

第四階段:Follower收到Leader的日誌,進行一致性檢查

Follower收到Leader的日誌,會進行一致性檢查,如果Follower的日誌情況和Leader給的日誌情況不同,就會拒絕接收日誌。

一般來說,Follower的日誌是和Leader的日誌保持一致的,但是由於某些情況,可能導致Follower的日誌中有Leader沒有的日誌,或者Follower的日誌中沒有Leader有的日誌,或者兩種情況都有。這個時候,Leader的許可權就會凸顯,它會強制Follower的日誌,與自己保持一致。具體是怎麼做的,我們後面再說,先看整體流程。

第五階段:Follower預寫日誌

一致性檢查通過,Follower也會預寫日誌,日誌為不可讀。

第六階段:Leader收到大多數Follower響應,提交日誌

Leader收到大多數Follower的響應後,會提交日誌,並把日誌應用到它的狀態機中,此時日誌是可讀的。

第七階段:Leader響應客戶端

Leader響應客戶端,經過這幾個階段,Leader才能響應客戶端。

第八階段:Leader通知Follower提交日誌

Leader與Follower保持心跳聯絡,會通知Follower:你們可以提交日誌了。可千萬別忘了,在第五階段,Follower也只是進行了日誌預寫。

第九階段:Follower提交日誌

Follower接收到Leader的提交日誌通知後,會進行日誌提交,並把日誌應用到它的狀態機中,此時日誌是可讀的。

第十階段:收尾

可以來到第十階段,說明至少大多數Follower和Leader是保持一致的,可能還會有部分Follower因為效能、故障等原因,沒有和Leader保持一致,Leader會不斷的嘗試,直到所有的Follower都和Leader保持一致。

一致性檢查失敗,怎麼辦?

在第四階段,說到Follower收到了Leader的日誌後,會進行一致性檢查,如果成功還好說,如果失敗,怎麼辦呢?

Leader針對每個Follower都維護了一個nextIndex。當Leader獲得權力的時候,會初始化每個Follower的nextIndex為自己的最後一條日誌的index+1,如果Follower的日誌和Leader的日誌不一樣,那麼一致性檢查就會失敗,就會拒絕Leader。Leader會逐步減小此Follower對應的nextIndex,並進行重試,說白了,就是回溯,找到兩者最近的一致點。找到兩者最近的一致點後,Follower會刪除衝突的日誌,並且應用Leader的日誌,此時,Follower便和Leader保持一致了。

安全性

Raft叢集的安全性是由三個特性來保障的:Leader只附加原則、Leader完全特性、日誌匹配特性。

Leader只附加原則

讓我們設想一種場景:Leader響應客戶端後,當機了,發生這樣的事情意味著什麼?既然Leader已經響應客戶端了,說明Leader已經提交日誌了,並且大多數Follower已經進行了預寫日誌,只是目前還沒有提交日誌,那這個日誌會被刪除嗎?

不會,因為Leader只能追加日誌,而不能刪除日誌。發生這種情況,說明大多數Follower已經進行了預寫日誌,這個寫請求是成功的,那新的Leader也一定會包含這條日誌(如果不包含這條日誌,說明日誌完整度不高,會在選舉中落敗),新的Leader會完成前任Leader的“遺囑”,完成這個日誌的完全提交(所有Follower都提交)。

Leader完全特性

Leader完全特性指的是某個日誌在某個Term中已經提交了,那麼這個日誌必定會出現在更大的Term日誌中。

日誌匹配特性

日誌匹配特性在上文已經說過了,就是Follower在接收到Leader的日誌後,會進行一致性檢查,如果一致性檢查失敗,會進行回溯,找到兩者日誌最近的一致點,Follower會刪除衝突的日誌,與Leader保持一致。

部落格到這裡就結束了,在寫部落格的時候,翻閱了很多文章,很多文章寫的挺細緻,挺優秀,但是真正讀起來,並不是那麼好理解,所以本篇部落格的目標就是坐上馬桶上也能看懂。

由於本人水平有限,並沒有閱讀過etcd的原始碼,也沒有讀過Raft的論文,所以部落格中可能會有不少錯誤,還希望大家指出。

相關文章