最近工作中討論到了Raft協議相關的一些問題,正好之前讀過多次Raft協議的那paper,所以趁着討論做一次總結整理。
我會將Raft協議拆成四個部分去總結:
-
算法基礎
-
選舉和日志復制
-
安全性
-
節點變更
這是第一篇:《解讀Raft(一 算法基礎)》
什么是RAFT
分布式系統除了提升整個體統的性能外還有一個重要特征就是提高系統的可靠性。
提供可靠性可以理解為系統中一台或多台的機器故障不會使系統不可用(或者丟失數據)。
保證系統可靠性的關鍵就是多副本(即數據需要有備份),一旦有多副本,那么久面臨多副本之間的一致性問題。
一致性算法正是用於解決分布式環境下多副本之間數據一致性的問題的。
業界最著名的一致性算法就是大名鼎鼎的Paxos(Chubby的作者曾說過:世上只有一種一致性算法,就是Paxos)。但Paxos是出了名的難懂,而Raft正是為了探索一種更易於理解的一致性算法而產生的。
Raft is a consensus algorithm for managing a replicated log.
Raft是一種管理復制日志的一致性算法。
它的首要設計目的就是易於理解,所以在選主的沖突處理等方式上它都選擇了非常簡單明了的解決方案。
Raft將一致性拆分為幾個關鍵元素:
-
Leader選舉
-
日志復制
-
安全性
Raft算法
所有一致性算法都會涉及到狀態機,而狀態機保證系統從一個一致的狀態開始,以相同的順序執行一些列指令最終會達到另一個一致的狀態。
以上是狀態機的示意圖。所有的節點以相同的順序處理日志,那么最終x、y、z的值在多個節點中都是一致的。
算法基礎
角色
Raft通過選舉Leader並由Leader節點負責管理日志復制來實現多副本的一致性。
在Raft中,節點有三種角色:
-
Leader:負責接收客戶端的請求,將日志復制到其他節點並告知其他節點何時應用這些日志是安全的
-
Candidate:用於選舉Leader的一種角色
-
Follower:負責響應來自Leader或者Candidate的請求
角色轉換如下圖所示:
-
所有節點初始狀態都是Follower角色
-
超時時間內沒有收到Leader的請求則轉換為Candidate進行選舉
-
Candidate收到大多數節點的選票則轉換為Leader;發現Leader或者收到更高任期的請求則轉換為Follower
-
Leader在收到更高任期的請求后轉換為Follower
任期
Raft把時間切割為任意長度的任期,每個任期都有一個任期號,采用連續的整數。
每個任期都由一次選舉開始,若選舉失敗則這個任期內沒有Leader;如果選舉出了Leader則這個任期內有Leader負責集群狀態管理。
算法
狀態
狀態 | 所有節點上持久化的狀態(在響應RPC請求之前變更且持久化的狀態) |
---|---|
currentTerm | 服務器的任期,初始為0,遞增 |
votedFor | 在當前獲得選票的候選人的 Id |
log[] | 日志條目集;每一個條目包含一個用戶狀態機執行的指令,和收到時的任期號 |
狀態 | 所有節點上非持久化的狀態 |
---|---|
commitIndex | 最大的已經被commit的日志的index |
lastApplied | 最大的已經被應用到狀態機的index |
狀態 | Leader節點上非持久化的狀態(選舉后重新初始化) |
---|---|
nextIndex[] | 每個節點下一次應該接收的日志的index(初始化為Leader節點最后一個日志的Index + 1) |
matchIndex[] | 每個節點已經復制的日志的最大的索引(初始化為0,之后遞增) |
AppendEntries RPC
用於Leader節點復制日志給其他節點,也作為心跳。
參數 | 解釋 |
---|---|
term | Leader節點的任期 |
leaderId | Leader節點的ID |
prevLogIndex | 此次追加請求的上一個日志的索引 |
prevLogTerm | 此次追加請求的上一個日志的任期 |
entries[] | 追加的日志(空則為心跳請求) |
leaderCommit | Leader上已經Commit的Index |
prevLogIndex和prevLogTerm表示上一次發送的日志的索引和任期,用於保證收到的日志是連續的。
返回值 | 解釋 |
---|---|
term | 當前任期號,用於Leader節點更新自己的任期(應該說是如果這個返回值比Leader自身的任期大,那么Leader需要更新自己的任期) |
success | 如何Follower節點匹配prevLogIndex和prevLogTerm,返回true |
接收者實現邏輯
-
返回false,如果收到的任期比當前任期小
-
返回false,如果不包含之前的日志條目(沒有匹配prevLogIndex和prevLogTerm)
-
如果存在index相同但是term不相同的日志,刪除從該位置開始所有的日志
-
追加所有不存在的日志
-
如果leaderCommit>commitIndex,將commitIndex設置為commitIndex = min(leaderCommit, index of last new entry)
RequestVote RPC
用於Candidate獲取選票。
參數 | 解釋 |
---|---|
term | Candidate的任期 |
candidateId | Candidate的ID |
lastLogIndex | Candidate最后一條日志的索引 |
lastLogTerm | Candidate最后一條日志的任期 |
參數 | 解釋 |
---|---|
term | 當前任期,用於Candidate更新自己的任期 |
voteGranted | true表示給Candidate投票 |
接收者的實現邏輯
-
返回false,如果收到的任期比當前任期小
-
如果本地狀態中votedFor為null或者candidateId,且candidate的日志等於或多余(按照index判斷)接收者的日志,則接收者投票給candidate,即返回true
節點的執行規則
所有節點
-
如果commitIndex > lastApplied,應用log[lastApplied]到狀態機,增加lastApplied
-
如果RPC請求或者響應包含的任期T > currentTerm,將currentTerm設置為T並轉換為Follower
Followers
-
響應來自Leader和Candidate的RPC請求
-
如果在選舉超時周期內沒有收到AppendEntries的請求或者給Candidate投票,轉換為Candidate角色
Candidates
-
轉換為candidate角色,開始選舉:
-
遞增currentTerm
-
給自己投票
-
重置選舉時間
-
發送RequestVote給其他所有節點
-
如果收到了大多數節點的選票,轉換為Leader節點
-
如果收到Leader節點的AppendEntries請求,轉換為Follower節點
-
如果選舉超時,重新開始新一輪的選舉
Leaders
-
一旦選舉完成:發送心跳給所有節點;在空閑的周期內不斷發送心跳保持Leader身份
-
如果收到客戶端的請求,將日志追加到本地log,在日志被應用到狀態機后響應給客戶端
-
如果對於一個跟隨者,最后日志條目的索引值大於等於 nextIndex,那么:發送從 nextIndex 開始的所有日志條目:
-
如果成功:更新相應跟隨者的 nextIndex 和 matchIndex
-
如果因為日志不一致而失敗,減少 nextIndex 重試
-
如果存在一個滿足N > commitIndex的 N,並且大多數的matchIndex[i] ≥ N成立,並且log[N].term == currentTerm成立,那么令commitIndex等於這個N