raft原理的動畫演示:
http://thesecretlivesofdata.com/raft/
前言
這是一篇學習raft論文的總結,主要是對看論文過程中難以理解的幾個問題的記錄。系統性的講解還是得看raft論文,論文原文是最好的材料。
概述
引用論文中的第一句話--“Raft 是一種為了管理復制日志的一致性算法”。從兩個角度來理解raft算法,第一部分是raft的基本規則,第二部分是raft的異常情況處理。下面放一張raft論文中的經典圖來了解一下raft是怎么在一個系統中工作的。下圖中一致性模塊Consensus Module執行的就是raft算法,它保證拷貝到所有server上的每一條日志是一致的。State Machine狀態機對應我們的業務邏輯,日志作為狀態機的輸入,輸入一致就能保證輸出是一致的。

基本規則
raft的工作模式是一個Leader和多個Follower模式,即我們通常說的領導者-追隨者模式。這種模式下需要解決的第一個問題就是Leader的選舉問題。其次是如何把日志從Leader復制到所有Follower上去。這里先不關心安全和可靠性,只理解raft運行起來基本規則。raft中的server有三種狀態,除了已經提到的Leader和Follower狀態外,還有Candidate狀態,即競選者狀態。下面是這三種狀態的轉化過程。

1、Leader的選舉過程
raft初始狀態時所有server都處於Follower狀態,並且隨機睡眠一段時間,這個時間在0~1000ms之間。最先醒來的server A進入Candidate狀態,Candidate狀態的server A有權利發起投票,向其它所有server發出requst_vote請求,請求其它server給它投票成為Leader。當其它server收到request_vote請求后,將自己僅有的一票投給server A,同時繼續保持Follower狀態並重置選舉計時器。當server A收到大多數(超過一半以上)server的投票后,就進入Leader狀態,成為系統中僅有的Leader。raft系統中只有Leader才有權利接收並處理client請求,並向其它server發出添加日志請求來提交日志。
2、日志復制過程
Leader選舉出來后,就可以開始處理客戶端請求。Leader收到客戶端請求后,將請求內容作為一條log日志添加到自己的log記錄中,並向其它server發送append_entries(添加日志)請求。其它server收到append_entries請求后,判斷該append請求滿足接收條件(接收條件在后面安全保證問題3給出),如果滿足條件就將其添加到本地的log中,並給Leader發送添加成功的response。Leader在收到大多數server添加成功的response后,就將該條log正式提交。提交后的log日志就意味着已經被raft系統接受,並能應用到狀態機中了。
Leader具有絕對的日志復制權力,其它server上存在日志不全或者與Leader日志不一致的情況時,一切都以Leader上的日志為主,最終所有server上的日志都會復制成與Leader一致的狀態。
以上就是raft允許的基本規則,如果不出現任何異常情況,那么只要上面兩個過程就能使raft運行起來了。但是現實的系統不可能這么一帆風順,總是有很多異常情況需要考慮。raft的復雜性就來源於對這些異常情況的考慮,下面一小節就以問答的方式來總結raft是怎么保證安全性的。
安全性保證
1、Leader選舉過程中,如果有兩個serverA和B同時醒來並發出request_vote請求怎么辦?
由於在一次選舉過程中,一個server最多只能投一票,這就保證了serverA和B不可能同時得到大多數(一半以上)的投票。如果A或者B中其一幸運地得到了大多數投票,就能順利地成為Leader,raft系統正常運行下去。但是A和B可能剛好都得到一半的投票,兩者都成為不了Leader。這時A和B繼續保持Candidate狀態,並且隨機睡眠一段時間,等待進入到下一個選舉周期。由於所有server都是隨機選擇睡眠時間,所以連續出現多個server競選的概率很低。
2、Leader掛了后,如何選舉出新的Leader?
Leader正常運作時,會周期性地發出append_entries請求。這個周期性的append_entries除了可以更新其它Follower的log信息,另外一個重要功能就是起到心跳作用。Follower收到append_entries后,就知道Leader還活着。如果Follower經過一個預定的時間(一般設為2000ms左右)都沒有收到Leader的心跳,就認為Leader掛了。於是轉入Candidate狀態,開始發起投票競選新的Leader。每個新的Leader產生后就是一個新的任期,每個任期都對應一個唯一的任期號term。這個term是單調遞增的,用來唯一標識一個Leader的任期。投票開始時,Candidate將自己的term加1,並在request_vote中帶上term;Follower只會接受任期號term比自己大的request_vote請求,並為之投票。這條規則保證了只有最新的Candidate才有可能成為Leader。

