實(shí)現(xiàn)分布式共識(shí)算法-Raft算法
筆者開(kāi)源了自己實(shí)現(xiàn)的Java版Raft算法框架raft-core
項(xiàng)目鏈接:https://github.com/wujiuye/delay-scheduler/tree/main/raft/raft-core
該項(xiàng)目代碼是delay-scheduler(分布式延遲調(diào)度中間件)的子模塊,水平有限,建議只學(xué)習(xí)使用。
關(guān)于CAP原理
C(一致性)A(可用性)P(分區(qū)容忍性)原理是分布式系統(tǒng)永遠(yuǎn)繞不開(kāi)的話題,在任何的分布式系統(tǒng)中,可用性、一致性和分區(qū)容忍性這三個(gè)方面都是相互矛盾的,三者不可兼得,最多只能取其二。
AP:如果要求系統(tǒng)高可用(A)和分區(qū)容錯(cuò)(P),那么就必須放棄一致性(C);
CP:如果要求數(shù)據(jù)強(qiáng)一致(C),由于網(wǎng)絡(luò)分區(qū)會(huì)導(dǎo)致同步時(shí)間無(wú)限延長(zhǎng)(P),可用性就得不到保障,那么就要放棄可用性(A);
CA:如果不存在網(wǎng)絡(luò)分區(qū)(分區(qū)指不同機(jī)房/國(guó)家/地區(qū))(P),那么強(qiáng)一致性(C)和可用性(A)可以同時(shí)滿足。
Raft一致性算法簡(jiǎn)介
在Raft集群中,每個(gè)節(jié)點(diǎn)都對(duì)應(yīng)一個(gè)角色,要么是Leader(領(lǐng)導(dǎo)節(jié)點(diǎn)),要么是Follower(跟隨節(jié)點(diǎn)),在未選舉出Leader之前,每個(gè)節(jié)點(diǎn)都可以是Candidate(候選節(jié)點(diǎn))。
Raft算法約定Raft集群只能有一個(gè)Leader節(jié)點(diǎn),并且只能由Leader節(jié)點(diǎn)處理客戶端的讀寫(xiě)請(qǐng)求,將寫(xiě)請(qǐng)求轉(zhuǎn)譯為操作日記,由Leader節(jié)點(diǎn)將操作日記復(fù)制給其它Follower節(jié)點(diǎn),當(dāng)Leader節(jié)點(diǎn)成功將一條操作日記同步到多數(shù)節(jié)點(diǎn)上時(shí)(包括自己在內(nèi)的多數(shù)節(jié)點(diǎn)),就可以將操作日記應(yīng)用到狀態(tài)機(jī),由狀態(tài)機(jī)執(zhí)行寫(xiě)操作(執(zhí)行命令),以此保證數(shù)據(jù)的最終一致性。
我們可以把Binlog看成Mysql數(shù)據(jù)庫(kù)執(zhí)行的寫(xiě)操作的命令,而MyISAM存儲(chǔ)引擎是Binlog的狀態(tài)機(jī),用于執(zhí)行命令。
實(shí)現(xiàn)Raft算法需要實(shí)現(xiàn)的兩個(gè)RPC接口:
- RequestVoteRpc:選舉時(shí)由當(dāng)前候選節(jié)點(diǎn)向其它節(jié)點(diǎn)發(fā)起拉票請(qǐng)求;
 - AppendEmtriesRpc:由Leader節(jié)點(diǎn)向其它Follower節(jié)點(diǎn)發(fā)送日記復(fù)制請(qǐng)求、心跳請(qǐng)求以及提交日記請(qǐng)求。
 
