成人无码视频,亚洲精品久久久久av无码,午夜精品久久久久久毛片,亚洲 中文字幕 日韩 无码

資訊專欄INFORMATION COLUMN

編寫你的第一個(gè) Java 版 Raft 分布式 KV 存儲(chǔ)

hearaway / 1343人閱讀

摘要:前言本文旨在講述如何使用語(yǔ)言實(shí)現(xiàn)基于算法的,分布式的,結(jié)構(gòu)的存儲(chǔ)項(xiàng)目。甚至像,可以利用實(shí)現(xiàn)分布式存儲(chǔ)。核心組件包括一致性模塊,通信,日志模塊,狀態(tài)機(jī)。狀態(tài)機(jī),可以是任何實(shí)現(xiàn),其實(shí)質(zhì)就是將日志中的內(nèi)容進(jìn)行處理。選舉者優(yōu)先選舉自己將自

前言

本文旨在講述如何使用 Java 語(yǔ)言實(shí)現(xiàn)基于 Raft 算法的,分布式的,KV 結(jié)構(gòu)的存儲(chǔ)項(xiàng)目。該項(xiàng)目的背景是為了深入理解 Raft 算法,從而深刻理解分布式環(huán)境下數(shù)據(jù)強(qiáng)一致性該如何實(shí)現(xiàn);該項(xiàng)目的目標(biāo)是:在復(fù)雜的分布式環(huán)境中,多個(gè)存儲(chǔ)節(jié)點(diǎn)能夠保證數(shù)據(jù)強(qiáng)一致性。

項(xiàng)目地址:https://github.com/stateIs0/l...

歡迎 star :)

什么是 Java 版 Raft 分布式 KV 存儲(chǔ)

Raft 算法大部分人都已經(jīng)了解,也有很多實(shí)現(xiàn),從 GitHub 上來(lái)看,似乎 Golang 語(yǔ)言實(shí)現(xiàn)的較多,比較有名的,例如 etcd。而 Java 版本的,在生產(chǎn)環(huán)境大規(guī)模使用的實(shí)現(xiàn)則較少;

同時(shí),他們的設(shè)計(jì)目標(biāo)大部分都是命名服務(wù),即服務(wù)注冊(cè)發(fā)現(xiàn),也就是說(shuō),他們通常都是基于 AP 實(shí)現(xiàn),就像 DNS,DNS 是一個(gè)命名服務(wù),同時(shí)也不是一個(gè)強(qiáng)一致性的服務(wù)。

比較不同的是 Zookeeper,ZK 常被大家用來(lái)做命名服務(wù),但他更多的是一個(gè)分布式服務(wù)協(xié)調(diào)者。

而上面的這些都不是存儲(chǔ)服務(wù),雖然也都可以做一些存儲(chǔ)工作。甚至像 kafka,可以利用 ZK 實(shí)現(xiàn)分布式存儲(chǔ)。

回到我們這邊。

此次我們語(yǔ)言部分使用 Java,RPC 網(wǎng)絡(luò)通信框架使用的是螞蟻金服 SOFA-Bolt,底層 KV 存儲(chǔ)使用的是 RocksDB,其中核心的 Raft 則由我們自己實(shí)現(xiàn)(如果不自己實(shí)現(xiàn),那這個(gè)項(xiàng)目沒(méi)有意義)。 注意,該項(xiàng)目將舍棄一部分性能和可用性,以追求盡可能的強(qiáng)一致性。

為什么要費(fèi)盡心力重復(fù)造輪子

小時(shí)候,我們閱讀關(guān)于高可用的文章時(shí),最后都會(huì)提到一個(gè)問(wèn)題:服務(wù)掛了怎么辦?

通常有 2 種回答:

如果是無(wú)狀態(tài)服務(wù),那么毫不影響使用。

如果是有狀態(tài)服務(wù),可以將狀態(tài)保存到一個(gè)別的地方,例如 Redis。如果 Redis 掛了怎么辦?那就放到 ZK。

