raft算法浅析
raft算法可分解成六部分:
選舉leader:包括檢測崩潰和選舉新的leader
復(fù)制log
leader發(fā)生變化時的安全性、可用性和一致性
中立舊leader
客戶端交互
修改配置:增加或刪除server
raft算法定義了term(任期)的概念。一個term可分為兩個階段:選舉和復(fù)制日志。選舉階段用于選出一個leader,且僅有一個leader;leader選舉出來以后,就可以開始復(fù)制日志了。當(dāng)然,選舉階段可能出現(xiàn)分裂投票的情況,即多個candidate都持有不足半數(shù)的投票,沒有一個candidate的票數(shù)過半,大家互不相讓,導(dǎo)致選舉超時,這時直接進(jìn)入下一輪term,重新開始選舉。
term默認(rèn)值為0,然后單調(diào)遞增。term的主要作用是用于識別出過時信息。比如網(wǎng)絡(luò)分區(qū)時,某一分區(qū)的server的term滯后,分區(qū)恢復(fù)后就能根據(jù)term值識別出過期的server,過期的server也可以根據(jù)收到的較大的term更新自己的term。
raft算法中,在一個term中,一個server只能是以下三種狀態(tài)之一:
leader:負(fù)責(zé)與客戶端交互,復(fù)制日志
follower:只能接收leader的rpc并作出響應(yīng)
candidate:選舉期間server的狀態(tài)
狀態(tài)轉(zhuǎn)換圖如下:
server啟動時,處于follower狀態(tài);由于收不到leader的心跳包,超時轉(zhuǎn)入candidate狀態(tài)。
candidate狀態(tài)下,可能會因?yàn)槭盏蕉鄶?shù)server的投票轉(zhuǎn)入leader狀態(tài);或者因?yàn)槭盏搅诵耹eader的心跳包而轉(zhuǎn)入follower狀態(tài),也可能因?yàn)榘l(fā)現(xiàn)了更大的term而轉(zhuǎn)入follower狀態(tài);或者是因?yàn)樯鲜鰩追N情況均未發(fā)生而一直等待至超時,導(dǎo)致進(jìn)入下一輪term,重新轉(zhuǎn)入candidate狀態(tài)。
leader狀態(tài)下,如果發(fā)現(xiàn)了更大的term,就會退位,轉(zhuǎn)入follower狀態(tài)。
上述提到了“發(fā)現(xiàn)了更大的term”這種情況,這種情況主要是在系統(tǒng)出現(xiàn)網(wǎng)絡(luò)分區(qū)一段時間后恢復(fù)正常時可能出現(xiàn)。
一、選舉過程(系統(tǒng)啟動時的選舉可以和leader崩潰時的選舉統(tǒng)一起來)
各follower將term加1,進(jìn)入新一輪term;然后所有的follower隨機(jī)等待一段時間(一般取T~2T,T一般取150ms)后轉(zhuǎn)入candidate狀態(tài),發(fā)起投票(即發(fā)送RequestVote RPC),同時開始計(jì)時一個election timeout時間;
如果在election timeout超時之前沒收到新leader的心跳包或者沒收到多數(shù)投票,則回到步驟1;
否則,根據(jù)情況轉(zhuǎn)入follower狀態(tài)或者leader狀態(tài),開始復(fù)制log;如果是follower狀態(tài),則監(jiān)聽leader的心跳包,如果心跳包超時,則回到步驟1。
步驟1中要求隨機(jī)等待一段時間再發(fā)起投票是為了防止上文提到的多個candidate同時發(fā)起投票導(dǎo)致出現(xiàn)分裂投票的情況。
二、復(fù)制log
log結(jié)構(gòu):log entry由index、term和command組成。
leader收到客戶端的命令后,封裝成一個log entry,append到本地log中,然后向所有的follower發(fā)送AppendEntries RPC。當(dāng)收到多數(shù)follower的響應(yīng)時,leader認(rèn)為該log entry已提交,然后可以在本地狀態(tài)機(jī)上執(zhí)行該命令,并返回結(jié)果給客戶端,同時通知各個follower該log entry已提交,follower收到該通知后就可以將命令送入狀態(tài)機(jī)執(zhí)行。
從上述描述可以總結(jié)出一點(diǎn),log entry已提交就是指該log entry在大多數(shù)server上都有了備份,且大多數(shù)server知曉這一點(diǎn)。
對于那些還沒向leader發(fā)送響應(yīng)的follower,leader會不斷向它們發(fā)送AppendEntries RPC,直到它們成功響應(yīng)。
log一致性特性:
如果兩個server上的log entry有相同的index和term,則
該index中存的命令一定相同;
小于該index的所有l(wèi)og entry 一定相同。
如果某一個log entry已被提交,則該log entry之前的所有l(wèi)og entry(index更小的log entry)均已被提交。
下面解釋一下為什么raft算法中的log具有這兩個特性。
第一,leader每次復(fù)制日志時,會進(jìn)行AppendEntries RPC調(diào)用,該調(diào)用包含了這樣幾個參數(shù):
prevLogIndex:leader的本地log中最新的log entry之前的log entry(因?yàn)閘eader是將客戶端命令封裝成log entry并append到本地log之后才開始復(fù)制日志的,所以才會說“之前”)的index
prevLogTerm:leader的本地log中最新的log entry之前的log entry的term
entries[]:將要復(fù)制到follower的log entries,可以不止一條
當(dāng)leader接收了客戶端發(fā)起的一個新的命令,并將命令封裝成log entry寫入了本地log后,他需要將該log entry復(fù)制到所有的follower上,這時follower不僅僅是簡單地將log entry寫入到本地log即可,還需要在寫入之前檢查所有已有的log entry是否與leader中的一致。follower將prevLogIndex和prevLogTerm這兩個參數(shù)與自己本地最新的log entry的index和term進(jìn)行對比,如果相同,可以認(rèn)為自己的本地log與leader是一致的,然后就可以將新的log entry append到本地log中;如果對比發(fā)現(xiàn)不相同,則拒絕append,并返回false。
上述提到的這種檢查其實(shí)是一種遞歸檢查。這一次檢查發(fā)現(xiàn)prevLogIndex和prevLogTerm與本地最新的log entry的index和term是匹配的,說明上一次檢查時相應(yīng)的prevLogIndex和prevLogTerm也是匹配的,一直遞歸到log為空,append第一條log entry時,prevLogIndex和prevLogTerm都為0,也是匹配的。所以每次檢查時如果prevLogIndex和prevLogTerm與本地最新的log entry匹配,則之前的所有的log entry也是一致的。
那么follower在檢查完之后,如果本地最新的log entry是一致的,則本次將log entry append到本地log中之后,整個log與leader上的log仍然是一致的。
此時已經(jīng)證明了第一條特性。在說明過程中提到的AppendEntries RPC的參數(shù)以及follower的一致性檢查是不完整的,只是摘取了與第一條特性相關(guān)的點(diǎn)。
第二條特性不知道怎么解釋,可以簡單理解為log entry是按順序提交的嗎?暫且當(dāng)結(jié)論記住吧( ̄ェ ̄;)
三、leader發(fā)生變化
leader發(fā)生變化時必須保證安全性,即:
如果當(dāng)前term的leader判斷一條log entry已提交,當(dāng)leader發(fā)生變化時,新的leader的log中必須有該log entry。
raft保證安全性的做法是candidate在發(fā)起RequestVote RPC時攜帶其最后一條log entry的index和term,收到rpc的server要進(jìn)行如下判斷:
如果結(jié)果為true,則拒絕給該candidate投票。
等價于:
如果選民的term比candidate的還大,直接拒絕投票;
如果選民的term跟candidate的一樣大,就再看log的長度,如果選民的log更長,直接拒絕投票;如果選民的log不比candidate更長,則投票;
如果選民的term比candidate的小,則直接投票。
這種方法能夠保證在如下情況下的安全性:
term2的leader s1將log entry(index=4, term=2)復(fù)制到s3上后完成該log entry的提交,然后s1崩潰,s4和s5不可能當(dāng)選為leader。
但是無法保證在如下情況下的安全性:
term4的leader s1將log entry(index=4, term=2)復(fù)制到s3上后完成該log entry的提交,然后s1崩潰。根據(jù)判決條件,s5是可能當(dāng)選為leader的。s5一旦當(dāng)選,將會把自己的log復(fù)制到其他server上,這樣log entry(index=4, term=2)將被覆蓋,即出現(xiàn)了已提交的log entry在leader改變后消失的情況。
針對這種情況,raft對commit規(guī)則作出了改進(jìn)。
leader上的一條老term的log entry需滿足如下兩個條件才能被認(rèn)為已提交:
必須在大多數(shù)server有備份
至少要有一條當(dāng)前term的log entry在大多數(shù)server上有備份
根據(jù)官方視頻的示例,貌似對于當(dāng)前term中創(chuàng)建的log entry,只要在大多數(shù)server上備份了就算是已提交了。
在下圖的情形中,根據(jù)commit的新定義,log entry(index=4, term=2)就不是已提交狀態(tài),leader s1也就不會將該命令傳入狀態(tài)機(jī)。那么,即使s5當(dāng)選為新term的leader之后,覆蓋掉s1、s2和s3上的log entry(index=4, term=2)也無所謂了。
如果滿足了commit的新定義,如下圖所示,則log entry(index=4, term=2)就是已提交的,于是也就可以放心地將該log entry傳入狀態(tài)機(jī)執(zhí)行了。s5不可能當(dāng)選為leader,leader只可能在s1、s2和s3中產(chǎn)生,所以log entry(index=4, term=2)不會被新的leader覆蓋掉。
總的來說,新的commit規(guī)則和選舉規(guī)則保證了舊leader上已提交的log entry不會被新的leader覆蓋掉。
很自然地,下面就要介紹新leader如何使所有follower上的log與新leader一致。
新leader出現(xiàn)后,與leader上的log相比,follower上的log中未提交的部分可能出現(xiàn)log entry缺失,也可能有多余的log entry。
“缺失”很好理解,就是指follower的log比leader的短。
“多余”包括兩種情況:
follower的log比leader長的部分
follower的log中與leader中不同的部分
下圖的示例中:
先看第一種情況,(c)、(d)和(f)中index>10的entry都是多余的entry。
再看第二種情況,(e)中index=6,7的entry都是多余的entry;(f)中4<=index<=10的entry都是多余的entry。
leader為每個follower維護(hù)一個nextIndex變量。為某個follower維護(hù)的nextIndex表示leader要發(fā)送給該follower的下一個entry的index。nextIndex初始化為leader的最后一個entry的index加1。
leader根據(jù)nextIndex設(shè)置好AppendEntries RPC的參數(shù)prevLogIndex和prevLogTerm,然后發(fā)送給follower。如果follower的響應(yīng)為false,則將nextIndex減一,然后重復(fù)上述操作。
當(dāng)follower收到leader的AppendEntries RPC后,會進(jìn)行一致性檢查。如果一致性檢查通過,則將AppendEntries RPC中的entries復(fù)制到自己的log中;否則返回false。
四、中立舊leader
當(dāng)出現(xiàn)網(wǎng)絡(luò)分區(qū)時,可能會有新的leader出現(xiàn),當(dāng)分區(qū)恢復(fù)時,如何處理兩個leader呢?
raft使用term識別舊的leader。
對于任何一個RPC,如果發(fā)送方的term小于接收方的term,則接收方拒絕該RPC,發(fā)送方收到拒絕后轉(zhuǎn)為follower,并更新自己的term。
如果發(fā)送方的term大于接收方的term,則接收方轉(zhuǎn)為follower,并更新自己的term,正常處理RPC請求。
五、客戶端協(xié)議
如果客戶端不知道哪個server是leader,則將命令發(fā)送到任何一個server上。接收到該命令的server如果不是server,則返回leader信息,讓客戶端重定向到leader。
leader收到客戶端的命令后,必須等待命令被提交,然后將命令注入狀態(tài)機(jī)執(zhí)行,最后才將執(zhí)行結(jié)果返回給客戶端。
如果客戶端的請求超時無響應(yīng),則客戶端會重發(fā)該命令到其他server上,最終重定向到新的leader。
如果leader執(zhí)行了客戶端的命令后,還沒來得及將結(jié)果返回給客戶端就崩潰了,那么客戶端會因?yàn)槌瑫r而重發(fā)該命令,這樣可能導(dǎo)致同一個命令執(zhí)行了兩次。如何避免這種情況呢?
如果leader接收了客戶端的命令后,提交了該entry,還未將命令注入狀態(tài)機(jī)執(zhí)行就崩潰了,也可能導(dǎo)致該命令被新的leader提交,最終也還是會執(zhí)行兩次。
raft的做法是在客戶端發(fā)送命令時攜帶一個唯一的id,leader收到該命令后首先檢查本地log中是否已有該id,若有,則直接返回對應(yīng)命令的結(jié)果;否則正常操作。
六、修改配置:增加或刪除server
當(dāng)配置發(fā)生變化時,由于網(wǎng)絡(luò)問題,不能保證新配置在所有的server上同時生效。比如某集群由3臺集群擴(kuò)展成5臺機(jī)器,新配置可能只在2臺新機(jī)器和1臺舊機(jī)器上生效,有2臺舊機(jī)器仍然在使用舊配置。2臺舊機(jī)器認(rèn)為目前集群中只有3臺機(jī)器,因此它們倆就構(gòu)成了大多數(shù),也就可以選舉出一個leader。同時,3臺使用新配置的機(jī)器認(rèn)為目前集群中有5臺機(jī)器,它們仨就構(gòu)成了大多數(shù),也可以選舉出一個leader。此時集群中就出現(xiàn)了2個leader。
raft規(guī)避這種情況的方法是采用聯(lián)合共識(joint consensus)。在從舊配置過渡到新配置的過程中,增加一個新舊配置同時生效的階段。具體做法如下圖所示:
當(dāng)leader收到客戶端更改配置的命令時,將新舊配置封裝到一個log entry(記為Cold+new)中,存入本地log;同時立刻讓新配置生效,此時leader中同時存在舊配置和新配置,兩者都有效。然后將Cold+new廣播給其它server。
follower,也就是其它server,收到Cold+new后,一旦決定將該entry寫入本地log,則使新配置立即生效,不用等到leader通知已提交后才生效。
leader如果確認(rèn)Cold+new已被提交,則集群進(jìn)入joint consensus階段。在此階段,不管是選舉還是提交命令,必須得到舊配置有效的機(jī)器中的大多數(shù)的確認(rèn),同時也必須得到新配置有效的機(jī)器中的大多數(shù)的確認(rèn)。
當(dāng)leader確認(rèn)Cold+new已被提交后,就可以開始將新配置廣播到其它server上了。一旦新配置提交成功,則集群就可以使用新配置了。
本文圖片截自油管官方視頻中的PPT《Raft: A Consensus Algorithm for Replicated Logs》。
歡迎關(guān)注博主個人微信公眾號~~~
總結(jié)
- 上一篇: 图书分类系统-总述
- 下一篇: freemarker截取字符串subSt