raft協議

蒙恬括發表於2022-04-17

一、Raft一致性演算法

  Eureka:Peer To Peer,每個節點的地位都是均等的,每個節點都可以接收寫入請求,每個節點接收請求之後,進行請求打包處理,非同步化延遲一點時間,將資料同步給 Eureka 叢集當中的其他節點。任何一臺節點當機之後,理論上應該是不影響叢集執行的,都可以從其他節點獲取登錄檔資訊。
  Etcd、Consul,Zookeeper, Nacos,其中的 CP 模式也是基於 Raft 協議 來實現分散式一致性演算法的。當然 Zookeeper 在 Raft 協議基礎上做了一些改良,使用的 ZAB 分散式一致性協議來實現的。
  1. 基本概念
1. leader選舉:當已有的leader故障時必須選出一個新的leader。
2. 日誌複製:leader接受來自客戶端的命令,記錄為日誌,並複製給叢集中的其他伺服器,並強制其他節點的日誌與leader保持一致。
3. 安全safety措施:通過一些措施確保系統的安全性,如確保所有狀態機按照相同順序執行相同命令的措施。
伺服器三種角色:leader、candidate、follower
1. follower只會響應leader和candidate的請求,
2. 客戶端的請求則全部由leader處理,
3. 有客戶端請求了一個follower也會將請求重定向到leader。
 
    叢集剛啟動時,所有節點都是follower,之後在time out訊號的驅使下,follower會轉變成candidate去拉取選票,獲得大多數選票後就會成為leader,這時候如果其他候選人發現了新的leader已經誕生,就會自動轉變為follower;而如果另一個time out訊號發出時,還沒有選舉出leader,將會重新開始一次新的選舉。
  Raft協議中,將時間分成了一些任意長度的時間片,稱為term,term使用連續遞增的編號的進行識別。
每一個term都從新的選舉開始,candidate們會努力爭取稱為leader。一旦獲勝,它就會在剩餘的term時間內保持leader狀態,在某些情況下(如term3)選票可能被多個candidate瓜分,形不成多數派,因此term可能直至結束都沒有leader,下一個term很快就會到來重新發起選舉。
 
  term也起到了系統中邏輯時鐘的作用,每一個server都儲存了當前term編號,在server之間進行交流的時候就會帶有該編號,如果一個server的編號小於另一個的,那麼它會將自己的編號更新為較大的那一個;如果leader或者candidate發現自己的編號不是最新的了,就會自動轉變為follower;如果接收到的請求的term編號小於自己的當前term將會拒絕執行。
 
server之間的交流是通過RPC進行的。只需要實現兩種RPC就能構建一個基本的Raft叢集:
* RequestVote RPC:它由選舉過程中的candidate發起,用於拉取選票
* AppendEntries RPC:它由leader發起,用於複製日誌或者傳送心跳訊號。

2. leader選舉

  Raft通過心跳機制發起leader選舉。節點都是從follower狀態開始的,如果收到了來自leader或candidate的RPC,那它就保持follower狀態,避免爭搶成為candidate。Leader會傳送空的AppendEntries RPC作為心跳訊號來確立自己的地位,如果follower一段時間(election timeout)沒有收到心跳,它就會認為leader已經掛了,發起新的一輪選舉。選舉發起後,一個follower會增加自己的當前term編號並轉變為candidate。它會首先投自己一票,然後向其他所有節點並行發起RequestVote RPC。