很多中間件,都會(huì)使用 ZK 來(lái)保證狀態(tài)一致,例如 codis,kafka。因?yàn)槭褂?ZK 能夠幫我們節(jié)省大量的時(shí)間。但有的時(shí)候,中間件的用戶覺(jué)得引入第三方中間件很麻煩,那么中間件開(kāi)發(fā)者會(huì)嘗試自己實(shí)現(xiàn)一致性,例如 Redis Cluster, TiDB 等。

而通常自己實(shí)現(xiàn),都會(huì)使用 Raft 算法,那有人問(wèn),為什么不使用"更牛逼的" paxos 算法?對(duì)不起,這個(gè)有點(diǎn)難,至少目前開(kāi)源的、生產(chǎn)環(huán)境大規(guī)模使用的 paxos 算法實(shí)現(xiàn)還沒(méi)有出現(xiàn),只聽(tīng)過(guò) Google 或者 alibaba 在其內(nèi)部實(shí)現(xiàn)過(guò),具體是什么樣子的,這里我們就不討論了。

回到我們的話題,為什么重復(fù)造輪子?從 3 個(gè)方面來(lái)回答:

有的時(shí)候 ZK 和 etcd 并不能解決我們的問(wèn)題,或者像上面說(shuō)的,引入其他的中間件部署起來(lái)太麻煩也太重。

完全處于好奇,好奇為什么 Raft 可以保證一致性(這通??梢酝ㄟ^(guò)汗牛充棟的文章來(lái)得到解答)?但是到底該怎么實(shí)現(xiàn)?

分布式開(kāi)發(fā)的要求,作為開(kāi)發(fā)分布式系統(tǒng)的程序員,如果能夠更深刻的理解分布式系統(tǒng)的核心算法,那么對(duì)如何合理設(shè)計(jì)一個(gè)分布式系統(tǒng)將大有益處。

好,有了以上 3 個(gè)原因,我們就有足夠的動(dòng)力來(lái)造輪子了,接下來(lái)就是如何造的問(wèn)題了。

編寫前的 Raft 理論基礎(chǔ)

任何實(shí)踐都是理論先行。如果你對(duì) Raft 理論已經(jīng)非常熟悉,那么可以跳過(guò)此節(jié),直接看實(shí)現(xiàn)的步驟。

Raft 為了算法的可理解性,將算法分成了 4 個(gè)部分。

leader 選舉

日志復(fù)制

成員變更

日志壓縮

同 zk 一樣,leader 都是必須的,所有的寫操作都是由 leader 發(fā)起,從而保證數(shù)據(jù)流向足夠簡(jiǎn)單。而 leader 的選舉則通過(guò)比較每個(gè)節(jié)點(diǎn)的邏輯時(shí)間(term)大小,以及日志下標(biāo)(index)的大小。

剛剛說(shuō) leader 選舉涉及日志下標(biāo),那么就要講日志復(fù)制。日志復(fù)制可以說(shuō)是 Raft 核心的核心,說(shuō)簡(jiǎn)單點(diǎn),Raft 就是為了保證多節(jié)點(diǎn)之間日志的一致。當(dāng)日志一致,我們可以認(rèn)為整個(gè)系統(tǒng)的狀態(tài)是一致的。這個(gè)日志你可以理解成 mysql 的 binlog。

Raft 通過(guò)各種補(bǔ)丁,保證了日志復(fù)制的正確性。

Raft leader 節(jié)點(diǎn)會(huì)將客戶端的請(qǐng)求都封裝成日志,發(fā)送到各個(gè) follower 中,如果集群中超過(guò)一半的 follower 回復(fù)成功,那么這個(gè)日志就可以被提交(commit),這個(gè) commit 可以理解為 ACID 的 D ,即持久化。當(dāng)日志被持久化到磁盤,后面的事情就好辦了。

而第三點(diǎn)則是為了節(jié)點(diǎn)的擴(kuò)展性。第四點(diǎn)是為了性能。相比較 leader 選舉和 日志復(fù)制,不是那么的重要,可以說(shuō),如果沒(méi)有成員變更和日志壓縮,也可以搞出一個(gè)可用的 Raft 分布式系統(tǒng),但沒(méi)有 leader 選舉和日志復(fù)制,是萬(wàn)萬(wàn)不能的。