3、Follower在收到一條append_entries添加日志請求后,是否立即保存並將其應用到狀態機中去?如果不是立即應用,那么由什么來決定該條日志生效的時間?
Follower在收到一條append_entries后,首先會檢查這條append_entries的來源信息是否與本地保存的leader信息符合,包括leaderId和任期號term。檢查合法后就將日志保存到本地log中,並給Leader回復添加log成功,但是不會立即將其應用到本地狀態機。Leader收到大部分Follower添加log成功的回復后,就正式將這條日志commit提交。Leader在隨后發出的心跳append_entires中會帶上已經提交日志索引。Follower收到Leader發出的心跳append_entries后,就可以確認剛才的log已經被commit(提交)了,這個時候Follower才會把日志應用到本地狀態機。下表即是append_entries請求的內容,其中leaderCommit即是Leader已經確認提交的最大日志索引。Follower在收到Leader發出的append_entries后即可以通過leaderCommit字段決定哪些日志可以應用到狀態機。

4、假設有一個server A宕機了很長一段時間,它的日志已經落后很多。如果A重新上線,而且此時現有Leader掛掉,server A剛好競選成為了Leader。按照日志都是由Leader復制給其它server的原則,server A會把其它Follower已經提交的日志給抹掉,而這違反了raft狀態機安全特性,raft怎么解決這種異常情況?
所謂的狀態機安全特性即是“如果一個領導人已經在給定的索引值位置的日志條目應用到狀態機中,那么其他任何的服務器在這個索引位置不會提交一個不同的日志”。如果server在競選Leader的過程中不加任何限制的話,攜帶舊日志的server也有可能競選成為Leader,就必然存在覆蓋之前Leader已經提交的日志可能性,從而違反狀態機安全特性。raft的解決辦法很簡單,就是只有具有最新日志的server的才有資格去競選當上Leader,具體是怎么做到的呢?首先任何server都還是有資格去發起request_vote請求去拉取投票的,request_vote中會帶上server的日志信息,這些信息標明了server日志的新舊程度,如下表所示。

其它server收到request_vote后,判斷如果lastLogTerm比自己的term大,那么就可以給它投票;lastLogTerm比自己的term小,就不給它投票。如果相等的話就比較lastLogIndex,lastLogIndex大的話日志就比較新,就給它投票。下圖是raft日志格式,每條日志中不僅保存了日志內容,還保存了發送這條日志的Leader的任期號term。為什么要在日志里保存任期號term,由於任期號是全局單調遞增且唯一的,所以根據任期號可以判斷一條日志的新舊程度,為選舉出具有最新日志的Leader提供依據。

5、存在如下圖一種異常情況,server S5在時序(d)中覆蓋了server S1在時序(c)中提交的index為2的日志,方框中的數字是日志的term。這違反了狀態機的安全特性--“如果一個領導人已經在給定的索引值位置的日志條目應用到狀態機中,那么其他任何的服務器在這個索引位置不會提交一個不同的日志”,raft要如何解決這個問題?

