raft是一種共識算法,各節點可以就指定值達成共識,達成共識后的值,就不再改變了。raft是基於論文 https://raft.github.io/raft.pdf,raft是paxos的一種實現,它簡化了paxos的模型,增加了很多約束和限定條件,使得更容易在生產中落地,簡要描述如下(摘自https://github.com/hashicorp/raft):
協議描述
raft節點有3種狀態:跟隨者、候選人、領導者。所有的節點初始都是跟隨者。在跟隨者這個狀態,節點可以接受來自領導者的日志復制和選舉投票。如果在隨機等待時間內沒有收到請求,節點就推舉自己為候選人。在候選人這個狀態,節點發送請求投票給其他節點。如果節點收到大多數節點的投票,那么候選人就晉升為領導者。領導者必須接收新的日志並且復制日志到其他節點。所有的讀寫請求都必須在領導者節點上。
一旦集群有了領導者,它就能夠接收新的日志。客戶端可以請求領導者添加新的日志項,對於raft來說,日志項是一個二進制對象。領導者可以把日志項寫到持久化存儲中並且嘗試復制日志到大多數節點上。一旦日志項被復制到大多數節點上,那么就視為提交,領導者就把日志項應用到有限狀態機。有限狀態機是一個應用特定的,並且用接口實現的。
有一個跟日志復制相關的明顯問題。raft提供一個機制來把當前狀態生成快照,並且日志是壓縮的。因為有限狀態機的抽象化,復原有限狀態機的狀態必須把所有的日志重放一遍。raft會生成快照,並且把快照之前的日志都刪除掉。整個過程是自動的,不需要人工干預的,並且這樣也可以防止一直消耗磁盤空間,也可以減少復原日志的時間。
最后,當集群增加節點或者移除節點時,只要存在大多數節點,那么raft就可以自動更新集群信息。但是如果大多數節點無法保證,那么這就會變成一個非常棘手的問題。假如,集群中只有2個節點,A和B。仲裁節點個數也是2,這就意味着所有的節點必須達成共識才能提交新的日志項。假如,節點A或者B失敗了,那么就不可能達成仲裁。這就意味着集群無法添加,或者刪除,或者提交其他的日志項。這樣會導致集群的不可用。這個時候,人工干預就不可避免,需要刪除節點A或者節點B,並且重新啟動集群。
3個節點的raft集群可以忍受1個節點宕機,5個節點的集群可以忍受2個節點宕機。推薦的配置是3個節點或者5個節點,否則更多的節點會導致性能問題。
raft比paxos的性能好一點。假如領導者比較穩定,那么提交一個新的日志項需要所有大多數節點的來回請求。性能表現也取決於磁盤I/O和網絡延遲。
成員狀態
raft節點有3種狀態:
- 領導者(leader) - 領導者是整個集群的領導者,所有讀寫請求都必須在領導者節點上。只要集群中有領導者,那么就不會發生新的選舉。領導者會定期發送心跳給跟隨者,告訴其它節點,我還活着,不要觸發選舉。領導者也會同步日志給跟隨者,跟隨者復制日志並應用到本地狀態機。
- 跟隨者(follower) - 跟隨者會接收來自領導者的心跳請求和復制日志請求。如果在一定的時間內沒有收到領導者的心跳請求,那么跟隨者會立馬把自己的狀態變成候選人。
- 候選人(candidate)- 跟隨者變成候選人之后,會立馬給自己投上一票,然后發送請求投票rpc消息給其它的節點尋求票數,只要票數足夠,候選人會變成領導者。在選舉期間,整個集群不可用,無法對外提供服務。
領導者選舉
以3節點為例,集群啟動時,所有節點的初始狀態是跟隨者:
- 節點1的timeout最小,最先因為心跳超時而觸發選舉,首先節點1把自己變為候選人,同時給自己投上一票,然后發送請求投票rpc消息給節點2
- 節點1同時發送請求投票rpc消息給節點3
- 節點2收到請求投票rpc消息,發現有新的選舉,給節點1投上一票
- 節點3收到請求投票rpc消息,發現有新的選舉,給節點1投上一票
- 節點1成為領導者之后,發送心跳rpc消息給節點2
- 節點1發送心跳rpc消息給節點3
- 節點2收到心跳rpc消息,知道領導者還活着,不觸發新的選舉
- 節點3收到心跳rpc消息,知道領導者還活着,不觸發新的選舉
***這里有幾個關鍵的點:***
- 心跳超時timeout是隨機的,這樣就不會因為心跳超時而同時觸發選舉,比如配置了x ms,隨機時間取x ms和 2x ms之間的數
- 每一屆領導者都是有任期編號term,rpc消息中都會帶上選舉的任期編號,當節點2/3接收請求投票rpc消息時,發現任期編號大於自己當前的任期編號,就給節點1投上一票
- 節點成為領導者之后,會周期性的給跟隨者發送心跳消息,阻止跟隨者觸發新的選舉
- 在一屆任期內,贏得大多數選票的節點成為領導者,如果在選舉超時時間內,沒有贏得大多數選票,就會觸發新的選舉。在一屆任期內,每個節點至多只會投1票。比如,節點1在任期4內請求節點2投票,節點2會把當前的票投給節點1,節點3在任期4內再來請求節點2投票時,節點2就沒有票可以投了
- 任期編號大的節點會拒絕投票給任期編號小的候選人。比如發生網絡分區時,有一個跟隨者單獨在一個網絡分區,因為無法跟其它節點通訊,會觸發選舉,增加任期編號為2,其它2個節點的任期編號已經是3了,這個時候,另外2個節點就會拒絕任期編號為2的節點的請求投票
- 日志完整性高的節點拒絕投票給日志完整性低的節點,rpc消息中都會帶上最后一條日志的索引和任期編號。比如發生網絡分區時,有一個跟隨者單獨在一個網絡分區,因為無法跟其它節點通訊,日志復制就慢一拍,那么日志完整性就低,當觸發新的選舉時,即使選舉的任期編號更大,但是,因為日志完整性低,其它節點也會拒絕投票
日志復制
日志復制是raft中非常重要的內容,客戶端給raft集群提交的數據是以日志的形式存在的,一條一條的指令就是日志中一個一個日志項。raft集群處理客戶端寫請求的過程,就是一個復制和應用日志項到狀態機的過程。
日志包含以下信息:
- 索引值:日志項對應的索引值,是連續的、遞增的數字
- 任期編號:當前領導者所在的任期編號
- 指令:就是客戶端提交的數據,以指令的形式存在日志項中,比如把鍵為foo的值設置成bar,可以寫成指令set foo=bar
***那么如何復制日志呢?***
- 客戶端發送寫請求提交數據
- 領導者接收到請求,發送日志復制rpc消息給跟隨者,rpc消息里面包含領導者最新的日志項
- 領導者確認日志復制到大多數節點上,然后提交日志並應用到本地狀態機
- 領導者返回結果給客戶端
- 在下一次心跳rpc消息或者日志復制rpc消息中,包含領導者已經提交的日志項
- 跟隨者接收到,發現領導者已經提交但是自己沒有提交時,就會應用到本地狀態機
上面第3點中,領導者確認日志復制到大多數節點上時就會提交日志,如果這個時候有一個跟隨者節點宕機或者網絡原因,沒有接收到最新的日志復制rpc消息,那么當它恢復時,
***raft算法時如何實現日志一致的呢?***
- 領導者發送日志復制請求給跟隨者時,消息中會有LogEntry(當前日志索引:4)、LogTerm(當前任期編號:2)、PrevLogEntry(上一條日志索引:3)、PrevLogTerm(上一條日志任期編號:1)
- 跟隨者發現自己不存在PrevLogEntry、PrevLogTerm時,返回錯誤給領導者
- 領導者發現跟隨者返回錯誤時,就發送復制前一條日志的請求給跟隨者,消息中會有LogEntry(當前日志索引:3)、LogTerm(當前任期編號:1)、PrevLogEntry(上一條日志索引:2)、PrevLogTerm(上一條日志任期編號:1)
- 跟隨者發現自己存在PrevLogEntry、PrevLogTerm時,就復制日志到自己的本地,返回成功給領導者
- 領導者接着發送復制下一條日志的請求給跟隨者
- 跟隨者復制日志成功並返回
成員變更
成員變更是生產上使用raft集群時會碰到的問題,比如raft集群中某個節點宕機了,需要進行節點替換或者增加節點。在成員變更的時候,可能發生雙leader的情況。比如有一個3節點集群,增加2個節點,那么可能發生下面這種情況:
- 通過調用領導者的增加節點接口添加節點D和E,leader把最新的集群節點信息[A, B, C, D, E]作為日志項復制到其它跟隨者節點
- 當日志復制到大多數節點C、D、E后,最新的配置信息作為日志項提交leader並應用到狀態機,這樣C、D、E中就是最新的5節點[A, B, C, D, E],A、B還是老的3節點[A, B, C]
- 這個時候,發生網絡分區,C、D、E在一個網絡里面,A和B在另一個網絡里面
- A和B觸發選舉,選舉出節點A作為leader,C、D、E中還是節點C作為leader,這個時候集群中就出現了2個leader
***那么如何避免出現雙leader的情況呢?***
- 因為配置信息是動態添加的,同步配置信息的時候發生網絡分區才出現雙leader。那么一開始把所有節點信息都配置好,然后統一啟動集群,可以避免這個問題。如果集群已經在運行,就需要停止集群再重新啟動。這種方式不適合生產上不能停機的情況
- 另外一種方式是進行單節點變更,一次只變更一個節點,等到添加完成后,再變更下一個節點,具體如下:
- 3節點集群增加一個節點D,新配置信息同步到B、C、D大多數節點后,B、C、D中的配置信息是4節點[A, B, C, D],A的信息可能還是[A, B, C]
- 繼續增加節點E,新配置信息同步到C、D、E大多數節點后,C、D、E中配置信息是5節點[A, B, C, D, E],此時,即使發生網絡分區,如C、D、E在一個分區中,C成為leader,如A、B在一個分區中,A無法成為leader,因為A的日志完整性比節點B低,而B要成為leader的條件是得到大多數的選票,至少需要3票,但是同一個網絡里面只有2個節點,B也無法成為leader,這樣整個5節點集群也只會有1個leader
可以發現,只要保證節點變更時,集群無法形成2個大多數,那么就不會出現雙leader的情況。這里執行單節點變更時,需要等待上一次單節點變更完成。如果一次單節點變更沒有完成,新的單節點變更又開始,在網絡分區的情況下,是可能出現2個領導者的。一般在領導者啟動時,創建一個NO_OP日志項,這樣可以確保可能正在進行的單節點變更,已執行完成,這樣再執行其它單節點變更不會有問題
基於raft算法實現一個簡單的KV存儲
本人基於開源的hashcorp raft算法實現一個簡單的KV存儲 https://gitee.com/liuanyou/myraft.git ,大概實現思路如下:
- 提供http接口寫KV,在接收到KV數據之后,把KV數據寫操作作為日志項應用到leader節點,等到日志項復制到大多數節點之后,應用日志項到狀態機。在應用狀態機的時候,獲取日志項中的KV數據,然后保存到文件中。調用http接口讀KV時,從文件中讀取對應key的value數據
- 啟動raft集群時,事先指定好所有節點的配置信息,這樣啟動后就會觸發選舉,選舉出leader節點
- 客戶端調用http接口,如果訪問的不是leader節點,那么會把leader節點的信息作為response發送給客戶端,這樣客戶端會請求leader節點的http接口