因此,本文和本項(xiàng)目將重點(diǎn)放在 leader 選舉和日志復(fù)制。

以上,就簡(jiǎn)單說(shuō)明了 Raft 的算法,關(guān)于 Raft 算法更多的文章,請(qǐng)參考本人博客中的其他文章(包含官方各個(gè)版本論文和 PPT & 動(dòng)畫 & 其他博客文章),博客地址:thinkinjava.cn

實(shí)現(xiàn)的步驟

實(shí)現(xiàn)目標(biāo):基于 Raft 論文實(shí)現(xiàn) Raft 核心功能,即 Leader 選舉 & 日志復(fù)制。

Raft 核心組件包括:一致性模塊,RPC 通信,日志模塊,狀態(tài)機(jī)。

技術(shù)選型:

一致性模塊,是 Raft 算法的核心實(shí)現(xiàn),通過(guò)一致性模塊,保證 Raft 集群節(jié)點(diǎn)數(shù)據(jù)的一致性。這里我們需要自己根據(jù)論文描述去實(shí)現(xiàn)

RPC 通信,可以使用 HTTP 短連接,也可以直接使用 TCP 長(zhǎng)連接,考慮到集群各個(gè)節(jié)點(diǎn)頻繁通信,同時(shí)節(jié)點(diǎn)通常都在一個(gè)局域網(wǎng)內(nèi),因此我們選用 TCP 長(zhǎng)連接。而 Java 社區(qū)長(zhǎng)連接框架首選 Netty,這里我們選用螞蟻金服網(wǎng)絡(luò)通信框架 SOFA-Bolt(基于 Netty),便于快速開(kāi)發(fā)。

日志模塊,Raft 算法中,日志實(shí)現(xiàn)是基礎(chǔ),考慮到時(shí)間因素,我們選用 RocksDB 作為日志存儲(chǔ)。

狀態(tài)機(jī),可以是任何實(shí)現(xiàn),其實(shí)質(zhì)就是將日志中的內(nèi)容進(jìn)行處理??梢岳斫鉃?Mysql binlog 中的具體數(shù)據(jù)。由于我們是要實(shí)現(xiàn)一個(gè) KV 存儲(chǔ),那么可以直接使用日志模塊的 RocksDB 組件。

以上。我們可以看到,得益于開(kāi)源世界,我們開(kāi)發(fā)一個(gè) Raft 存儲(chǔ),只需要編寫一個(gè)“一致性模塊”就行了,其他模塊都有現(xiàn)成的輪子可以使用,真是美滋滋。

接口設(shè)計(jì):

上面我們說(shuō)了 Raft 的幾個(gè)核心功能,事實(shí)上,就可以理解為接口。所以我們定義以下幾個(gè)接口:

Consensus, 一致性模塊接口

LogModule,日志模塊接口

StateMachine, 狀態(tài)機(jī)接口

RpcServer & RpcClient, RPC 接口

Node,同時(shí),為了聚合上面的幾個(gè)接口,我們需要定義一個(gè) Node 接口,即節(jié)點(diǎn),Raft 抽象的機(jī)器節(jié)點(diǎn)。

LifeCycle, 最后,我們需要管理以上組件的生命周期,因此需要一個(gè) LifeCycle 接口。

接下來(lái),我們需要詳細(xì)定義核心接口 Consensus。我們根據(jù)論文定義了 2 個(gè)核心接口:

   /**
     * 請(qǐng)求投票 RPC
     *
     * 接收者實(shí)現(xiàn):
     *
     *      如果term < currentTerm返回 false (5.2 節(jié))
     *      如果 votedFor 為空或者就是 candidateId,并且候選人的日志至少和自己一樣新,那么就投票給他(5.2 節(jié),5.4 節(jié))
     */
    RvoteResult requestVote(RvoteParam param);

    /**
     * 附加日志(多個(gè)日志,為了提高效率) RPC
     *
     * 接收者實(shí)現(xiàn):
     *
     *    如果 term < currentTerm 就返回 false (5.1 節(jié))
     *    如果日志在 prevLogIndex 位置處的日志條目的任期號(hào)和 prevLogTerm 不匹配,則返回 false (5.3 節(jié))
     *    如果已經(jīng)存在的日志條目和新的產(chǎn)生沖突(索引值相同但是任期號(hào)不同),刪除這一條和之后所有的 (5.3 節(jié))
     *    附加任何在已有的日志中不存在的條目
     *    如果 leaderCommit > commitIndex,令 commitIndex 等于 leaderCommit 和 新日志條目索引值中較小的一個(gè)
     */
    AentryResult appendEntries(AentryParam param);

請(qǐng)求投票 & 附加日志。也就是我們的 Raft 節(jié)點(diǎn)的核心功能,leader 選舉和 日志復(fù)制。實(shí)現(xiàn)這兩個(gè)接口是 Raft 的關(guān)鍵所在。

然后再看 LogModule 接口,這個(gè)自由發(fā)揮,考慮日志的特點(diǎn),我定義了以下幾個(gè)接口:

void write(LogEntry logEntry);

LogEntry read(Long index);

void removeOnStartIndex(Long startIndex);

LogEntry getLast();

Long getLastIndex();

分別是寫,讀,刪,最后是兩個(gè)關(guān)于 Last 的接口,在 Raft 中,Last 是一個(gè)非常關(guān)鍵的東西,因此我這里多帶帶定義了 2個(gè)方法,雖然看起來(lái)不是很好看 :)

狀態(tài)機(jī)接口,在 Raft 論文中,將數(shù)據(jù)保存到狀態(tài)機(jī),作者稱之為應(yīng)用,那么我們也這么命名,說(shuō)白了,就是將已成功提交的日志應(yīng)用到狀態(tài)機(jī)中:

    /**
     * 將數(shù)據(jù)應(yīng)用到狀態(tài)機(jī).
     *
     * 原則上,只需這一個(gè)方法(apply). 其他的方法是為了更方便的使用狀態(tài)機(jī).
     * @param logEntry 日志中的數(shù)據(jù).
     */
    void apply(LogEntry logEntry);

    LogEntry get(String key);

    String getString(String key);

    void setString(String key, String value);

    void delString(String... key);
    

第一個(gè) apply 方法,就是 Raft 論文常常提及的方法,即將日志應(yīng)用到狀態(tài)機(jī)中,后面的幾個(gè)方法,都是我為了方便獲取數(shù)據(jù)設(shè)計(jì)的,可以不用在意,甚至于,這幾個(gè)方法不存在也不影響 Raft 的實(shí)現(xiàn),但影響 KV 存儲(chǔ)的實(shí)現(xiàn),試想:一個(gè)系統(tǒng)只有保存功能,沒(méi)有獲取功能,要你何用?。

RpcClient 和 RPCServer 沒(méi)什么好講的,其實(shí)就是 send 和 receive。

然后是 Node 接口,Node 接口也是 Raft 沒(méi)有定義的,我們依靠自己的理解定義了幾個(gè)接口:

    /**
     * 設(shè)置配置文件.
     *
     * @param config
     */
    void setConfig(NodeConfig config);

    /**
     * 處理請(qǐng)求投票 RPC.
     *
     * @param param
     * @return
     */
    RvoteResult handlerRequestVote(RvoteParam param);

    /**
     * 處理附加日志請(qǐng)求.
     *
     * @param param
     * @return
     */
    AentryResult handlerAppendEntries(AentryParam param);

    /**
     * 處理客戶端請(qǐng)求.
     *
     * @param request
     * @return
     */
    ClientKVAck handlerClientRequest(ClientKVReq request);

    /**
     * 轉(zhuǎn)發(fā)給 leader 節(jié)點(diǎn).
     * @param request
     * @return
     */
    ClientKVAck redirect(ClientKVReq request);