出現這個問題的根本原因是S1在時序(c) 的任期4內提交了一個之前任期2的log,這樣S1提交的日志中最大的term僅僅是2,那么一些日志比較舊的server,比如S5(它最日志的term為 3),就有機會成為leader,並覆蓋S1提交的日志。解決辦法就是S1在時序(c)的任期term4提交term2的舊日志時,舊日志必須附帶在當前term 4的日志下一起提交。這樣就把S1日志的最大term提高到了4,讓那些日志比較舊的S5沒有機會競選成為Leader,也就不會用舊的日志覆蓋已經提交的日志了。
簡單點說,Leader如果要提交之前term的舊日志,那么必須要提交一條當前term的日志。提交一條當前term的日志相當於為那些舊的日志加了一把安全鎖,讓那些日志比較舊的server失去得到Leader的機會,從而不會修改那些之前term的舊日志。
怎么具體實現舊日志必須附帶在當前term的日志下一起提交呢?在問題3中有給出append_entries請求中的字段,其中有兩個字段preLogIndex和preLogTerm的作用沒有提到,這兩個字段就是為了保證Leader和Followers的歷史日志完全一致而存在的。當Leader在提交一條新日志的時候,會帶上新日志前一條日志的index和term,即preLogIndex和preLogTerm。Follower收到append_entries后,會檢查preLogIndex和preLogTerm是否和自己當前最新那條日志的index和term對得上,如果對不上就會給Leader返回自己當前日志的index和term。Leader收到后就將Follower返回的index對應的日志以及對應的preLogIndex和preLogTerm發送給Follower。這個過程一直重復,直到Leader和Follower找到了第一個index和term能對得上的日志,然后Leader從這條日志開始拷貝給Follower。回答段首的問題,Leader在提交一條最新的日志時,Follow會檢驗之前的日志是否與Leader保持了一致,如果不一致會一直同步到與Leader一致后才添加最新的日志,這個機制就保證了Leader在提交最新日志時,也提交了之前舊的日志。
6、向raft系統中添加新機器時,由於配置信息不可能在各個系統上同時達到同步狀態,總會有某些server先得到新機器的信息,有些server后得到新機器的信息。比如下圖raft系統中新增加了server4和server5這兩台機器。只有server3率先感知到了這兩台機器的添加。這個時候如果進行選舉,就有可能出現兩個Leader選舉成功。因為server3認為有3台server給它投了票,它就是Leader,而server1認為只要有2台server給它投票就是Leader了。raft怎么解決這個問題呢?

產生這個問題的根本原因是,raft系統中有一部分機器使用了舊的配置,如server1和server2,有一部分使用新的配置,如server3。解決這個問題的方法是添加一個中間配置(Cold, Cnew),這個中間配置的內容是舊的配置表Cold和新的配置Cnew。還是拿上圖中的例子來說明,這個時候server3收到添加機器的消息后,不是直接使用新的配置Cnew,而是使用(Cold, Cnew)來做決策。比如說server3在競選Leader的時候,不僅需要得到Cold中的大部分投票,還要得到Cnew中的大部分投票才能成為Leader。這樣就保證了server1和server2在使用Cold配置的情況下,還是只可能產生一個Leader。當所有server都獲得了添加機器的消息后,再統一切換到Cnew。raft實現中,將Cold,(Cold,Cnew)以及Cnew都當成一條普通的日志。配置更改信息發送Leader后,由Leader先添加一條 (Cold, Cnew)日志,並同步給其它Follower。當這條日志(Cold, Cnew)提交后,再添加一條Cnew日志同步給其它Follower,通過Cnew日志將所有Follower的配置切換到最新。
有的raft實現采用了一種更加簡單粗暴的方法來解決成員變化的問題。這個辦法就是每次只更新一台機器的配置變化,收到配置變化的機器立馬采用新的配置。這樣的做法為什么能確保安全性呢?下面舉例說明。比如說系統中原來有5台機器A,B,C,D,E,現在新加了一台機器F,A,B,C三台機器沒有感知到F的加入,只有D,E兩台機器感知到了F的加入。現在就有了兩個舊機器集合X{A, B, C, D, E}和新機器集合Y{F}。假設A和D同時進入Candidate狀態去競選Leader,其中D要想競選成功,必須得有一半以上機器投票,即6/2+1=4台機器,就算Y集合中的F機器給D投了票,還得至少在集合X中得到3票;而A要想競選成功,也必須得到5/2+1 = 3張票,由於A只知道集合X的存在,所以也必須從集合X中獲得至少3票。而A和D不可能同時從集合X同時獲得3票,所以A和D不可能同時競選成為Leader,從而保證了安全性。可以使用更加形式化的數學公式來證明一次添加一台機器配置不會導致產生兩個Leader,證明過程就暫時省略了。
參考資料
raft論文中文翻譯:https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md
raft論文英文原址:https://raft.github.io/raft.pdf
raft使用C語言實現:https://github.com/willemt/raft
raft成員變更過程分析:http://blog.csdn.net/zhang_shuai_2011/article/details/38585725
作者:asmer
鏈接:https://www.jianshu.com/p/4711c4c32aab