定時(shí)心跳計(jì)時(shí)器
Leader節(jié)點(diǎn)需要定時(shí)向其它Follower節(jié)點(diǎn)發(fā)送心跳包,以刷新其它Follower節(jié)點(diǎn)上的選舉超時(shí)計(jì)時(shí)。
心跳計(jì)時(shí)器在節(jié)點(diǎn)成為L(zhǎng)eader節(jié)點(diǎn)時(shí)啟動(dòng),而在節(jié)點(diǎn)變?yōu)镕ollower節(jié)點(diǎn)時(shí)停止。要求心跳超時(shí)時(shí)間間隔要比超時(shí)選舉時(shí)間間隔長(zhǎng),即Heartbeat Timeout(心跳包廣播時(shí)間)< Election Timeout(選舉超時(shí)時(shí)間)。
超時(shí)選舉計(jì)時(shí)器
當(dāng)計(jì)時(shí)達(dá)到超時(shí)(Election Timeout)閾值時(shí)觸發(fā)Leader選舉,當(dāng)前節(jié)點(diǎn)將任期號(hào)+1,并嘗試給自己投一票(如果還未將票投給其它候選人),給自己投票成功則將自己變成候選人,并向其它節(jié)點(diǎn)發(fā)起拉票請(qǐng)求。
超時(shí)選舉計(jì)時(shí)器的當(dāng)前計(jì)時(shí)可被重置,在接收到AppendEntriesRPC(含心跳請(qǐng)求)請(qǐng)求時(shí)重新計(jì)時(shí)。要求每個(gè)節(jié)點(diǎn)的超時(shí)閾值要不一樣,避免同時(shí)發(fā)起拉票請(qǐng)求,導(dǎo)致多輪選舉都未能選出Leader的情況發(fā)生。
Leader選舉流程
Leader通過(guò)投票選舉機(jī)制選舉,每個(gè)任期號(hào)每個(gè)節(jié)點(diǎn)都只能有一票,每個(gè)節(jié)點(diǎn)都優(yōu)先考慮投給自己,獲得多數(shù)選票的節(jié)點(diǎn)將成為L(zhǎng)eader節(jié)點(diǎn),因此Raft集群要求至少3個(gè)節(jié)點(diǎn),并且Raft集群節(jié)點(diǎn)總數(shù)最好是奇數(shù)。
RequestVoteRpc請(qǐng)求數(shù)據(jù)包(拉票數(shù)據(jù)包):
- public class RequestVote {
 - private long term;
 - private int candidateId;
 - private long lastLogIndex;
 - private long lastLogTerm;
 - }
 
- term:拉票方(候選節(jié)點(diǎn))的當(dāng)前任期號(hào);
 - candidateId:拉票方的節(jié)點(diǎn)ID;
 - lastLogIndex:拉票方最新日記條目的索引值;
 - lastLogTerm:拉票方最新日記條目對(duì)應(yīng)的任期號(hào)。
 
RequestVoteRpc響應(yīng)數(shù)據(jù)包(投票數(shù)據(jù)包):
- public class RequestVoteResp {
 - private long term;
 - private boolean voteGranted;
 - }
 
- term:投票方的當(dāng)前任期號(hào),用于告知拉票方更新term值;
 - voteGranted:如果投票方將選票投給拉票方,則voteGranted為true,否則為false。
 
