日誌一致性協議Raft

前言

       數據一致性是分佈式服務的一大難題。假設我們能無成本的保證數據一致性,在分佈式環境中就可以使用無限擴容來分發流量。那麼數據一致性,何爲數據 ?往往日誌log是一應用的可靠性的保障,例如 mysql的binlog,redis 的aof日誌。通過分析日誌從機子可以達到和本機子相同的狀態。因此數據一致性的問題,化簡爲日誌一致性問題。Raft則是一個用於管理日誌一致性的協議

Raft簡介

Raft又稱 木筏協議,一種共識算法,旨在替代PaxOS,相比容易理解許多。斯坦福大學的兩個博士Diego Ongaro和John Ousterhout兩個人以易懂爲目標設計的一致性算法,2013以論文方式發佈。由於易懂,不從論文發佈到不到兩年的時間,即出現大量的Raft開源實現。爲簡化PaxOS的晦澀,它採用分而治之的辦法,將算法分爲三個子問題:選舉(Leader Election)、日誌複製(Log Replication)、安全性(Safety)和集羣成員動態變更(Cluster Membership Changes)。

Raft選舉

Raft 中node 的 3 種角色/狀態
在這裏插入圖片描述

  1. Follower:完全被動,不能發送任何請求,只接受並響應來自 Leader 和 Candidate 的 Message,每個節點啓動後的初始狀態一定是 Follower;
  2. Leader:處理所有來自客戶端的請求,以及複製 Log 到所有 Followers;
  3. Candidate:用來競選一個新 Leader (Candidate 由 Follower 觸發超時而來)。
Follower --> Candidate
  1. Follower沒收到心跳的時間 > Heartbeat Timeout
Candidate --> Candidate (選舉超時)
  1. 處於 Candidate 狀態的時間 > Election Timeout
Candidate(選舉)
  1. 先建個term並投票給自己
  2. Candidate 向其它節點 node發投票請求
  1. Candidate.term小於 node. term,失敗並降級爲Follower
  2. Candidate.term大於 node. term,成功如果node也是Candidate則降級,並更新term
  3. Candidate.term等於node. term,如果node也是Candidate失敗,否則4
  4. 比較上一條日誌的term,如果node大則失敗,小則成功 ,等於到5
  5. 比較上一條日誌的index,如果node大則失敗,否則成功
Candidate --> Follower
  1. Candidate選舉時,發現其它節點term > 自己term.
Candidate --> Leader
  1. Candidate選舉結果 > 總節點數/2

獲取Leader結點特性
1.有最大的Term;
2. 如果Term相同,則有最大的Index;
3. 如果Term相同,Index也相同,就看誰最先發起;

Leader(維持)
  1. 向所有結點發送心跳和日誌
Leader --> Follower
  1. 發現其它節點term > 自己term.

Raft日誌複製

日誌複製是分佈式一致性算法的核心,所謂的一致性,就是集羣多節點的數據一致性。

日誌的數據結構
  1. 創建日誌時的任期號term(用來檢查節點日誌是否出現不一致的情況)
  2. 狀態機需要執行的指令(真正的內容)
  3. 索引index:整數索引表示日誌條目在日誌中位置

Raft 把每條日誌都附加了任期號和下標來保證日誌的唯一性。如果 2 個日誌的相同的索引位置的日誌條目的任期號相同,那麼 Raft 就認爲這個日誌從頭到這個索引之間全部相同。

日誌的複製過程

在這裏插入圖片描述

  1. 客戶端的向服務端發送請求(指令)。
  2. follower接收到請求轉發給leader
  3. leader 把這個指令作爲一條新的日誌條目添加到日誌中,然後並行發起 RPC 給其他的服務器,讓他們複製這條信息。
  4. 假如這條日誌被安全的複製 > 總結點/2,leader就提交這條日誌並返回給客戶端。
  5. 關於日誌複製異常的Follower,Raft異步會通過強制 follower 直接複製 leader 的日誌解決