首先,一個(gè) Node 肯定需要配置文件,所以有一個(gè) setConfig 接口,
然后,肯定需要處理“請(qǐng)求投票”和“附加日志”,同時(shí),還需要接收用戶,也就是客戶端的請(qǐng)求(不然數(shù)據(jù)從哪來(lái)?),所以有 handlerClientRequest 接口,最后,考慮到靈活性,我們讓每個(gè)節(jié)點(diǎn)都可以接收客戶端的請(qǐng)求,但 follower 節(jié)點(diǎn)并不能處理請(qǐng)求,所以需要重定向到 leader 節(jié)點(diǎn),因此,我們需要一個(gè)重定向接口。

最后是生命周期接口,這里我們簡(jiǎn)單定義了 2 個(gè),有需要的話,再另外加上組合接口:

    void init() throws Throwable;

    void destroy() throws Throwable;

好,基本的接口定義完了,后面就是實(shí)現(xiàn)了。實(shí)現(xiàn)才是關(guān)鍵。

Leader 選舉的實(shí)現(xiàn)

選舉,其實(shí)就是一個(gè)定時(shí)器,根據(jù) Raft 論文描述,如果超時(shí)了就需要重新選舉,我們使用 Java 的定時(shí)任務(wù)線程池進(jìn)行實(shí)現(xiàn),實(shí)現(xiàn)之前,需要確定幾個(gè)點(diǎn):

選舉者必須不是 leader。

必須超時(shí)了才能選舉,具體超時(shí)時(shí)間根據(jù)你的設(shè)計(jì)而定,注意,每個(gè)節(jié)點(diǎn)的超時(shí)時(shí)間不能相同,應(yīng)當(dāng)使用隨機(jī)算法錯(cuò)開(kāi)(Raft 關(guān)鍵實(shí)現(xiàn)),避免無(wú)謂的死鎖。

選舉者優(yōu)先選舉自己,將自己變成 candidate。

選舉的第一步就是把自己的 term 加一。

然后像其他節(jié)點(diǎn)發(fā)送請(qǐng)求投票 RPC,請(qǐng)求參數(shù)參照論文,包括自身的 term,自身的 lastIndex,以及日志的 lastTerm。同時(shí),請(qǐng)求投票 RPC 應(yīng)該是并行請(qǐng)求的。

等待投票結(jié)果應(yīng)該有超時(shí)控制,如果超時(shí)了,就不等待了。

最后,如果有超過(guò)半數(shù)的響應(yīng)為 success,那么就需要立即變成 leader ,并發(fā)送心跳阻止其他選舉。

如果失敗了,就需要重新選舉。注意,這個(gè)期間,如果有其他節(jié)點(diǎn)發(fā)送心跳,也需要立刻變成 follower,否則,將死循環(huán)。

具體代碼,可參見(jiàn) https://github.com/stateIs0/l...

上面說(shuō)的,其實(shí)是 Leader 選舉中,請(qǐng)求者的實(shí)現(xiàn),那么接收者如何實(shí)現(xiàn)呢?接收者在收到“請(qǐng)求投票” RPC 后,需要做以下事情:

注意,選舉操作應(yīng)該是串行的,因?yàn)樯婕暗綘顟B(tài)修改,并發(fā)操作將導(dǎo)致數(shù)據(jù)錯(cuò)亂。也就是說(shuō),如果搶鎖失敗,應(yīng)當(dāng)立即返回錯(cuò)誤。

首先判斷對(duì)方的 term 是否小于自己,如果小于自己,直接返回失敗。

如果當(dāng)前節(jié)點(diǎn)沒(méi)有投票給任何人,或者投的正好是對(duì)方,那么就可以比較日志的大小,反之,返回失敗。

如果對(duì)方日志沒(méi)有自己大,返回失敗。反之,投票給對(duì)方,并變成 follower。變成 follower 的同時(shí),異步的選舉任務(wù)在最后從 condidate 變成 leader 之前,會(huì)判斷是否是 follower,如果是 follower,就放棄成為 leader。這是一個(gè)兜底的措施。

具體代碼參見(jiàn) https://github.com/stateIs0/l...

到這里,基本就能夠?qū)崿F(xiàn) Raft Leader 選舉的邏輯。

注意,我們上面涉及到的 LastIndex 等參數(shù),還沒(méi)有實(shí)現(xiàn),但不影響我們編寫偽代碼,畢竟日志復(fù)制比 leader 選舉要復(fù)雜的多,我們的原則是從易到難。:)

日志復(fù)制的實(shí)現(xiàn)

日志復(fù)制是 Raft 實(shí)現(xiàn)一致性的核心。

日志復(fù)制有 2 種形式,1種是心跳,一種是真正的日志,心跳的日志內(nèi)容是空的,其他部分基本相同,也就是說(shuō),接收方在收到日志時(shí),如果發(fā)現(xiàn)是空的,那么他就是心跳。

心跳

既然是心跳,肯定就是個(gè)定時(shí)任務(wù),和選舉一樣。在我們的實(shí)現(xiàn)中,我們每 5 秒發(fā)送一次心跳。注意點(diǎn):

首先自己必須是 leader 才能發(fā)送心跳。

必須滿足 5 秒的時(shí)間間隔。

并發(fā)的向其他 follower 節(jié)點(diǎn)發(fā)送心跳。

心跳參數(shù)包括自身的 ID,自身的 term,以便讓對(duì)方檢查 term,防止網(wǎng)絡(luò)分區(qū)導(dǎo)致的腦裂。

如果任意 follower 的返回值的 term 大于自身,說(shuō)明自己分區(qū)了,那么需要變成 follower,并更新自己的 term。然后重新發(fā)起選舉。

具體代碼查看:https://github.com/stateIs0/l...

然后是心跳接收者的實(shí)現(xiàn),這個(gè)就比較簡(jiǎn)單了,接收者需要做幾件事情:

無(wú)論成功失敗首先設(shè)置返回值,也就是將自己的 term 返回給 leader。

判斷對(duì)方的 term 是否大于自身,如果大于自身,變成 follower,防止異步的選舉任務(wù)誤操作。同時(shí)更新選舉時(shí)間和心跳時(shí)間。

如果對(duì)方 term 小于自身,返回失敗。不更新選舉時(shí)間和心跳時(shí)間。以便觸發(fā)選舉。

具體代碼參見(jiàn):https://github.com/stateIs0/l...

說(shuō)完了心跳,再說(shuō)說(shuō)真正的日志附加。

簡(jiǎn)單來(lái)說(shuō),當(dāng)用戶向 Leader 發(fā)送一個(gè) KV 數(shù)據(jù),那么 Leader 需要將 KV數(shù)據(jù)封裝成日志,并行的發(fā)送到其他的 follower 節(jié)點(diǎn),只要在指定的超時(shí)時(shí)間內(nèi),有過(guò)半幾點(diǎn)返回成功,那么久提交(持久化)這條日志,返回客戶端成功,否者返回失敗。

因此,Leader 節(jié)點(diǎn)會(huì)有一個(gè) ClientKVAck handlerClientRequest(ClientKVReq request) 接口,用于接收用戶的 KV 數(shù)據(jù),同時(shí),會(huì)并行向其他節(jié)點(diǎn)復(fù)制數(shù)據(jù),具體步驟如下:

每個(gè)節(jié)點(diǎn)都可能會(huì)接收到客戶端的請(qǐng)求,但只有 leader 能處理,所以如果自身不是 leader,則需要轉(zhuǎn)發(fā)給 leader。

然后將用戶的 KV 數(shù)據(jù)封裝成日志結(jié)構(gòu),包括 term,index,command,預(yù)提交到本地。

并行的向其他節(jié)點(diǎn)發(fā)送數(shù)據(jù),也就是日志復(fù)制。

如果在指定的時(shí)間內(nèi),過(guò)半節(jié)點(diǎn)返回成功,那么就提交這條日志。

最后,更新自己的 commitIndex,lastApplied 等信息。