在選舉計(jì)時(shí)器超時(shí)時(shí)發(fā)起拉票請(qǐng)求流程如下:
1)將自己本地維護(hù)的當(dāng)前任期號(hào)(term)加1;
2)為自己投票,投票成功再將自己的狀態(tài)切換到候選節(jié)點(diǎn)(Candidate),因此每個(gè)候選節(jié)點(diǎn)的第一張選票來(lái)自于它自己;
3)向其所在集群中的其他節(jié)點(diǎn)發(fā)送RequestVoteRPC請(qǐng)求(拉票請(qǐng)求),要求它們投票給自己。
每個(gè)節(jié)點(diǎn)接收到其它候選節(jié)點(diǎn)發(fā)來(lái)的拉票請(qǐng)求時(shí)需根據(jù)節(jié)點(diǎn)當(dāng)前任期號(hào)、日記同步情況、是否已經(jīng)將當(dāng)前期的一票投給了其它節(jié)點(diǎn)(包括自己)等作出如下反應(yīng):
1)、如果拉票方的term小于自身的當(dāng)前term,返回false,提醒拉票方term過(guò)時(shí),并明確告訴拉票方,這張選票不會(huì)投給它;
2)、如果拉票方的term大于自身的當(dāng)前term,且如果之前沒(méi)有把選票投給任何人(包括自己),則將選票投給該節(jié)點(diǎn),返回拉票方的term和true;
3)、否則如果拉票方的term等于自身的當(dāng)前term,如果已經(jīng)把選票投給了拉票方(重復(fù)發(fā)起請(qǐng)求場(chǎng)景),并且請(qǐng)求方的日記和自己的日記一樣新,則返回拉票方的term和true;
4)、否則,如果在此之前,已經(jīng)把選票投給了其他人,則這張選票不能投給請(qǐng)求方,并明確告訴請(qǐng)求方,這張選票不會(huì)投給它。
候選節(jié)點(diǎn)廣播發(fā)起拉票請(qǐng)求后需根據(jù)最終投票結(jié)果作出如下反應(yīng):
1)、如果多數(shù)節(jié)點(diǎn)連接異常,則繼續(xù)當(dāng)前期重新發(fā)起一次拉票,即多數(shù)節(jié)點(diǎn)掛掉選舉異常;
2)、得到大多數(shù)節(jié)點(diǎn)的選票成為L(zhǎng)eader,包括自己投給自己的一票,但每個(gè)節(jié)點(diǎn)只有一票,投給了自己就不能投給其它節(jié)點(diǎn);
3)、發(fā)現(xiàn)其它節(jié)點(diǎn)贏得了選舉(當(dāng)拉票請(qǐng)求響應(yīng)的term大于當(dāng)前候選節(jié)點(diǎn)的term時(shí),認(rèn)為其它節(jié)點(diǎn)贏得了選舉)則主動(dòng)切換回Follower;
4)、當(dāng)超時(shí)選舉計(jì)時(shí)器又觸發(fā)超時(shí)選舉時(shí),說(shuō)明沒(méi)有接收到Leader的心跳包,最后一次選舉沒(méi)有節(jié)點(diǎn)贏得選舉成為L(zhǎng)eader,那么繼續(xù)發(fā)起選舉。
如果是其它節(jié)點(diǎn)成為當(dāng)前期的Leader,Leader會(huì)通過(guò)發(fā)送心跳包告知自己,要留給Leader足夠時(shí)間發(fā)送心跳包給自己,因此選舉超時(shí)要大于心跳超時(shí),也就是:Heartbeat Timeout(心跳包廣播時(shí)間)< Election Timeout(選舉超時(shí)時(shí)間)。
在選舉結(jié)束后,每個(gè)Follower節(jié)點(diǎn)必須記錄當(dāng)前期的Leader節(jié)點(diǎn)是哪個(gè),Leader節(jié)點(diǎn)必須記錄其它所有Follower節(jié)點(diǎn)。Leader節(jié)點(diǎn)需要向其它Follower節(jié)點(diǎn)發(fā)送心跳包以及日記同步請(qǐng)求,而其它Follower節(jié)點(diǎn)在接收到客戶端請(qǐng)求時(shí)需要告知客戶端重定向到Leader節(jié)點(diǎn)發(fā)送請(qǐng)求。
Raft日志復(fù)制流程
在Raft集群中,Leader節(jié)點(diǎn)負(fù)責(zé)接收客戶端的讀寫(xiě)請(qǐng)求,如果是Follower接收請(qǐng)求,則需要將請(qǐng)求重定向到Leader節(jié)點(diǎn)。
如果Leader節(jié)點(diǎn)接收的是讀請(qǐng)求,則Leader節(jié)點(diǎn)可直接查詢數(shù)據(jù)響應(yīng)給客戶端;如果Leader節(jié)點(diǎn)接收的是寫(xiě)請(qǐng)求,則Leader節(jié)點(diǎn)先將寫(xiě)請(qǐng)求轉(zhuǎn)譯為一條操作日記,并將操作日記Append到本地,同時(shí)向其它節(jié)點(diǎn)發(fā)起AppendEntriesRPC調(diào)用,將該操作日記復(fù)制給其它節(jié)點(diǎn),在成功復(fù)制多數(shù)節(jié)點(diǎn)后,Leader節(jié)點(diǎn)提交該操作日記,提交成功則應(yīng)用到狀態(tài)機(jī),再異步的向其它節(jié)點(diǎn)發(fā)起AppendEntriesRPC調(diào)用,告知其它Follower節(jié)點(diǎn)該日記已經(jīng)提交,F(xiàn)ollower節(jié)點(diǎn)接收提交請(qǐng)求后,先將日記改為已提交狀態(tài),再將日記應(yīng)用到狀態(tài)機(jī)。
AppendEntriesRPC請(qǐng)求數(shù)據(jù)包(Leader節(jié)點(diǎn)向其它Follower節(jié)點(diǎn)發(fā)起rpc請(qǐng)求,要求其它Follower節(jié)點(diǎn)復(fù)制這個(gè)日記條目):
- public class AppendEntries implements Cloneable {
 - private long term;
 - private int leaderId;
 - private long prevLogIndex;
 - private long prevLogTerm;
 - private long leaderCommit;
 - private CommandLog[] entries;
 - }
 
- term:Leader節(jié)點(diǎn)創(chuàng)建該日記條目時(shí)的任期號(hào);
 - leaderId:Leader節(jié)點(diǎn)的ID,為了其它Follower節(jié)點(diǎn)能夠重定向客戶端請(qǐng)求到Leader節(jié)點(diǎn);
 - prevLogIndex:Leader節(jié)點(diǎn)已提交的日記中最新一條日記的索引;
 - prevLogTerm:Leader節(jié)點(diǎn)已提交的日記中最新一條日記的任期號(hào);
 - leaderCommit:Leader節(jié)點(diǎn)為每個(gè)Follower都維護(hù)一個(gè)leaderCommit,表示Leader節(jié)點(diǎn)認(rèn)為Follower已經(jīng)提交的日記條目索引值;
 - entries:將要追加到Follower上的日記條目,如果是心跳包,則entries為空。
 