日誌衝突及解決

下圖,所有的 follower 都和 leader 的日誌衝突了,leader 的最後一條日誌的任期是 6, 下標是 10 ,而其他 follower 的日誌都與 leader 不匹配。
在這裏插入圖片描述
一致性操作:

  1. a follower 不需要刪除任何條目,
  2. b 也不需要刪除
  3. c follower 需要刪除最後一個條目
  4. d follower 需要刪除最後 2 個任期爲 7 的條目,
  5. e 需要刪除最後 2 個任期爲 4 的條目,
  6. f 則比較厲害,需要刪除 下標爲 3 之後的所有條目。

具體思路:

  1. Leader 爲每一個 follower 維護一個下標,稱之爲 nextIndex,表示下一個需要發送給 follower 的日誌條目的索引。
  2. 當一個新 leader 剛獲得權力的時候,他將自己的最後一條日誌的 index + 1,也就是上面提的 nextIndex 值,如果一個 follower 的日誌和 leader 不一致,那麼在下一次 RPC 附加日誌請求中,一致性檢查就會失敗(不會插入數據)。
  3. 如果檢查失敗,leader 就會把 nextIndex 遞減進行重試,直到遇到匹配到正確的日誌。當匹配成功之後,follower 就會把衝突的日誌全部刪除,此時,follower 和 leader 的日誌就達成一致。

安全性

Raft在非拜占庭錯誤情況下,包括網絡延遲、分區、丟包、冗餘和亂序等錯誤都可以保證正確,不會返回錯誤結果,這就是安全性保證。

  • Leader選舉之後,如果Follower與Leader日誌有衝突該如何處理?
    :通過強制 follower直接複製 leader 的日誌解決,詳情見上文。
  • 如果在一個Follower宕機的時候Leader提交了若干的日誌條目,然後這個Follower上線後可能會被選舉爲Leader並且覆蓋這些日誌條目,如此就會產生不一致?
    :candidate發送出去的投票請求消息必須帶上其最後一條日誌條目的Index與Term;接收者需要判斷該Index與Term至少與本地日誌的最後一條日誌條目一樣新,否則不給投票。因爲前一個Leader提交日誌條目的條件是日誌複製給集羣中的過半成員,選舉爲Leader的條件也是需要過半成員的投票。

在這裏插入圖片描述

  • a) S1是Leader,並且部分地複製了index-2;
  • b) S1宕機,S5得到S3、S4、S5的投票當選爲新的Leader(S2不會選擇S5,因爲S2的日誌較S5新),並且在index-2寫入到一個新的條目,此時是term=3(注:之所以是term=3,是因爲在term-2的選舉中,S3、S4、S5至少有一個參與投票,也就是至少有一個知道term-2,雖然他們沒有term-2的日誌);
  • c) S5宕機,S1恢復並被選舉爲Leader,並且開始繼續複製日誌(也就是將來自term-2的index-2複製給了S3),此時term-2,index-2已經複製給了多數的服務器,但是還沒有提交;
  • d) S1再次宕機,並且S5恢復又被選舉爲Leader(通過S2、S3、S4投票,因爲S2、S3、S4的term=4<5,且日誌條目(爲term=2,index=2)並沒有S5的日誌條目新,所以可以選舉成功),然後覆蓋Follower中的index-2爲來自term-3的index-2;(注:此時出現了,term-2中的index-2已經複製到三臺服務器,還是被覆蓋掉);
  • e) 然而,如果S1在宕機之前已經將其當前任期(term-4)的條目都複製出去,然後該條目被提交(那麼S5將不能贏得選舉,因爲S1、S2、S3的日誌term=4比S5都新)。此時所有在前的條目都會被很好地提交。
  • 如果上述情況©中,term=2,index=2的日誌條目被複制到大多數後,如果此時當選的S1提交了該日誌條目,則後續產生的term=3,index=2會覆蓋它,此時就可能會在同一個index位置先後提交一個不同的日誌,這就違反了狀態機安全性,產生不一致。
    :爲了消除上述場景就規定Leader可以複製前面任期的日誌,但是不會主動提交前面任期的日誌。而是通過提交當前任期的日誌,而間接地提交前面任期的日誌。


  • 網絡分區會不會產生不一致的狀況
    :機子是2n+1的個數。如產生分區,機子小於等於n的分區,將不會產生日誌數據以及term不再增加。如果分區合併,n分區的機子板本落後也不可能成爲Leader

