Raft 協議(附完整實現源碼)


Paxos 存在的問題

Paxos 算法的描述偏學術化,缺失了很多細節,無法直接應用於工程領域。實際工程應用中的分布式算法大多是 Paxos 的變種,驗證這些算法的正確性也成為了一個難題。

舉個例子:上一篇文章的 最后 介紹了一個應用 Paxos 算法的工程模型,這個模型存在明顯的寫性能瓶頸:

  • 使用多主架構,寫入沖突的概率高
  • 每次更新操作都需要至少 2 輪以上的網絡通信,通信開銷大

如果要提高該模型的性能,仍需要在很多細節上做進一步調整,最終實現出來的算法已經和原始的版本的 Paxos 相去甚遠。

為了解決以上問題,另一個高性能且易於理解的一致性算法橫空出世:Raft

在學習算法的過程中,使用 Java 實現了一個功能完善的 Raft 協議:rafting
 
代碼忠實於論文原文,包含了其中的眾多算法細節,希望對各位學習 Raft 的朋友有所幫助

基本概念

Raft 算法基於 復制狀態機Replicated State Machine模型,本質上就是一個管理 日志復制 的算法。

Raft 集群采用 Single Leader 架構,集群中有唯一的 Leader 進程負責管理日志復制,其職責有:

  • 接受 Client 發送的請求
  • 將日志記錄同步到其他進程
  • 告知其他進程的何時能夠提交日志

復制狀態機

復制狀態機的本質就是:Paxos + WAL

每個進程維護一個狀態機 State Machine,並且使用一個日志存儲其所要執行指令。 如果兩個狀態機執行按照相同的順序,執行相同的指令,則這兩個進程最終能夠收斂到同個狀態。如果能保證所有進程的日志一致,則每個進程的狀態必定也是一致的。

任期

為了減少不必要的網絡通信,日志追加順序由集群唯一的 Leader 決定,無須與其他節點協商。通信開銷從最低 2 次降為固定的 1 次,從而大幅提高了算法的性能。

出於可用性考慮,當前 Leader 下線后,集群需要從存活的節點中挑選一個新的 Leader,這個過程被稱為選舉 election

每次選舉都會產生一個新的任期號 term(單調遞增),如果選舉中產生了一個新的 Leader,那么這個任期號會伴隨這個 Leader 直到其下線。

每個 參與者 進程都會維護一個 current_term 用於表示已知的最新任期,進程之間通過彼此交換該值來感知 Leader 變化。

/**
 * 基礎信息
 */
public abstract class RaftMember implements RaftParticipant {

    // 響應 RPC 前需要持久化以下兩個屬性
    protected final long currentTerm; // 已知的最新的 term(初始為 0)
    protected final ID lastCandidate; // 最近一次贊成投票的 candidate

    protected RaftMember(long term, ID candidate) {
        this.currentTerm = term;
        this.lastCandidate = candidate;
        stableStorage().persist(currentTerm, lastCandidate);
    }

    /**
     *  @see RaftParticipant#currentTerm()
     */
    @Override
    public long currentTerm() {
        return currentTerm;
    }

    /**
     *  @see RaftParticipant#votedFor()
     */
    @Override
    public ID votedFor() {
        return lastCandidate;
    }

}

日志

日志是 Raft 的核心概念。Raft 保證日志是連續且一致的,並且最終能夠被所有進程按照日志索引的順序提交。

每條日志記錄包含:

  • 任期term:生成該條記錄的 Leader 對應的任期
  • 索引index:其在日志中的順序
  • 命令command:可執行的狀態機指令

一旦某條日志中的命令被狀態機執行了,那么我們稱這條記錄為已提交committed,Raft 保證已提交的記錄不會丟失。

角色

Raft集群中每個進程只能擔任其中一個角色:

  • Leader:發送心跳、管理日志復制與提交
  • Follower:被動響應其他節點發送過來的請求
  • Candidate:主動發起並參與選舉

Raft 進程間使用 RPC 的方式進行通信,實現最基礎的共識算法只需 兩種RPC

  • RequestVote:用於選舉產生 Leader
  • AppendEntries:復制日志與發送心跳
/**
 * RPC 接口
 * */
public interface RaftService {