注意,復(fù)制不僅僅是簡(jiǎn)單的將這條日志發(fā)送到其他節(jié)點(diǎn),這可能比我們想象的復(fù)雜,為了保證復(fù)雜網(wǎng)絡(luò)環(huán)境下的一致性,Raft 保存了每個(gè)節(jié)點(diǎn)的成功復(fù)制過(guò)的日志的 index,即 nextIndex ,因此,如果對(duì)方之前一段時(shí)間宕機(jī)了,那么,從宕機(jī)那一刻開(kāi)始,到當(dāng)前這段時(shí)間的所有日志,都要發(fā)送給對(duì)方。

甚至于,如果對(duì)方覺(jué)得你發(fā)送的日志還是太大,那么就要遞減的減小 nextIndex,復(fù)制更多的日志給對(duì)方。注意:這里是 Raft 實(shí)現(xiàn)分布式一致性的關(guān)鍵所在。

具體代碼參見(jiàn):https://github.com/stateIs0/l...

再來(lái)看看日志接收者的實(shí)現(xiàn)步驟:

和心跳一樣,要先檢查對(duì)方 term,如果 term 都不對(duì),那么就沒(méi)什么好說(shuō)的了。

如果日志不匹配,那么返回 leader,告訴他,減小 nextIndex 重試。

如果本地存在的日志和 leader 的日志沖突了,以 leader 的為準(zhǔn),刪除自身的。

最后,將日志應(yīng)用到狀態(tài)機(jī),更新本地的 commitIndex,返回 leader 成功。

具體代碼參見(jiàn):https://github.com/stateIs0/l...

到這里,日志復(fù)制的部分就講完了。

注意,實(shí)現(xiàn)日志復(fù)制的前提是,必須有一個(gè)正確的日志存儲(chǔ)系統(tǒng),即我們的 RocksDB,我們?cè)?RocksDB 的基礎(chǔ)上,使用一種機(jī)制,維護(hù)了 每個(gè)節(jié)點(diǎn) 的LastIndex,無(wú)論何時(shí)何地,都能夠得到正確的 LastIndex,這是實(shí)現(xiàn)日志復(fù)制不可獲取的一部分。

驗(yàn)證“Leader 選舉”和“日志復(fù)制”

寫完了程序,如何驗(yàn)證是否正確呢?

當(dāng)然是寫驗(yàn)證程序。

我們首先驗(yàn)證 “Leader 選舉”。其實(shí)這個(gè)比較好測(cè)試。

在 idea 中配置 5 個(gè) application 啟動(dòng)項(xiàng),配置 main 類為 RaftNodeBootStrap 類, 加入 -DserverPort=8775 -DserverPort=8776 -DserverPort=8777 -DserverPort=8778 -DserverPort=8779
系統(tǒng)配置, 表示分布式環(huán)境下的 5 個(gè)機(jī)器節(jié)點(diǎn).

依次啟動(dòng) 5 個(gè) RaftNodeBootStrap 節(jié)點(diǎn), 端口分別是 8775,8776, 8777, 8778, 8779.

觀察控制臺(tái), 約 6 秒后, 會(huì)發(fā)生選舉事件,此時(shí),會(huì)產(chǎn)生一個(gè) leader. 而 leader 會(huì)立刻發(fā)送心跳維持自己的地位.

如果leader 的端口是 8775, 使用 idea 關(guān)閉 8775 端口,模擬節(jié)點(diǎn)掛掉, 大約 15 秒后, 會(huì)重新開(kāi)始選舉, 并且會(huì)在剩余的 4 個(gè)節(jié)點(diǎn)中,產(chǎn)生一個(gè)新的 leader. 并開(kāi)始發(fā)送心跳日志。

然后驗(yàn)證 日志復(fù)制,分為 2 種情況:

正常狀態(tài)下

在 idea 中配置 5 個(gè) application 啟動(dòng)項(xiàng),配置 main 類為 RaftNodeBootStrap 類, 加入 -DserverPort=8775 -DserverPort=8776 -DserverPort=8777 -DserverPort=8778 -DserverPort=8779

依次啟動(dòng) 5 個(gè) RaftNodeBootStrap 節(jié)點(diǎn), 端口分別是 8775,8776, 8777, 8778, 8779.