線性一致性

所謂線性一致性,就是在 t1 的時間我們寫入了一個值,那麼在 t1 之後,我們的讀一定能讀到這個值,不可能讀到 t1 之前的值。

在這裏插入圖片描述

Mysql 主從異步複製,你t1時間向主庫插入數據x,t2時間去從庫讀數據x,是有可能讀不到的。而線性一致性系統則是必須讀到,即CAP理論中的C.

爲什麼Raft中沒有線性一致性

Raft中有一個狀態機,專門用來執行日誌,相當於Mysql的解析執行器。狀態機執行是要花時間的,Follower沒這麼快。

怎麼實現線性一致性
  • Raft log
    • 任何的讀請求走一次Leader
    • Leader 將讀請求也記到log裏面(像寫請求一樣)
    • 等這個 log 提交之後,在 apply 的時候從狀態機裏面讀取值

點評:非常方便的實現線性 read,但每次read 都需要走 Raft流程,全部依賴Leader, Follower在浪費。

  • ReadIndex Read
    • 讀請進來,將Leader commit index 記錄到一個 local 變量 ReadIndex
    • 向其他節點發起一次 heartbeat,如果大多數節點返回了對應的 heartbeat response,那麼 leader 就能夠確定現在自己仍然是 leader。(防止網絡分區了,自己不知道)
    • 將Leader commit index (Follower通過網絡獲取)記錄到一個 local 變量 ReadIndex
    • 等待自己的狀態機執行,直到 apply index 超過了 ReadIndex

點評:Follower可通過網絡獲取ReadIndex。Follower幫助分擔了Learder的部份壓力,向其他節點發起一次 heartbeat仍是Learder的負擔

Lease Read
- 讀請進來,將Leader commit index 記錄到一個 local 變量 ReadIndex
- heartbeat + election timeout內默認自己是leader。超過時間 向其他節點發起一次 heartbeat並刷新heartbeat
- 將Leader commit index (Follower通過網絡獲取)記錄到一個 local 變量 ReadIndex
- 等待自己的狀態機執行,直到 apply index 超過了 ReadIndex

點評:優化了ReadIndex Read ,進一步節省了heartbeat對Leader的負擔

Raft存儲及日誌壓縮

在這裏插入圖片描述
在實際的系統中,不能讓日誌無限增長,否則系統重啓時需要花很長的時間進行回放,從而影響可用性。Raft採用對整個系統進行snapshot來解決,snapshot之前的日誌都可以丟棄。

集羣成員動態變更

集羣配額從 3 臺機器變成了 5 臺,可能存在這樣的一個時間點,兩個不同的領導者在同一個任期裏都可以被選舉成功(雙主問題),一個是通過舊的配置,一個通過新的配置。
在這裏插入圖片描述

解決方法

Raft解決方法是每次成員變更只允許增加或刪除一個成員(如果要變更多個成員,連續變更多次)。
在這裏插入圖片描述

Paxos算法

Client   Proposer      Acceptor     Learner
   |         |          |  |  |       |  |
   X-------->|          |  |  |       |  |  Request
   |         X--------->|->|->|       |  |  Prepare(1)
   |         |<---------X--X--X       |  |  Promise(1,{Va,Vb,Vc})
   |         X--------->|->|->|       |  |  Accept!(1,V)
   |         |<---------X--X--X------>|->|  Accepted(1,V)
   |<---------------------------------X--X  Response
   |         |          |  |  |       |  |