candidate狀態將可能發生如下三種變化:
  1. 贏得選舉,成為leader(如果它在一個term內收到了大多數的選票,將會在接下的剩餘term時間內稱為leader,然後就可以通過傳送心跳確立自己的地位。每一個server在一個term內只能投一張選票,並且按照先到先得的原則投出)
  2. 其他server成為leader(在等待投票時,可能會收到其他server發出AppendEntries RPC心跳訊號,說明其他leader已經產生了。這時通過比較自己的term編號和RPC過來的term編號,如果比對方大,說明leader的term過期了,就會拒絕該RPC,並繼續保持候選人身份; 如果對方編號不比自己小,則承認對方的地位,轉為follower.)
  3. 選票被瓜分,選舉失敗(如果沒有candidate獲取大多數選票, 則沒有leader產生, candidate們等待超時後發起另一輪選舉. 為了防止下一次選票還被瓜分,必須採取一些額外的措施, raft採用隨機election timeout的機制防止選票被持續瓜分。通過將timeout隨機設為一段區間上的某個值, 因此很大概率會有某個candidate率先超時然後贏得大部分選票.)
  1. 日誌複製過程
  客戶端提交每一條命令都會被按順序記錄到leader的日誌中,每一條命令都包含term編號和順序索引,然後向其他節點並行傳送AppendEntries RPC用以複製命令(如果命令丟失會不斷重發),當複製成功也就是大多數節點成功複製後,leader就會提交命令,即執行該命令並且將執行結果返回客戶端,raft保證已經提交的命令最終也會被其他節點成功執行。leader會儲存有當前已經提交的最高日誌編號。順序性確保了相同日誌索引處的命令是相同的,而且之前的命令也是相同的。當傳送AppendEntries RPC時,會包含leader上一條剛處理過的命令,接收節點如果發現上一條命令不匹配,就會拒絕執行。
  特殊故障:如果leader崩潰了,它所記錄的日誌沒有完全被複制,會造成日誌不一致的情況,follower相比於當前的leader可能會丟失幾條日誌,也可能會額外多出幾條日誌,這種情況可能會持續幾個term。
    在上圖中,框內的數字是term編號,a、b丟失了一些命令,c、d多出來了一些命令,e、f既有丟失也有增多,這些情況都有可能發生。比如f可能發生在這樣的情況下:f節點在term2時是leader,在此期間寫入了幾條命令,然後在提交之前崩潰了,在之後的term3中它很快重啟並再次成為leader,又寫入了幾條日誌,在提交之前又崩潰了,等他甦醒過來時新的leader來了,就形成了上圖情形。在Raft中,leader通過強制follower複製自己的日誌來解決上述日誌不一致的情形,那麼衝突的日誌將會被重寫。為了讓日誌一致,先找到最新的一致的那條日誌(如f中索引為3的日誌條目),然後把follower之後的日誌全部刪除,leader再把自己在那之後的日誌一股腦推送給follower,這樣就實現了一致。而尋找該條日誌,可以通過AppendEntries RPC,該RPC中包含著下一次要執行的命令索引,如果能和follower的當前索引對上,那就執行,否則拒絕,然後leader將會逐次遞減索引,直到找到相同的那條日誌。
    然而這樣也還是會有問題,比如某個follower在leader提交時當機了,也就是少了幾條命令,然後它又經過選舉成了新的leader,這樣它就會強制其他follower跟自己一樣,使得其他節點上剛剛提交的命令被刪除,導致客戶端提交的一些命令被丟失了
Raft通過為選舉過程新增一個限制條件,解決了上面提出的問題,該限制確保leader包含之前term已經提交過的所有命令。Raft通過投票過程確保只有擁有全部已提交日誌的candidate能成為leader。由於candidate為了拉選票需要通過RequestVote RPC聯絡其他節點,而之前提交的命令至少會存在於其中某一個節點上,因此只要candidate的日誌至少和其他大部分節點的一樣新就可以了, follower如果收到了不如自己新的candidate的RPC,就會將其丟棄.
    還可能會出現另外一個問題, 如果命令已經被複制到了大部分節點上,但是還沒來的及提交就崩潰了,這樣後來的leader應該完成之前term未完成的提交. Raft通過讓leader統計當前term內還未提交的命令已經被複制的數量是否半數以上, 然後進行提交.
  1. 日誌壓縮
  隨著日誌大小的增長,會佔用更多的記憶體空間,處理起來也會耗費更多的時間,對系統的可用性造成影響,因此必須想辦法壓縮日誌大小。Snapshotting是最簡單的壓縮方法,系統的全部狀態會寫入一個snapshot儲存起來,然後丟棄截止到snapshot時間點之前的所有日誌。
  每一個server都有自己的snapshot,它只儲存當前狀態,如上圖中的當前狀態為x=0,y=9,而last included index和last included term代表snapshot之前最新的命令,用於AppendEntries的狀態檢查。
  雖然每一個server都儲存有自己的snapshot,但是當follower嚴重落後於leader時,leader需要把自己的snapshot傳送給follower加快同步,此時用到了一個新的RPC:InstallSnapshot RPC。follower收到snapshot時,需要決定如何處理自己的日誌,如果收到的snapshot包含有更新的資訊,它將丟棄自己已有的日誌,按snapshot更新自己的狀態,如果snapshot包含的資訊更少,那麼它會丟棄snapshot中的內容,但是自己之後的內容會儲存下來。

二、zab對比raft

1. 上一輪次的leader的殘留的資料:
  Raft:對於之前term的過半或未過半複製的日誌採取的是保守的策略,全部判定為未提交,只有噹噹前term的日誌過半了,才會順便將之前term的日誌進行提交
  ZooKeeper:採取激進的策略,對於所有過半還是未過半的日誌都判定為提交,都將其應用到狀態機中
