科普: 中间件底层实现的分布式协议之Raft
正式介紹 Raft 協(xié)議之前,我們先來舉一個例子🌰進(jìn)行展開。
方式一:
在一個技術(shù)團(tuán)隊內(nèi)假設(shè)角色都是?均等的,會導(dǎo)致什么情況呢?產(chǎn)品提出一個需求,就可以隨便去找團(tuán)隊中的任意一個人去發(fā)起需求。如果這個人因為請假走了,但是他沒有把需求及時同步給團(tuán)隊其他人,因此會導(dǎo)致該需求存在很大的延遲。
方式二:
在技術(shù)團(tuán)隊中選舉一個?Leader 角色,產(chǎn)品提出的需求必須優(yōu)先提給 Leader,找 Leader 先溝通。Leader 自己消化完后,在將需求傳達(dá)給團(tuán)隊其他成員。如果 Leader 請假了,會指定某一個人充當(dāng) Leader 角色負(fù)責(zé)接收產(chǎn)品需求,并將需求同步給其他成員。
上述很簡單的案例,可以對應(yīng)理解分布式系統(tǒng)中的數(shù)據(jù)一致性算法。
分布式系統(tǒng)數(shù)據(jù)一致性算法一般都應(yīng)用在中間件項目中,平時你寫的業(yè)務(wù)代碼,一般根本不會接觸到的。
SpringCloud 家族中集成了大名鼎鼎的注冊中心 Eureka?,就使用的上面的?方式一?的思路來完成節(jié)點之間數(shù)據(jù)同步的,稱為?Peer To Peer?集群架構(gòu)。
Eureka 當(dāng)中的每個節(jié)點的地位都是均等的,每個節(jié)點都可以接收寫入請求,每個節(jié)點接收請求之后,進(jìn)行請求打包處理,異步化延遲一點時間,將數(shù)據(jù)同步給 Eureka 集群當(dāng)中的其他節(jié)點。任何一臺節(jié)點宕機之后,理論上應(yīng)該是不影響集群運行的,都可以從其他節(jié)點獲取注冊表信息。
另外的一些開源項目采用?方式二?的思路,比如 Etcd、Consul,Redis-Sentinel、Zookeeper,還有未來會流行的國產(chǎn)技術(shù)阿里開源的 Nacos 項目,其中的 CP 模式也是基于?Raft 協(xié)議?來實現(xiàn)分布式一致性算法的。當(dāng)然 Zookeeper 在 Raft 協(xié)議基礎(chǔ)上做了一些改良,使用的 ZAB 分布式一致性協(xié)議來實現(xiàn)的。
在 Raft 協(xié)議出現(xiàn)之前,廣泛使用的一致性算法是 Paxos。Paxos 算法解決的是如何保障單一客戶端操作的一致性,完成每個操作都需要至少兩輪的消息交換。和 Paxos 算法不同,Raft 里有 Leader 的概念,Raft 在處理任何客戶端操作之前必須要選舉出來一個 leader 角色,選舉一個 Leader 經(jīng)過至少一輪的消息交換,但是選舉了 Leader 之后,處理每個客戶端的操作只需要一輪消息交換。
詳解 Raft 協(xié)議
?
接下來,圍繞如下幾點展開:
1)Leader選舉流程
2)節(jié)點心跳檢測
3)選主超時操作
4)日志復(fù)制流程
5)日志條目結(jié)構(gòu)
6)Leader崩潰恢復(fù)操作
1)Leader選舉流程
?
如果系統(tǒng)中只有唯一一個節(jié)點,讀寫操作都由這一個節(jié)點來負(fù)責(zé),不會存在數(shù)據(jù)不一致的問題。
但是,對于分布式系統(tǒng)會存在至少兩個節(jié)點,需要有一個角色來充唯一的節(jié)點,我們把這個唯一節(jié)點叫做?Leader 節(jié)點,其他節(jié)點叫做?Follower 節(jié)點, leader 選舉過程中才會有的狀態(tài)叫做?Candidate 節(jié)點。
?
為了便于理解和說明,假設(shè)集群中有三個節(jié)點:節(jié)點A、節(jié)點B、節(jié)點C。
集群啟動后,初始節(jié)點狀態(tài)都是 Follower 狀態(tài)。最開始各個節(jié)點啟動之后,此時集群內(nèi)還沒有 Leader。
?
如果每次各節(jié)點都投票給自己,Leader 會始終無法選出來,這樣僵持下去肯定是不行的。所以出現(xiàn)了 electionTimeout 的概念,稱為?選舉超時時間?每個節(jié)點都會有 electionTimeout。
一旦發(fā)現(xiàn)一輪投票沒有結(jié)果,集群中各節(jié)點自身設(shè)置一個 electionTimeout,時間范圍在 150ms ~ 300ms 之間的一個隨機值。
?
當(dāng)節(jié)點 A 的 electionTimeout 時間到了,會將節(jié)點 A 狀態(tài)變?yōu)?Candidate 狀態(tài),節(jié)點 A 蘇醒過來,很Happy,終于可以參與競選了,立馬給自己投了一票 。然后向集群中其他兩個節(jié)點發(fā)起選舉投票(Http協(xié)議或者RPC協(xié)議請求都可以)。
節(jié)點B、C可以認(rèn)為處于 Follower 狀態(tài),如何進(jìn)行選舉投票呢?
這里就出現(xiàn)了?term?的概念,每個節(jié)點都會有個 term,term 表示任期,跟美國總統(tǒng)競選里的任期類似。term 是一個全局且連續(xù)遞增的整數(shù),每完成一次選舉,term會遞增 +1。
Follower 狀態(tài)的節(jié)點此時收到 Candidate 選舉請求,發(fā)現(xiàn)集群中就只有一個 Candidate 狀態(tài)的節(jié)點 A,所以直接投票給 Candidate 節(jié)點。
這里就出現(xiàn)了?voteFor?的概念,每個節(jié)點都會有voteFor,voteFor 表示投票的數(shù)據(jù)。
節(jié)點 A 收到投票反饋之后,引出了另外的判斷條件:必須是集群中過半節(jié)點都投票給自己,也包括自己。
算式:n / 2 + 1
舉個🌰:
集群中有三個節(jié)點,3 / 2 + 1 = 2 必須有 2 個節(jié)點投票成功。
集群中有五個節(jié)點,5 / 2 + 1 = 3 必須有 3 個節(jié)點投票成功。
最終 Candidate 狀態(tài)的節(jié)點 A 很榮幸晉升為 Leader。
2)節(jié)點心跳檢測
我們看到上述的狀態(tài)流轉(zhuǎn)圖,當(dāng)重新選舉 Leader 時,各節(jié)點自身的狀態(tài)會在 Candidate、Follower、Leader 之間不斷的變換。
所以,被選舉的節(jié)點 A 成為了 Leader ,為了保持它的『統(tǒng)治』地位,要不斷的向其他節(jié)點發(fā)送心跳,告訴他們「我還活著,我還活著... 」。
其他節(jié)點 B、C 收到心跳請求后,會返回響應(yīng),發(fā)現(xiàn)原來 Leader 還在,不需要發(fā)起選舉,同時要重置選舉超時時間 electionTimeout。
如果作為 Leader 的節(jié)點 A 因為意外,比如網(wǎng)絡(luò)問題無法正常發(fā)送心跳給其他節(jié)點。
此時,節(jié)點 B 的 electionTimeout 超時時間到了,變成了 Candidate 狀態(tài),term + 1,voteFor 投票給自己,并發(fā)送投票請求給節(jié)點 A、C。
之前還是 Leader 的節(jié)點 A 收到了節(jié)點 B 發(fā)來的投票請求,發(fā)現(xiàn) term 任期不是最大的了,更新自身的 term 值為節(jié)點 B 的 term,從 Leader 狀態(tài)切換為 Follower 狀態(tài),并重置 electionTimeout。
3)選主超時操作
?
如果恰巧有兩個節(jié)點同時都發(fā)起了投票,但都沒有獲得多數(shù)節(jié)點的選票,此時就得打加時賽了,直到獲得多數(shù)選票的節(jié)點成為 Leader。
如下圖所示:
?
假設(shè)集群中有4個節(jié)點,A、B、C、D。出現(xiàn)了兩個節(jié)點處于 Candidate 狀態(tài)。
第一輪選舉狀態(tài)如下:
節(jié)點A[Candidate] <----- 節(jié)點 C 選舉節(jié)點A節(jié)點B[Candidate] <----- 節(jié)點 D 選舉節(jié)點B第一輪選舉之后,節(jié)點 A 和節(jié)點 B 還是處于 Candidate 狀態(tài),都不滿足集群過半數(shù)投票成功的條件,選舉 Leader 失敗。
下一輪就要看 Candidate 狀態(tài)的節(jié)點誰的 electionTimeout 先到,誰就先發(fā)起新一輪選舉操作。只要發(fā)起選舉,term 任期就要遞增的。
如果節(jié)點 B 的 electionTimeout 先到,先發(fā)起選舉操作,那么集群中按照條件必須是有 3 個節(jié)點投票成功之后最終成為 Leader。
為了避免出現(xiàn)上述這種平局的情況,所以一般集群中節(jié)點部署個數(shù)都是奇數(shù):?2n + 1
?
4)日志復(fù)制流程
?
當(dāng) Leader 選出來之后,Client 客戶端發(fā)起的寫請求都會由 Leader 節(jié)點來處理。
即使其他的 Follower 節(jié)點收到了 Client 客戶端的請求,也會將請求轉(zhuǎn)交給 Leader 來處理。
Leader 接收到 Client 的請求之后,會優(yōu)先將數(shù)據(jù)寫入到自身節(jié)點的 log 日志文件中。
被寫入到日志文件里的日志條目,被稱為?append entries?。
Leader 將發(fā)送 append entries 消息給其他節(jié)點,這里并不是每次都將相同的日志條目,都發(fā)送給其他節(jié)點,而是根據(jù)節(jié)點的不同對應(yīng)的消息也是不同的。因為集群中節(jié)點之間數(shù)據(jù)可能會有不一致的情況。
其他 Follower 節(jié)點收到 Leader 的消息后,將數(shù)據(jù)添加到本地,然后返回給 Leader 響應(yīng),確認(rèn)消息已收到。
Leader 節(jié)點收到其他節(jié)點確認(rèn)消息且過半數(shù),先 Commit 提交記錄,返回 Client 客戶端響應(yīng)。
然后,Leader 節(jié)點再次向其他 Follower 節(jié)點發(fā)送 Commit 提交數(shù)據(jù)通知。
其他 Follower 節(jié)點收到通知后,Commit 提交自身的日志條目數(shù)據(jù),返回 Leader 更新結(jié)束的通知。
日志條目提交保證兩點:
容錯:
在數(shù)量少于 Raft 服務(wù)器節(jié)點總數(shù)一半的 Follower 失敗的情況下 ,Raft 集群仍然可以正常處理來自客戶端的請求。
確保重疊:
一旦 Leader 響應(yīng)了一個客戶端的請求,即使出現(xiàn)了 Raft 集群中少數(shù)服務(wù)器的失敗,也會有一個服務(wù)器包含所有以前提交的日志條目。
?
5)日志條目結(jié)構(gòu)
?
每個節(jié)點都會有兩個索引,日志條目最終提交完成,提交的索引值稱為?commitIndex,另外一個是目前最后一行索引值,稱為?lastApplied
Leader 節(jié)點除了存儲上面提到的 commitIndex 和 lastApplied之外,還需要存儲其他 Follower 節(jié)點的數(shù)據(jù)情況。
一個是?nextIndex?記錄的是 Leader 里有,Follower 節(jié)點沒有的數(shù)據(jù)索引,即需要發(fā)送 append entries 的數(shù)據(jù)索引。
另外一個是?matchIndex?,記錄的是 Leader 節(jié)點已知,已經(jīng)復(fù)制給他 Follower 節(jié)點日志的最高索引值。
當(dāng)數(shù)據(jù)變更時, Leader 向其他節(jié)點發(fā)送不同的 append entries 消息數(shù)據(jù)。
如果重新選舉了 Leader,新的 Leader 并不知道原來 Leader 的 nextIndex 和 matchIndex 兩個數(shù)據(jù),會將自身節(jié)點的 nextIndex 重置為 commitIndex,matchIndex 則全部重置為 0。
?
6)選主后日志同步
?
當(dāng)集群中的 Leader 接收到 Client 請求,發(fā)現(xiàn)部分節(jié)點因為網(wǎng)絡(luò)問題無法正常通訊,所以日志不能復(fù)制給多數(shù)節(jié)點,日志無法正常提交。
如下圖所示:
其他 Follower 節(jié)點無法正常收到 Leader 心跳檢測請求,會發(fā)生重新選主操作。
當(dāng)新的 Leader1 產(chǎn)生,原來的 Leader2 集群恢復(fù)之后,發(fā)現(xiàn)自己不是選票最多的節(jié)點,即 term 不是最大的,節(jié)點狀態(tài)切換到 Follower,回滾未提交的日志。
新的 Leader1 重新將同步日志重新同步給其他 Follower 節(jié)點。
如下圖所示 :
?
總結(jié)
?
經(jīng)過對 Raft 協(xié)議的詳細(xì)分析,我們能夠總結(jié)到,其實 Raft 協(xié)議就是?2PC (兩階段提交) + 集群過半節(jié)點寫機制?結(jié)合實現(xiàn)的。
1、每個節(jié)點都有 electionTimeout 選舉超時時間,用于當(dāng) Follower 節(jié)點無法收到心跳時,到期發(fā)起選舉 Leader 的操作。
2、Leader 節(jié)點選舉時,最終選出來的 Leader 的 term 任期一定是最大的。
3、Leader 節(jié)點給其他 Follower 節(jié)點發(fā)送 append enties 日志條目時,針對不同的節(jié)點發(fā)送不同的日志消息。
4、Leader 接收客戶端的寫入請求,此時處于 uncommitted 狀態(tài),Leader 將 uncommitted 消息發(fā)送給其他 Follower 節(jié)點,作為第一階段。
5、其他 Follower 節(jié)點收到 uncommitted 請求之后,返回確認(rèn) ack 響應(yīng)給 Leader,如果 Leader 收到了過半數(shù) Follower 節(jié)點的 ack 響應(yīng),則將請求標(biāo)記為 committed,同時發(fā)送 committed 請求發(fā)送給其他 Follower 節(jié)點,讓其他節(jié)點對消息進(jìn)行 committed。作為第二階段 + 過半數(shù)機制。
另外,剛剛文中提到阿里開源的 Nacos 注冊中心,其內(nèi)部自己實現(xiàn)了一套簡化版的 Raft 協(xié)議。看了 Nacos 官方的 Roadmap 能夠了解到,規(guī)劃在 Nacos 1.7.0 GA 中會替換掉現(xiàn)在的 Raft 協(xié)議實現(xiàn),有可能會采用螞蟻金服開源的SOFA-JRaft,SOFA-JRaft 基于 Raft 一致性算法的生產(chǎn)級高性能 Java 實現(xiàn),支持 MULTI-RAFT-GROUP,適用于高負(fù)載低延遲的場景。
?
參考:
http://ifeve.com/raft/?
http://thesecretlivesofdata.com/raft/
總結(jié)
以上是生活随笔為你收集整理的科普: 中间件底层实现的分布式协议之Raft的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: rpc核心实现和原理
- 下一篇: 类加载机制-深入理解jvm