我的RaftDemo

/**
 *
 * 1.electionTime最終時間短的會選爲主
 * 2.日誌存儲和讀取
 */
public class RaftTest {

    public static void main(String[] args) {

        List<String> list = Lists.newArrayList("localhost:8771", "localhost:8772", "localhost:8773", "localhost:8774", "localhost:8775");

        List<RpcServer> ss = Lists.newArrayList();

        for (int i = 0; i < list.size(); i++) {

            String url = list.get(i);

            RpcServer server = new RpcServer(url);

            ss.add(server);
            if (i == 0) {
                server.setElectionTime(500l);
            }
            server.start();
        }

        ThreadUtil.sleep(3000);

        //結果大概率是8771
        for (int i = 0; i < ss.size(); i++) {

            if(i == ss.size() -1) {
                i = 0;
                ThreadUtil.sleep(2000);
            }
            RpcServer server = ss.get(i);
            if(server.isLeader()){
                System.out.println("leader node is " + server.getUrl());
                break;
            }

        }

        //讓node結點接收一波心跳新
        ThreadUtil.sleep(2000);

        RpcClient rpcClient = new RpcClient();
        ClientKVReq putReq = ClientKVReq.newBuilder()
                .type(0)
                .key("yoyo")
                .value("eee")
                .build();
        ClientKVAck putRes = rpcClient.sendMassage(putReq);
        System.out.println(putRes.getResult());


        //讓node結點接收一波心跳新
        //ThreadUtil.sleep(20000);


        ClientKVReq getReq = ClientKVReq.newBuilder()
                .type(1)
                .key("yoyo")
                .build();
        ClientKVAck getRes = rpcClient.sendMassage(getReq);
        System.out.println(getRes.getResult());

    }
}

運行結果:

[localhost:8771] electionTime:500
[localhost:8774] electionTime:6785
[localhost:8772] electionTime:4948
[localhost:8775] electionTime:3759
[localhost:8773] electionTime:4643
[localhost:8772] stepDown become FOLLOWER
[localhost:8774] stepDown become FOLLOWER
[localhost:8774] agree [localhost:8771]
[localhost:8775] stepDown become FOLLOWER
[localhost:8775] agree [localhost:8771]
[localhost:8772] agree [localhost:8771]
[localhost:8773] stepDown become FOLLOWER
[localhost:8773] agree [localhost:8771]
[localhost:8771] become leader
leader node is localhost:8771
LEADER[localhost:8771]  write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8772]  write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8773]  write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8774]  write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8775]  write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
LEADER[localhost:8771]  apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
ok
FOLLOWER[localhost:8772]  apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8774]  apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8773]  apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8775]  apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8772]  ReadIndex to [0] with value:eee
eee

資源地址:https://download.csdn.net/download/y3over/12577334

Raft進階-SOFAJRaft

SOFAJRaft 是一個基於 Raft 一致性算法的生產級高性能 Java 實現,是從百度的 braft 移植而來,做了一些優化和改進。
SOFAStack 開源 SOFAJRaft:生產級 Java Raft 算法庫
SOFAJRaft 選舉機制剖析 | SOFAJRaft 實現原理
SOFAJRaft 線性一致讀實現剖析 | SOFAJRaft 實現原理
SOFAJRaft 實現原理 - 生產級 Raft 算法庫存儲模塊剖析
SOFAJRaft 實現原理 - SOFAJRaft-RheaKV 是如何使用 Raft 的
SOFAJRaft—初次使用

主要參考

Raft 日誌複製 Log replication
編寫你的第一個 Java 版 Raft 分佈式 KV 存儲
Raft協議安全性保證
TiKV 功能介紹 - Lease Read
RAFT算法詳解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章