2. 怎麼阻止上一輪次的leader假死的問題
  Raft的copycat實現為:每個follower開通一個複製資料的RPC介面,誰都可以連線並呼叫該介面,所以Raft需要來阻止上一輪次的leader的呼叫。每一輪次都會有對應的輪次號,用來進行區分,Raft的輪次號就是term,一旦舊leader對follower傳送請求,follower會發現當前請求term小於自己的term,則直接忽略掉該請求,自然就解決了舊leader的干擾問題
  ZooKeeper:一旦server進入leader選舉狀態則該follower會關閉與leader之間的連線,所以舊leader就無法傳送複製資料的請求到新的follower了,也就無法造成干擾了
3. raft流程
1. client連線follower或者leader,如果連線的是follower則,follower會把client的請求(寫請求,讀請求則自身就可以直接處理)轉發到leader
2. leader接收到client的請求,將該請求轉換成entry,寫入到自己的日誌中,得到在日誌中的index,會將該entry傳送給所有的follower(實際上是批量的entries)
3. follower接收到leader的AppendEntries RPC請求之後,會將leader傳過來的批量entries寫入到檔案中(通常並沒有立即重新整理到磁碟),然後向leader回覆OK
4. leader收到過半的OK回覆之後,就認為可以提交了,然後應用到leader自己的狀態機中,leader更新commitIndex,應用完畢後回覆客戶端
5. 在下一次leader發給follower的心跳中,會將leader的commitIndex傳遞給follower,follower發現commitIndex更新了則也將commitIndex之前的日誌都進行提交和應用到狀態機中
4. zab流程
1. client連線follower或者leader,如果連線的是follower則,follower會把client的請求(寫請求,讀請求則自身就可以直接處理)轉發到leader
2. leader接收到client的請求,將該請求轉換成一個議案,寫入到自己的日誌中,會將該議案傳送給所有的follower(這裡只是單個傳送)
3. follower接收到leader的議案請求之後,會將該議案寫入到檔案中(通常並沒有立即重新整理到磁碟),然後向leader回覆OK
4. leader收到過半的OK回覆之後,就認為可以提交了,leader會向所有的follower傳送一個提交上述議案的請求,同時leader自己也會提交該議案,應用到自己的狀態機中,完畢後回覆客戶端
5. follower在接收到leader傳過來的提交議案請求之後,對該議案進行提交,應用到狀態機中
5. 連續性日誌:
  如果是連續性日誌,則leader在分發給各個follower的時候,只需要記錄每個follower目前已經同步的index即可,如Raft
  如果是非連續性日誌,如ZooKeeper,則leader需要為每個follower單獨儲存一個佇列,用於存放所有的改動,如ZooKeeper,一旦是佇列就引入了一個問題即順序性問題,即follower在和leader進行同步的時候,需要阻塞leader處理寫請求,先將follower和leader之間的差異資料先放入佇列,完成之後,解除阻塞,允許leader處理寫請求,即允許往該佇列中放入新的寫請求,從而來保證順序性
  • 正常情況下:
  Raft對請求先轉換成entry,複製時,也是按照leader中log的順序複製給follower的,對entry的提交是按index進行順序提交的,是可以保證順序的。
  ZooKeeper在提交議案的時候也是按順序寫入各個follower對應在leader中的佇列,然後follower必然是按照順序來接收到議案的,對於議案的過半提交也都是一個個來進行的。
  • 異常情況:follower掛掉又重啟的過程:
  Raft:重啟之後,由於leader的AppendEntries RPC呼叫,識別到leader,leader仍然會按照leader的log進行順序複製,也不用關心在複製期間新的新增的日誌,在下一次同步中自動會同步。
  ZooKeeper:重啟之後,需要和當前leader資料之間進行差異的確定,同時期間又有新的請求到來,所以需要暫時獲取leader資料的讀鎖,禁止此期間的資料更改,先將差異的資料先放入佇列,差異確定完畢之後,還需要將leader中已提交的議案和未提交的議案也全部放入佇列,即ZooKeeper的2個集合資料,讀寫鎖。
 
  • 會不會有亂序的問題?
  Raft:Raft對於之前term的entry被過半複製暫不提交,只有當本term的資料提交了才能將之前term的資料一起提交,也是能保證順序的
  ZooKeeper:ZooKeeper每次leader選舉之後都會進行資料同步,不會有亂序問題
總結:2PC (兩階段提交) + 叢集過半節點寫機制

三、分割槽

  目前ZooKeeper和Raft都是過半即可,所以對於分割槽是容忍的。如5臺機器,分割槽發生後分成2部分,一部分3臺,另一部分2臺,這2部分之間無法相互通訊
  其中,含有3臺的那部分,仍然可以湊成一個過半,仍然可以對外提供服務,但是它不允許有server再掛了,一旦再掛一臺則就全部不可用了。
含有2臺的那部分,則無法提供服務,即只要連線的是這2臺機器,都無法執行相關請求。
 
參考:
 

 

相關文章