AppendEntriesRPC響應(yīng)數(shù)據(jù)包(AppendEntries RPC響應(yīng)):
- public class AppendEntriesResp {
 - private long term;
 - private boolean success;
 - }
 
term:當(dāng)前任期號(hào),取值為Max(AppendEntries請(qǐng)求攜帶的term,F(xiàn)ollower本地維護(hù)的term),用于Leader節(jié)點(diǎn)更新自己的任期號(hào),一旦Leader節(jié)點(diǎn)發(fā)現(xiàn)任期號(hào)比自己的要大,就表明自己是一個(gè)過(guò)時(shí)的Leader,需要停止發(fā)送心跳包,主動(dòng)切換為Follower;
success:接收者(Follower)是否能夠匹配prevLogIndex和prevLogTerm,匹配即說(shuō)明請(qǐng)求成功。
Leader節(jié)點(diǎn)處理客戶端寫(xiě)請(qǐng)求以及將寫(xiě)請(qǐng)求日記復(fù)制給Follower的流程:
0)、客戶端向Leader發(fā)送寫(xiě)請(qǐng)求;
1)、Leader將寫(xiě)請(qǐng)求解析成操作指令日記追加到本地日志文件中;
2)、Leader異步向其它Follower節(jié)點(diǎn)發(fā)送AppendEntriesRPC請(qǐng)求;
3)、阻塞等待多數(shù)節(jié)點(diǎn)響應(yīng)成功,多數(shù)節(jié)點(diǎn)至少是節(jié)點(diǎn)總數(shù)除以2加1,由于Leader節(jié)點(diǎn)自己也算一個(gè),因此只需要節(jié)點(diǎn)總數(shù)除以2個(gè)節(jié)點(diǎn)響應(yīng)成功即可;
4)、如果多數(shù)節(jié)點(diǎn)響應(yīng)成功:Leader將該日志條目提交并應(yīng)用到本地狀態(tài)機(jī),異步告知其它Follower節(jié)點(diǎn)日記已經(jīng)提交,之后立即向客戶端返回操作結(jié)果;
5)、否則:響應(yīng)失敗給客戶端。
Follower節(jié)點(diǎn)處理日記復(fù)制請(qǐng)求流程:
0)、接收到任何AppendEntriesRPC請(qǐng)求(包含心跳包請(qǐng)求、提交日記請(qǐng)求、追加日記請(qǐng)求),都重置選舉超時(shí)計(jì)時(shí)器的當(dāng)前計(jì)時(shí);
1)、如果自身的term大于請(qǐng)求參數(shù)term,另本地記錄的Leader的任期號(hào)小于自身,則返回自身的term,且success為false(告知請(qǐng)求方:你已經(jīng)是過(guò)期的Leader);
2)、否則如果Follower自身在prevLogIndex日記的任期號(hào)與請(qǐng)求參數(shù)prevLogTerm不匹配,返回自身的term,且success為false(當(dāng)前Follower節(jié)點(diǎn)的日記落后了);
3)、否則如果當(dāng)前只是一個(gè)心跳包,說(shuō)明是接收到Leader的心跳,說(shuō)明自己已經(jīng)是Follower,如果需要?jiǎng)t將自己從候選節(jié)點(diǎn)切換為Follower節(jié)點(diǎn),返回自身的term,且success為true;
4)、否則,F(xiàn)ollower進(jìn)行日記一致性檢查,刪除已經(jīng)存在但不一致的日記,添加任何在已有的日記中不存在的條目,刪除多余的條目,并且,如果是復(fù)制已經(jīng)提交的條目,復(fù)制成功時(shí)直接提交;
5)、如果請(qǐng)求參數(shù)的leaderCommit大于自身的當(dāng)前commitIndex,則將commitIndex更新為Max(leaderCommit,commitIndex),樂(lè)觀地將本地已提交日記的commitIndex躍進(jìn)到領(lǐng)導(dǎo)人為該Follower跟蹤記得的值,用于Follower剛從故障中恢復(fù)過(guò)來(lái)的場(chǎng)景。
如果Follower節(jié)點(diǎn)向Leader節(jié)點(diǎn)響應(yīng)日記追加失敗且Follower節(jié)點(diǎn)的當(dāng)前期號(hào)小于等于Leader的當(dāng)前期號(hào),Leader節(jié)點(diǎn)將請(qǐng)求參數(shù)prevLogIndex遞減,然后重新發(fā)起AppendEntriesRPC請(qǐng)求,直到AppendEntriesRPC返回成功為止,這才表明在prevLogIndex位置的日志條目中領(lǐng)導(dǎo)人與追隨者的保持一致。這時(shí),F(xiàn)ollower節(jié)點(diǎn)上prevLogIndex位置之前的日志條目將全部保留,在prevLogIndex位置之后(與Leader有沖突)的日志條目將被Follower全部刪除,并且從該位置起追加Leader上在prevLogIndex位置之后的所有日志條目。因此,一旦AppendEntriesRPC返回成功,Leader和Follower的日志就可以保持一致了。
一致性
由于一個(gè)候選節(jié)點(diǎn)必須是得到多數(shù)節(jié)點(diǎn)投票才能成為L(zhǎng)eader,且投票時(shí)節(jié)點(diǎn)不會(huì)把票投給沒(méi)有自己的日志新的候選節(jié)點(diǎn),再者Leader只在已經(jīng)將日記成功同步給多數(shù)節(jié)點(diǎn)(包括自己)才提交日記(將日記變成已提交狀態(tài),同時(shí)應(yīng)用到狀態(tài)機(jī)),因此每次選舉出來(lái)的Leader就都是包含所有已提交日志的節(jié)點(diǎn)。
當(dāng)新的Leader節(jié)點(diǎn)將新日記同步給某個(gè)Follower節(jié)點(diǎn)時(shí),如果該Follower節(jié)點(diǎn)的日記落后很多,該Follower節(jié)點(diǎn)會(huì)主動(dòng)移除Leader上沒(méi)有的日記,并且同步Leader節(jié)點(diǎn)日記給Follower。對(duì)于Leader節(jié)點(diǎn)已經(jīng)標(biāo)志為已提交的日記,F(xiàn)ollower在接收時(shí)就可以直接應(yīng)用到狀態(tài)機(jī),以保持?jǐn)?shù)據(jù)最終一致性。
Multi Raft
假設(shè)有三臺(tái)機(jī)器,每臺(tái)機(jī)器部署一個(gè)Raft節(jié)點(diǎn)服務(wù),由于讀寫(xiě)請(qǐng)求都由Leader節(jié)點(diǎn)處理,那么不就只能有一臺(tái)機(jī)器工作?
我們可以給一個(gè)節(jié)點(diǎn)服務(wù)啟動(dòng)多個(gè)Raft服務(wù)(注意不是多個(gè)進(jìn)程),構(gòu)造成多個(gè)Raft集群,即Multi Raft,這樣每個(gè)Raft集群的Leader節(jié)點(diǎn)就可能均勻分布在多臺(tái)機(jī)器上。例如:
| 機(jī)器 | Raft節(jié)點(diǎn) | 
            Raft節(jié)點(diǎn) | 
            Raft節(jié)點(diǎn) | 
        
