Raft 算法之日誌複製

原文地址: https://qeesung.github.io/202...
Raft 論文地址:https://ramcloud.atlassian.ne...html

Raft論文中分爲三塊:git

  • 領導選舉
  • 日誌複製
  • 安全性

本文中主要介紹日誌複製github

領導人必須從客戶端接收日誌而後複製到集羣中的其餘節點,而且強制要求其餘節點的日誌保持和本身相同。數組

鑑於日誌複製這一塊比較複雜,能夠結合下面兩個網頁來理解:安全

複製狀態機

複製狀態機一般都是基於複製日誌實現的,每個服務器存儲一個包含一系列指令的日誌,而且按照日誌的順序進行執行。以下圖所示:服務器

  1. 客戶端請求服務器,請求的信息就是一系列的指明,好比PUT KEY VALUE
  2. 服務器在收到請求之後,將操做指令同步到全部的服務器中
  3. 服務器收到同步的指令之後,就將指令應用到狀態機中
  4. 最後響應客戶端操做成功

raft-state-machine.png

狀態機是按照同步到服務器上的指令的順序,一個一個的去Apply指令,因此指令的順序很重要,若是指令Apply的順序不一致,或者丟失部分指令,那麼最終狀態機的狀態也會不一致。網絡

而咱們知道網絡是不穩定的,好比延遲,分區,丟包,榮譽和亂序等錯誤。若是不保證狀態機Apply指令徹底如出一轍,那麼將會致使不一致的結果。而Raft的日誌複製機制則能保證在發生上述網絡問題的時候,全部的服務器都能同步到徹底如出一轍的日誌,也就是說能保證每個服務器的狀態機Apply到徹底如出一轍的指令。併發

附加日誌RPC

請求體結構

全部服務器上都維持的狀態:優化

  • commitIndex:最大的已經被提交的日誌索引
  • lastApplied:最後被應用到狀態的日誌條目索引

lastApplied應該小於等於commitIndex,由於只有在日誌被提交之後才能被Apply到狀態機中spa

領導人常常改變的:

  • nextIndex[]:對於每個服務器,須要發送給他的下一個日誌條目的索引值,初始化爲當前領導人的最後的日誌索引值加一
  • matchIndex[]:對於每個服務器,已經複製給他的日誌的最高索引值

nextIndex[]matchIndex[]的長度等於整個集羣中的服務器數量。

附加日誌RPC的請求結構:

  • term:附加日誌的領導人任期號
  • leaderId:當前領導人的Id
  • preLogIndex:當前要附加的日誌entries的上一條的日誌索引
  • preLogTerm:當前要附加的日誌entries的上一條的日誌任期號
  • entries[]:須要附加的日誌條目(心跳時爲空)
  • leaderCommit:當前領導人已經提交的最大的日誌索引值

preLogIndexpreLogTerm主要是用於跟隨者檢測當前領導人要附加的日誌是否和跟隨者當前的日誌匹配,若是不匹配的話,那麼就須要繼續向前搜尋和領導人匹配的日誌(下面章節會介紹)

leaderCommit的話用於告訴跟隨者當前提交到什麼位置了(由於收到附加日誌還不能立刻提交,不然可能存在日誌丟失的狀況),以便跟隨者將已經提交的日誌Apply到狀態機中

附加日誌RPC的響應結構:

  • term:跟隨者的當前的任務號
  • success:跟隨者是否接收了當前的日誌,在preLogIndexpreLogTerm匹配的狀況下爲true,不然返回false

請求的流程

每個日誌條目存儲一條狀態機指令和從領導人收到這條指令時的任期號。日誌中的任期號用來檢查是否出現不一致的狀況,每一條日誌條目同時也都有一個整數索引值來代表它在日誌中的位置。

raft-commit-entries.png

領導人附加日誌

一旦成爲領導人,那麼領導人就會在固定在必定時間內發送空的附加日誌,也就是心跳,以組織更隨着超時。

若是領導人收到來本身客戶端的請求,那麼首先將請求的指令附加到本身的日誌隊列中,而後領導人會將新附加的日誌條目經過附加日誌的RPC發送到全部的跟隨者。

領導人須要附加那些日誌?

領導人中維護了兩個常常變更的屬性nextIndex[]matchIndex[],用於記錄要發什麼日誌和跟隨者收到了什麼日誌。

其中nextIndex[]記錄了須要發送給每個服務器的日誌的下一個索引值,這個數組會在領導人被選舉出來的時候初始化爲領導人最後的索引加一。注意這個nextIndex[]只是記錄了要發給下一次附加日誌要發給服務器的索引值,這個索引值可能並不必定準確,好比在跟隨者和領導人的日誌不一致的狀況下,nextIndex[]的值就須要進行遞減以找到和跟隨者最大的匹配的日誌,具體流程後文中會解釋。

matchIndex[]主要是用來記錄跟隨者收到了那些日誌,以方便領導人確認是否當前的日誌能夠進行提交,由於必需要至少N/2+1的集羣成員(包括領導人本身)都確認收到了日誌才能夠進行的日誌的提交。

領導人收到收到跟隨者附加日誌的響應該如何處理?

