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 在接收到命令之后,首先會將命令轉換為一條對應的 日志記錄log entry
,並追加到本地的日志中。然后調用 AppendEntries 將這條日志復制到其他節點的日志中。
當日志被復制到過半數節點上時,Leader 會將這條日志中包含的命令 提交commit
狀態機執行,最后將執行結果告知客戶端。
網絡分區
使用 過半數majority
機制來處理腦裂:
如果日志無法復制到多數節點,Leader 會拒絕提交這些日志,當網絡分區消失后,集群會自動恢復到一致的狀態。
安全性保證
選舉時…
保證新的 Leader 擁有所有已經提交的日志
- 每個 Follower 節點在投票時會檢查 Candidate 的日志索引,並拒絕為日志不完整的 Candidate 投贊成票
- 半數以上的 Follower 節點都投了贊成票,意味着 Candidate 中包含了所有可能已經被提交的日志
提交日志時…
Leader 只主動提交自己任期內產生的日志
- 如果記錄是當前 Leader 所創建的,那么當這條記錄被復制到大多數節點上時,Leader 就可以提交這條記錄以及之前的記錄
- 如果記錄是之前 Leader 所創建的,則只有當前 Leader 創建的記錄被提交后,才能提交這些由之前 Leader 創建的日志
總結
一致性算法的本質:一致性與可用性之間的權衡。
Raft 的優點:Single Leader 的架構簡化日志管理
Raft 的缺點:對日志的連續性有較高要求
Multi-Raft
的模式對無關的業務進行解耦,從而提高系統的並發度。