您好,登錄后才能下訂單哦!
本篇文章為大家展示了SOFAJRaft | SOFAChannel#8 的示例分析,內(nèi)容簡明扼要并且容易理解,絕對(duì)能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。
Raft 是一種共識(shí)算法,其特點(diǎn)是讓多個(gè)參與者針對(duì)某一件事達(dá)成完全一致:一件事,一個(gè)結(jié)論。同時(shí)對(duì)已達(dá)成一致的結(jié)論,是不可推翻的??梢耘e一個(gè)銀行賬戶的例子來解釋共識(shí)算法:假如由一批服務(wù)器組成一個(gè)集群來維護(hù)銀行賬戶系統(tǒng),如果有一個(gè) Client 向集群發(fā)出“存 100 元”的指令,那么當(dāng)集群返回成功應(yīng)答之后,Client 再向集群發(fā)起查詢時(shí),一定能夠查到被存儲(chǔ)成功的這 100 元錢,就算有機(jī)器出現(xiàn)不可用情況,這 100 元的賬也不可篡改。這就是共識(shí)算法要達(dá)到的效果。
Raft 算法和其他的共識(shí)算法相比,又有了如下幾個(gè)不同的特性:
Strong leader:Raft 集群中最多只能有一個(gè) Leader,日志只能從 Leader 復(fù)制到 Follower 上;
Leader election:Raft 算法采用隨機(jī)選舉超時(shí)時(shí)間觸發(fā)選舉來避免選票被瓜分的情況,保證選舉的順利完成;
Membership changes:通過兩階段的方式應(yīng)對(duì)集群內(nèi)成員的加入或者退出情況,在此期間并不影響集群對(duì)外的服務(wù);
共識(shí)算法有一個(gè)很典型的應(yīng)用場景就是復(fù)制狀態(tài)機(jī)。Client 向復(fù)制狀態(tài)機(jī)發(fā)送一系列能夠在狀態(tài)機(jī)上執(zhí)行的命令,共識(shí)算法負(fù)責(zé)將這些命令以 Log 的形式復(fù)制給其他的狀態(tài)機(jī),這樣不同的狀態(tài)機(jī)只要按照完全一樣的順序來執(zhí)行這些命令,就能得到一樣的輸出結(jié)果。所以這就需要利用共識(shí)算法保證被復(fù)制日志的內(nèi)容和順序一致。
cdn.nlark.com/yuque/0/2019/jpeg/307286/1567322241949-064625c5-2919-4853-8c2a-b9b8a62ff688.jpeg"> 圖1 - 復(fù)制狀態(tài)機(jī)
SOFAJRaft 是基于 Raft 算法的生產(chǎn)級(jí)高性能 Java 實(shí)現(xiàn),支持 MULTI-RAFT-GROUP。應(yīng)用場景有 Leader 選舉、分布式鎖服務(wù)、高可靠的元信息管理、分布式存儲(chǔ)系統(tǒng)。
圖2 - SOFAJRaft 結(jié)構(gòu)
這張圖就是 SOFAJRaft 的設(shè)計(jì)圖,Node 代表了一個(gè) SOFAJRaft Server 節(jié)點(diǎn),這些方框代表他內(nèi)部的各個(gè)模塊,我們依然用之前的銀行賬戶系統(tǒng)舉例來說明 SOFAJRaft 的各模塊是如何工作的。
當(dāng) Client 向 SOFAJRaft 發(fā)來一個(gè)“存 100 元”的命令之后,Node 的 Log 存儲(chǔ)模塊首先將這個(gè)命令以 Log 的形式存儲(chǔ)到本地,同時(shí) Replicator 會(huì)把這個(gè) Log 復(fù)制給其他的 Node,Replicator 是有多個(gè)的,集群中有多少個(gè) Follower 就會(huì)有多少個(gè) Replicator,這樣就能實(shí)現(xiàn)并發(fā)的日志復(fù)制。當(dāng) Node 收到集群中半數(shù)以上的 Follower 返回的“復(fù)制成功” 的響應(yīng)之后,就可以把這條 Log 以及之前的 Log 有序的送到狀態(tài)機(jī)里去執(zhí)行了。狀態(tài)機(jī)是由用戶來實(shí)現(xiàn)的,比如我們現(xiàn)在舉的例子是銀行賬戶系統(tǒng),所以狀態(tài)機(jī)執(zhí)行的就是賬戶金額的借貸操作。如果 SOFAJRaft 在別的場景中使用,狀態(tài)機(jī)就會(huì)有其他的執(zhí)行方式。
Snapshot 是快照,所謂快照就是對(duì)數(shù)據(jù)當(dāng)前值的一個(gè)記錄,Leader 生成快照有這么幾個(gè)作用:
當(dāng)有新的 Node 加入集群的時(shí)候,不用只靠日志復(fù)制、回放去和 Leader 保持?jǐn)?shù)據(jù)一致,而是通過安裝 Leader 的快照來跳過早期大量日志的回放;
Leader 用快照替代 Log 復(fù)制可以減少網(wǎng)絡(luò)上的數(shù)據(jù)量;
用快照替代早期的 Log 可以節(jié)省存儲(chǔ)空間;
圖3 - 需要用戶實(shí)現(xiàn):StateMachine、Client
SOFAJRaft 需要用戶去實(shí)現(xiàn)兩部分:StateMachine 和 Client。
因?yàn)?SOFAJRaft 只是一個(gè)工具,他的目的是幫助我們?cè)诩簝?nèi)達(dá)成共識(shí),而具體要對(duì)什么業(yè)務(wù)邏輯達(dá)成共識(shí)是需要用戶自己去定義的,我們將用戶需要去實(shí)現(xiàn)的部分定義為 StateMachine 接口。比如賬務(wù)系統(tǒng)和分布式存儲(chǔ)這兩種業(yè)務(wù)就需要用戶去實(shí)現(xiàn)不同的 StateMachine 邏輯。而 Client 也很好理解,根據(jù)業(yè)務(wù)的不同,用戶需要去定義不同的消息類型和客戶端的處理邏輯。
圖4 - 需要用戶實(shí)現(xiàn)一些接口
前面介紹了這么多,我們引出今天的主題:如何用 SOFAJRaft 實(shí)現(xiàn)一個(gè)分布式計(jì)數(shù)器?
我們的需求其實(shí)很簡單,用一句話來說就是:提供一個(gè) Counter,Client 每次計(jì)數(shù)時(shí)可以指定步幅,也可以隨時(shí)發(fā)起查詢。
我們對(duì)這個(gè)需求稍作分析后,將它翻譯成具體的功能點(diǎn),主要有三部分:
實(shí)現(xiàn):Counter server,具備計(jì)數(shù)功能,具體運(yùn)算公式為:Cn = Cn-1 + delta;
提供寫服務(wù),寫入 delta 觸發(fā)計(jì)數(shù)器運(yùn)算;
提供讀服務(wù),讀取當(dāng)前 Cn 值;
除此之外,我們還有一個(gè)可用性的可選需求,需要有備份機(jī)器,讀寫服務(wù)不能不可用。
根據(jù)剛才分析出來的功能需求,我們?cè)O(shè)計(jì)出 1.0 的架構(gòu),這個(gè)架構(gòu)很簡單,一個(gè)節(jié)點(diǎn) Counter Server 提供計(jì)數(shù)功能,接收客戶端發(fā)起的計(jì)數(shù)請(qǐng)求和查詢請(qǐng)求。
圖5 - 架構(gòu) 1.0
但是這樣的架構(gòu)設(shè)計(jì)存在這樣兩個(gè)問題:一是 Server 是一個(gè)單點(diǎn),一旦 Server 節(jié)點(diǎn)故障服務(wù)就不可用了;二是運(yùn)算結(jié)果都存儲(chǔ)在內(nèi)存當(dāng)中,節(jié)點(diǎn)故障會(huì)導(dǎo)致數(shù)據(jù)丟失。
圖6 - 架構(gòu) 1.0 的不足:單點(diǎn)
針對(duì)第二個(gè)問題,我們優(yōu)化一下,加一個(gè)本地文件存儲(chǔ)。這樣每次計(jì)數(shù)器完成運(yùn)算之后都將數(shù)據(jù)落盤,當(dāng)節(jié)點(diǎn)故障之時(shí),我們要新起一臺(tái)備用機(jī)器,將文件數(shù)據(jù)拷貝過來,然后接替故障機(jī)器對(duì)外提供服務(wù)。這樣就解決了數(shù)據(jù)丟失的風(fēng)險(xiǎn),但是同時(shí)也引來另外的問題:磁盤 IO 很頻繁,同時(shí)這種冷備的模式也依然會(huì)導(dǎo)致一段時(shí)間的服務(wù)不可用。
圖7 - 架構(gòu) 1.0 的不足:冷備
所以我們提出架構(gòu) 2.0,采用集群的模式提供服務(wù)。我們用三個(gè)節(jié)點(diǎn)組成集群,由一個(gè)節(jié)點(diǎn)對(duì)外提供服務(wù),當(dāng) Server 接收到 Client 發(fā)來的寫請(qǐng)求之后,Server 運(yùn)算出結(jié)果,然后將結(jié)果復(fù)制給另外兩臺(tái)機(jī)器,當(dāng)收到其他所有節(jié)點(diǎn)的成功響應(yīng)之后,Server 向 Client 返回運(yùn)算結(jié)果。
圖8 - 架構(gòu) 2.0
但是這樣的架構(gòu)也存在這問題:
我們選擇哪一臺(tái) Server 扮演 Leader 的角色對(duì)外提供服務(wù);
當(dāng) Leader 不可用之后,選擇哪一臺(tái)接替它;
Leader 處理寫請(qǐng)求的時(shí)候需要等到所有節(jié)點(diǎn)都響應(yīng)之后才能響應(yīng) Client;
也是比較重要的,我們無法保證 Leader 向 Follower 復(fù)制數(shù)據(jù)是有序的,所以任一時(shí)刻三個(gè)節(jié)點(diǎn)的數(shù)據(jù)都可能是不一樣的;
保證復(fù)制數(shù)據(jù)的順序和內(nèi)容,這就有了共識(shí)算法的用武之地,所以在接下來的 3.0 架構(gòu)里,我們使用 SOFAJRaft 來助力集群的實(shí)現(xiàn)。
圖8 - 架構(gòu) 3.0:使用 SOFAJRaft
3.0 架構(gòu)中,Counter Server 使用 SOFAJRaft 來組成一個(gè)集群,Leader 的選舉和數(shù)據(jù)的復(fù)制都交給 SOFAJRaft 來完成。在時(shí)序圖中我們可以看到,Counter 的業(yè)務(wù)邏輯重新變得像架構(gòu) 1.0 中一樣簡潔,維護(hù)數(shù)據(jù)一致的工作都交給 SOFAJRaft 來完成,所以圖中灰色的部分對(duì)業(yè)務(wù)就不感知了。
圖9 - 架構(gòu) 3.0:時(shí)序圖
在使用 SOFAJRaft 的 3.0 架構(gòu)中,SOFAJRaft 幫我們完成了 Leader 選舉、節(jié)點(diǎn)間數(shù)據(jù)同步的工作,除此之外,SOFAJRaft 只需要半數(shù)以上節(jié)點(diǎn)響應(yīng)即可,不再需要集群所有節(jié)點(diǎn)的應(yīng)答,這樣可以進(jìn)一步提高寫請(qǐng)求的處理效率。
圖10 - 架構(gòu) 3.0:SOFAJRaft 實(shí)現(xiàn) Leader 選舉、日志復(fù)制
那么怎么使用 SOFAJRaft 呢?我們之前說過,SOFAJRaft 主要暴露了兩個(gè)地方給我們?nèi)?shí)現(xiàn),一是 Cilent,另一個(gè)是 StateMachine,所以我們的計(jì)數(shù)器也就是要去做這兩部分。
在 Client 上,我們要定義具體的消息類型,針對(duì)不同的消息類型,還需要去實(shí)現(xiàn)消息的 Processor 來處理這些消息,接下來這些消息就交給 SOFAJRaft 去完成集群內(nèi)部的數(shù)據(jù)同步。
在 StateMachine 上,我們要去實(shí)現(xiàn)狀態(tài)機(jī)暴露給我們待實(shí)現(xiàn)的幾個(gè)接口,最重要的是 onApply 接口,要在這個(gè)接口里將 Cilent 的請(qǐng)求指令進(jìn)行運(yùn)算,轉(zhuǎn)換成具體的計(jì)數(shù)器值。而 onSnapshotSave 和 onSnapshotLoad 接口則是負(fù)責(zé)快照的生成和加載。
圖11 - 模塊關(guān)系
下面這張圖是最終實(shí)現(xiàn)的模塊關(guān)系圖,其實(shí)他已經(jīng)是代碼實(shí)現(xiàn)之后的產(chǎn)物了,在這里并沒有貼出具體的代碼,因?yàn)榇a已經(jīng)隨我們的項(xiàng)目一起開源了。我們實(shí)現(xiàn)了兩種消息類型 IncrementAndGetRequest 和 GetValueRequest,分別對(duì)應(yīng)寫請(qǐng)求和讀請(qǐng)求,因?yàn)閮煞N請(qǐng)求的響應(yīng)都是計(jì)數(shù)器的值,所以同用一個(gè) ValueResponse。兩種請(qǐng)求,所以對(duì)應(yīng)兩種 Processor:IncrementAndGetRequestProcessor 和 GetValueRequestProcessor,狀態(tài)機(jī) CounterStateMachine 實(shí)現(xiàn)了之前提到的三個(gè)接口,除此之外還實(shí)現(xiàn)了 onLeaderStart 和 onLeaderStop,用來在節(jié)點(diǎn)成為 leader 和失去 leader 資格時(shí)做一些處理。這個(gè)地方在寫請(qǐng)求的處理中使用了 IncrementAndAddClosure ,這樣就可以通過 callback 的方式來實(shí)現(xiàn)響應(yīng)。
圖12 - 類關(guān)系圖
來看看整個(gè)的啟動(dòng)過程。首先來看 Follower 節(jié)點(diǎn)的啟動(dòng) (當(dāng)然,在啟動(dòng)之前,我們并不知道哪個(gè)節(jié)點(diǎn)會(huì)是 Leader),Counter 在本地起三個(gè)進(jìn)程用來模擬三個(gè)節(jié)點(diǎn),它們分別使用 8081、8082、8083 三個(gè)端口,標(biāo)記其為 A、B、C 節(jié)點(diǎn)。
A 節(jié)點(diǎn)率先啟動(dòng),然后開始向 B 和 C 發(fā)送 preVote 請(qǐng)求,但是這時(shí)候另外兩個(gè)節(jié)點(diǎn)都尚未啟動(dòng),所以 A 節(jié)點(diǎn)通信失敗,然后等待,再重試,如此往復(fù)。在 A 節(jié)點(diǎn)某次通信失敗后的等待之中,它突然收到了 B 節(jié)點(diǎn)發(fā)來的 preVote 請(qǐng)求,在經(jīng)過一系列 check 之后,它認(rèn)可了這個(gè) preVote 請(qǐng)求,并且返回成功響應(yīng),隨后又對(duì) B 節(jié)點(diǎn)發(fā)來的 vote 請(qǐng)求成功響應(yīng),然后我們可以看到,B 節(jié)點(diǎn)成功當(dāng)選 Leader。這就是 Follower A 的啟動(dòng)、投票過程。
圖13 - Follower 啟動(dòng)日志
我們?cè)倏纯?B 節(jié)點(diǎn)的啟動(dòng),B 節(jié)點(diǎn)在啟動(dòng)之后,剛好處于 A 節(jié)點(diǎn)的一次等待間隙之中,所以它沒有收到其他節(jié)點(diǎn)發(fā)來的 preVote 請(qǐng)求,因此它向另外兩個(gè)節(jié)點(diǎn)發(fā)起了 preVote 請(qǐng)求,試圖競選。接下來它收到了 A 節(jié)點(diǎn)發(fā)來的確認(rèn)響應(yīng),接著 B 節(jié)點(diǎn)又發(fā)起了 vote 請(qǐng)求,依然收到了 A 節(jié)點(diǎn)的響應(yīng)。這樣 B 節(jié)點(diǎn)就收到了超過集群半數(shù)以上的投票并成功當(dāng)選 (A 節(jié)點(diǎn)和 B 節(jié)點(diǎn)自己,達(dá)到 2/3) 。在此過程中,C 節(jié)點(diǎn)一直沒有啟動(dòng),但是由于 A 和 B 構(gòu)成半數(shù)以上,所以共識(shí)算法已經(jīng)可以正常 work。
圖14 - Leader 啟動(dòng)日志
在剛才的過程中,我們提到了兩個(gè)關(guān)鍵詞:preVote 和 vote,這是選舉中的兩個(gè)階段,之所以要設(shè)置 preVote,是為了應(yīng)對(duì)網(wǎng)絡(luò)分區(qū)的情況。關(guān)于 SOFAJRaft 的選舉,我們有專門的文章去解析 ,大家可以進(jìn)一步了解。在這里我們將選舉的評(píng)選原則粗略的描述為:哪個(gè)節(jié)點(diǎn)保存的日志最新最完整,它就更有資格成為 leader。
接下來我們看看 Client 發(fā)起的一次寫請(qǐng)求。Client 共發(fā)起了三次寫請(qǐng)求,分別是 "+0"、"+1"、"+2"。從日志上我們可以看到,Leader 在收到這些請(qǐng)求之后,先把他們以日志的形式發(fā)送給其他節(jié)點(diǎn) (并且是批量的),當(dāng)它收到其他節(jié)點(diǎn)對(duì)日志復(fù)制的成功響應(yīng)之后,再更新 committedIndex,最后調(diào)用 onApply 接口,執(zhí)行 counter 的計(jì)數(shù)運(yùn)算,將 client 發(fā)來的指令加到計(jì)數(shù)器當(dāng)中。在這個(gè)過程中,可以看到 Leader 在處理寫請(qǐng)求的時(shí)候一個(gè)很重要的步驟就是將日志復(fù)制給其他節(jié)點(diǎn)。來詳細(xì)看下這個(gè)過程,以及當(dāng)中提到的 committedIndex。
圖15 - Leader 處理寫請(qǐng)求
CommittedIndex 標(biāo)志了一個(gè)位點(diǎn),它標(biāo)志在此之前的所有日志都已經(jīng)復(fù)制到了集群半數(shù)以上的節(jié)點(diǎn)之中。圖中可以看到,committedIndex 初始指在 "3" 這個(gè)位置上,表示 "0-3" 的日志都已經(jīng)復(fù)制到了半數(shù)以上節(jié)點(diǎn)之中 (在 Follower 上我們也已經(jīng)看到),接下來 Leader 又把 "4"、"5" 兩條日志批量的復(fù)制到了 Follower 上,這是就可以把 committedIndex 右滑動(dòng)到 "5" 的位置,表示 "0-5" 的日志都已經(jīng)復(fù)制到了半數(shù)以上節(jié)點(diǎn)之中。
圖16 - 日志復(fù)制
這時(shí)又產(chǎn)生了另一個(gè)問題:我們?nèi)绾沃?StateMachine 執(zhí)行到哪一條日志了?通過 committedIndex 我們可以知道哪些日志已經(jīng)成功復(fù)制到集群其他節(jié)點(diǎn)之中了,但是 StateMachine 中此刻的狀態(tài)代表哪一條日志執(zhí)行之后的結(jié)果呢?這就要用 applyIndex 來表示。在圖中,applyIndex 指向 "3",這表示:"0-3" 的日志代表的指令都已經(jīng)被 StateMachine 執(zhí)行,狀態(tài)機(jī)此刻的狀態(tài)代表 "3" 日志執(zhí)行完畢之后的結(jié)果,當(dāng) committedIndex 向右滑動(dòng)之后,applyIndex 就可以伴隨狀態(tài)機(jī)的執(zhí)行繼續(xù)向右滑動(dòng)了。ApplyIndex 和 committedIndex 就可以支持線性一致性讀,關(guān)于這個(gè)概念,我們也已經(jīng)有文章去專門解析了,可以在文末鏈接中了解。
圖17 - ApplyIndex 更新
上述內(nèi)容就是SOFAJRaft | SOFAChannel#8 的示例分析,你們學(xué)到知識(shí)或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識(shí)儲(chǔ)備,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。