使用客戶端寫入 kv 數(shù)據(jù).

殺掉所有節(jié)點(diǎn), 使用 junit test 讀取每個(gè) rocksDB 的值, 驗(yàn)證每個(gè)節(jié)點(diǎn)的數(shù)據(jù)是否一致.

非正常狀態(tài)下

在 idea 中配置 5 個(gè) application 啟動(dòng)項(xiàng),配置 main 類為 RaftNodeBootStrap 類, 加入 -DserverPort=8775 -DserverPort=8776 -DserverPort=8777 -DserverPort=8778 -DserverPort=8779

依次啟動(dòng) 5 個(gè) RaftNodeBootStrap 節(jié)點(diǎn), 端口分別是 8775,8776, 8777, 8778, 8779.

使用客戶端寫入 kv 數(shù)據(jù).

殺掉 leader (假設(shè)是 8775).

再次寫入數(shù)據(jù).

重啟 8775.

關(guān)閉所有節(jié)點(diǎn), 讀取 RocksDB 驗(yàn)證數(shù)據(jù)一致性.

Summary

本文并沒(méi)有貼很多代碼,如果要貼代碼的話,閱讀體驗(yàn)將不會(huì)很好,并且代碼也不能說(shuō)明什么,如果想看具體實(shí)現(xiàn),可以到 github 上看看,順便給個(gè) star :)

該項(xiàng)目 Java 代碼約 2500 行,核心代碼估計(jì)也就 1000 多行。你甚至可以說(shuō),這是個(gè)玩具代碼,但我相信畢玄大師所說(shuō),玩具代碼經(jīng)過(guò)優(yōu)化后,也是可以變成可在商業(yè)系統(tǒng)中真正健壯運(yùn)行的代碼(http://hellojava.info/?p=508) :)

回到我們的初衷,我們并不奢望這段代碼能夠運(yùn)行在生產(chǎn)環(huán)境中,就像我的另一個(gè)項(xiàng)目 Lu-RPC 一樣。但,經(jīng)歷了一次編寫可正確運(yùn)行的玩具代碼的經(jīng)歷,下次再次編寫工程化的代碼,應(yīng)該會(huì)更加容易些。這點(diǎn)我深有體會(huì)。

可以稍微展開(kāi)講一下,在寫完 Lu-RPC 項(xiàng)目后,我就接到了開(kāi)發(fā)生產(chǎn)環(huán)境運(yùn)行的限流熔斷框架任務(wù),此時(shí),開(kāi)發(fā) Lu-RPC 的經(jīng)歷讓我在開(kāi)發(fā)該框架時(shí),更加的從容和自如:)

再回到 Raft 上面來(lái),雖然上面的測(cè)試用例跑過(guò)了,程序也經(jīng)過(guò)了我反反復(fù)復(fù)的測(cè)試,但不代表這個(gè)程序就是 100% 正確的,特別是在復(fù)雜的分布式環(huán)境下。如果你對(duì) Raft 有興趣,歡迎一起交流溝通 :)

項(xiàng)目地址:https://github.com/stateIs0/l...

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/72960.html

相關(guān)文章

  • Spring Cloud Consul 之Greenwich本全攻略

    摘要:在我們的文檔中,我們使用來(lái)表明就選舉和事務(wù)的順序達(dá)成一致。提供成員關(guān)系,故障檢測(cè)和事件廣播。這是一個(gè)允許請(qǐng)求的請(qǐng)求響應(yīng)機(jī)制。這包括服務(wù)發(fā)現(xiàn),還包括豐富的運(yùn)行狀況檢查,鎖定,鍵值,多數(shù)據(jù)中心聯(lián)合,事件系統(tǒng)和。 轉(zhuǎn)載請(qǐng)標(biāo)明出處: http://blog.csdn.net/forezp/a...本文出自方志朋的博客 什么是Consul Consul是HashiCorp公司推出的開(kāi)源軟件,使...

    qingshanli1988 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

閱讀需要支付1元查看
<