|---|---|---|---|
機(jī)器1 | 
            Raft服務(wù)A節(jié)點(diǎn)1(Leader) | 
            Raft服務(wù)B節(jié)點(diǎn)1(Follower) | 
            Raft服務(wù)C節(jié)點(diǎn)1(Follower) | 
        
機(jī)器2 | 
            Raft服務(wù)A節(jié)點(diǎn)2(Follower) | 
            Raft服務(wù)B節(jié)點(diǎn)2(Leader) | 
            Raft服務(wù)C節(jié)點(diǎn)2(Follower) | 
        
機(jī)器3 | 
            Raft服務(wù)A節(jié)點(diǎn)3(Follower) | 
            Raft服務(wù)B節(jié)點(diǎn)3(Follower) | 
            Raft服務(wù)C節(jié)點(diǎn)3(Leader) | 
        
在分布式數(shù)據(jù)庫(kù)TiDB中,就采用了Multi Raft,將數(shù)據(jù)進(jìn)行分片處理,讓每個(gè)Raft集群?jiǎn)为?dú)負(fù)責(zé)一部分?jǐn)?shù)據(jù)。
參考文獻(xiàn)
華為云容器服務(wù)團(tuán)隊(duì).《云原生分布式存儲(chǔ)基石:etcd深入解析》 (云計(jì)算技術(shù)系列叢書(shū))
Raft論文地址
Raft論文中文版:https://github.com/maemual/raft-zh_cn
圖片來(lái)源
圖片來(lái)源:https://github.com/maemual/raft-zh_cn/tree/master/images
本文轉(zhuǎn)載自微信公眾號(hào)「Java藝術(shù)」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Java藝術(shù)公眾號(hào)。



















 
 
 













 
 
 
 