咱們知道領導人在附加日誌的時候是併發的向全部跟隨者發起的,而且是在固定週期內定時發送的,因此可能在上一個RPC沒有收到響應的狀況下,發出下一個RPC請求,或者是收到過時的日誌追加請求等等。

  1. 比對響應的Term和當前的Term以確認本身是否過時:

    • 若是響應的Term大於當前的Term,那麼說明當前的領導人已通過期,立刻將本身切換爲跟隨者
    • 若是響應的Term小於當前的Term,那麼說明當前的收到了過時的響應(可能網路延遲致使),那麼忽略
    • 不然進行步驟2
  2. 判斷附加RPC時的preLogIndex和當前的nextIndex[peer]-1 是否相等,用於判斷是不是過時的響應,或者是nextIndex[peer]是否被更改了:

    • 若是不相等,那麼直接忽略
    • 不然進行步驟3
  3. 判斷響應的success是否爲真:

    • 若是爲真:那麼說明附加日誌成功,進行步驟4
    • 若是爲假:那麼說明附加日誌失敗,preLogIndexpreLogTerm和跟隨者的日誌不匹配,進行步驟5
  4. 附加日誌成功之後,須要更新matchIndex[peer]中的值爲已經追加日誌的索引值,表示追加日誌成功,判斷整個matchIndex[]數組中的是否有一半都大於等於追加日誌的索引:

    • 若是是:那麼更新commitIndex,進行日誌的提交
    • 不然跳過處理
  5. 日誌不匹配,那麼須要找到下一個和跟隨者匹配的日誌索引,簡單一點能夠經過遞減nextIndex[peer]來實現。

跟隨者接收日誌

跟隨者收到附加日誌的請求,不能簡單的將日誌追加到本身的日誌後面,由於跟隨者的日誌可能和領導人有衝突,或者跟隨者缺失更多的日誌,入下圖所示。

raft-conflict-log.png

那麼必定要確保本次附加日誌的以前的全部日誌都相同,也就是說附加當前的日誌以前,缺日誌就要把缺失的日誌補上,日誌衝突了,就要把衝突的日誌覆蓋(領導人能夠強行覆蓋跟隨者的日誌)

  1. 判斷附加日誌任期Term和當前的Term是否相同:

    • 若是請求的Term小於當前的Term,那麼說明收到了來自過時的領導人的附加日誌請求,那麼拒接處理。
    • 若是請求的Term大於當前的Term,那麼更新當前的Term爲請求的Term,進行步驟2
    • 若是請求的Term和當前的Term相等,那麼說明請求合法,進行步驟2
  2. 判斷preLogIndex是否大於當前的日誌長度或者preLogIndex位置處的任期是否和preLogTerm相等以檢測要附加的日誌以前的日誌是否匹配:

    • 若是preLogIndex的長度大於當前的日誌的長度,那麼說明跟隨者缺失日誌,那麼拒絕附加日誌,返回false
    • 若是preLogIndex處的任期和preLogTerm不相等,那麼說明日誌有衝突,拒絕附加日誌,返回false
    • 不然說明以前的日誌全都匹配,那麼進行步驟3,檢測preLogIndex以後的日誌是否匹配。
  3. 逐一比對要附加的日誌entries[]是否和本身preLogIndex以後的日誌是否有衝突:

    • 若是entries[]中的任何一位置的日誌發生衝突,那麼須要將以後的日誌進行截斷,並追加爲entries[]中以後的日誌
    • 若是沒有發生衝突,那麼存在兩種狀況:

      • 跟隨者的日誌和entries[]的日誌所有匹配,這種狀況多是重複的附加日誌RPC,那麼這種狀況只會簡單的校驗一遍全部日誌
      • entries[]的日誌匹配當前的全部日誌,那麼將沒有匹配的日誌全都追加到當前的日誌後面。

上述的步驟3是很重要的,若是不對現有的日誌進行比對,並且簡單的進行截斷追加日誌,那麼是很危險,由於可能收到延時的重複日誌附加請求而致使日誌沒必要要的截斷,從而致使已經提交的日誌丟失。

另外還有一個優化點,若是存在大量的衝突日誌,那麼若是經過遞減nextIndex[peer]將會很慢,因此能夠經過批量跳過沖突日誌方式來作到,能夠再響應中添加conflictIndexconflictTerm來作到,這裏不展開詳細討論。

日誌的提交和應用

全部的服務器都有兩個常常變更的值commitIndexlastAppliedcommitIndex表示已經提交的日誌索引,而lastApplied表示最後Apply到狀態機中的日誌索引。commtiIndex會在日誌成功附加到集羣中的N/2+1的節點上更新。

通常應用日誌到狀態機中是經過一個獨立的線程來作到的,經過監控是否有新提交的日誌,若是有新提交的日誌,那麼就將日誌Apply到狀態機中,而且更新lastApplied。因此通常commitIndex >= lastApplied

在實際的應用中,通常將Raft單獨做爲獨立的一層共識模塊,上層模塊將須要達到共識的指令下發給Raft共識模塊,在Raft模塊達到共識之後,就將達成共識的指令Apply到上層模塊中。好比EtcdTiKV等等

相關文章
相關標籤/搜索