    /**
     * 復制日志+發送心跳(由 leader 調用)
     * @param term leader 任期
     * @param leaderId leader 在集群中的唯一標識
     * @param prevLogIndex 緊接在新的之前的日志條目索引
     * @param prevLogTerm prevLogIndex 對應的任期
     * @param entries 日志條目(發送心跳時為空)
     * @param leaderCommit leader 已經提交的日志條目索引
     * @return 當 follower 中的日志包含 prevLogIndex 與 prevLogTerm 匹配的日志條目返回 true
     * */
    Async<RaftResponse> appendEntries(
            long term, ID leaderId,
            long prevLogIndex, long prevLogTerm,
            Entry[] entries, long leaderCommit) throws Exception;

    /**
     * 選主(由 candidate 調用)
     * @param term candidate 任期
     * @param candidateId candidate 在集群中的唯一標識
     * @param lastLogIndex candidate 最后一條日志條目的索引
     * @param lastLogTerm  candidate 最后一條日志條目的任期
     * @return 當收到贊成票時返回 true
     * */
    Async<RaftResponse> requestVote(
            long term, ID candidateId,
            long lastLogIndex, long lastLogTerm) throws Exception;

}

算法流程

基於 Single Leader 模型,Raft 將一致性問題分解為 3 個獨立的子問題:

  • Leader 選舉Election:Leader 進程失效后能夠自動選舉出一個新的 Leader
  • 日志復制Replication:Leader 保證其他節點的日志與其保持一致
  • 狀態安全 Safety:Leader 保證狀態機執行指令的順序與內容完全一致

為了方便理解,下面結合 動畫 進行介紹。

選舉

使用 心跳超時heartbeat timeout機制來觸發 Leader 選舉:

  • 節點啟動時默認處於 Follower 狀態,如果 Follower 超時未收到 Leader 心跳信息,會轉換為 Candidate 並向其他節點發起 RequestVote 請求。
  • 當 Candidate 收到半數以上的選票之后成為 Leader,開始定時向其他節點發起 AppendEntries 請求以維持其 Leader 的地位。
  • Leader 失效之后停止發送心跳,Follower 的心跳超時機制又會被觸發,開始新一輪的選舉。

復制

未提交日志 已提交日志

集群中只有 Leader 對外提供服務:

客戶端與 Leader 進行通信時,每個請求包含一條可以被狀態機執行的命令。

當 Leader 在接收到命令之后,首先會將命令轉換為一條對應的 日志記錄log entry,並追加到本地的日志中。然后調用 AppendEntries 將這條日志復制到其他節點的日志中。

當日志被復制到過半數節點上時,Leader 會將這條日志中包含的命令 提交commit狀態機執行,最后將執行結果告知客戶端。

網絡分區

使用 過半數majority機制來處理腦裂:

發生網絡分區后,集群中可能同時出現多個 Leader,復制機制保證了最多只有一個 Leader 能夠正常對外提供服務。
如果日志無法復制到多數節點,Leader 會拒絕提交這些日志,當網絡分區消失后,集群會自動恢復到一致的狀態。

安全性保證

選舉時…

保證新的 Leader 擁有所有已經提交的日志

  • 每個 Follower 節點在投票時會檢查 Candidate 的日志索引,並拒絕為日志不完整的 Candidate 投贊成票
  • 半數以上的 Follower 節點都投了贊成票,意味着 Candidate 中包含了所有可能已經被提交的日志

提交日志時…

Leader 只主動提交自己任期內產生的日志

  • 如果記錄是當前 Leader 所創建的,那么當這條記錄被復制到大多數節點上時,Leader 就可以提交這條記錄以及之前的記錄
  • 如果記錄是之前 Leader 所創建的,則只有當前 Leader 創建的記錄被提交后,才能提交這些由之前 Leader 創建的日志

總結

一致性算法的本質:一致性與可用性之間的權衡

Raft 的優點:Single Leader 的架構簡化日志管理

所有日志都由 Leader 流向其他節點,無需與其他節點進行協商。其他節點只需要記錄並應用 Leader 發送過來日志內容即可,將原來的兩階段請求優化為一次 RPC 調用。

Raft 的缺點:對日志的連續性有較高要求

為了簡化日志管理,Raft 的日志不允許存在空隙,限制了並發性。某些應用場景下,需要通過 Multi-Raft的模式對無關的業務進行解耦,從而提高系統的並發度。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM