編寫你的第一個 Java 版 Raft 分布式 KV 存儲

前言

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

項目地址:https://github.com/stateIs0/lu-raft-kv

歡迎 star :)

什么是 Java 版 Raft 分布式 KV 存儲

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

同時裆针,他們的設(shè)計目標(biāo)大部分都是命名服務(wù),即服務(wù)注冊發(fā)現(xiàn)寺晌,也就是說世吨,他們通常都是基于 AP 實現(xiàn),就像 DNS呻征,DNS 是一個命名服務(wù)耘婚,同時也不是一個強(qiáng)一致性的服務(wù)。

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

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

回到我們這邊思杯。

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

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

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

通常有 2 種回答:

  1. 如果是無狀態(tài)服務(wù)局蚀,那么毫不影響使用麦锯。
  2. 如果是有狀態(tài)服務(wù),可以將狀態(tài)保存到一個別的地方琅绅,例如 Redis扶欣。如果 Redis 掛了怎么辦?那就放到 ZK千扶。

很多中間件料祠,都會使用 ZK 來保證狀態(tài)一致,例如 codis澎羞,kafka髓绽。因為使用 ZK 能夠幫我們節(jié)省大量的時間。但有的時候煤痕,中間件的用戶覺得引入第三方中間件很麻煩梧宫,那么中間件開發(fā)者會嘗試自己實現(xiàn)一致性,例如 Redis Cluster摆碉, TiDB 等塘匣。

而通常自己實現(xiàn),都會使用 Raft 算法巷帝,那有人問忌卤,為什么不使用"更牛逼的" paxos 算法?對不起楞泼,這個有點難驰徊,至少目前開源的、生產(chǎn)環(huán)境大規(guī)模使用的 paxos 算法實現(xiàn)還沒有出現(xiàn)堕阔,只聽過 Google 或者 alibaba 在其內(nèi)部實現(xiàn)過棍厂,具體是什么樣子的,這里我們就不討論了超陆。

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

  1. 有的時候 ZK 和 etcd 并不能解決我們的問題,或者像上面說的张漂,引入其他的中間件部署起來太麻煩也太重晶默。
  2. 完全處于好奇,好奇為什么 Raft 可以保證一致性(這通澈皆埽可以通過汗牛充棟的文章來得到解答)磺陡?但是到底該怎么實現(xiàn)?
  3. 分布式開發(fā)的要求漠畜,作為開發(fā)分布式系統(tǒng)的程序員币他,如果能夠更深刻的理解分布式系統(tǒng)的核心算法,那么對如何合理設(shè)計一個分布式系統(tǒng)將大有益處盆驹。

好圆丹,有了以上 3 個原因,我們就有足夠的動力來造輪子了躯喇,接下來就是如何造的問題了。

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

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

Raft 為了算法的可理解性正压,將算法分成了 4 個部分。

  1. leader 選舉
  2. 日志復(fù)制
  3. 成員變更
  4. 日志壓縮

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

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

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

Raft leader 節(jié)點會將客戶端的請求都封裝成日志假瞬,發(fā)送到各個 follower 中,如果集群中超過一半的 follower 回復(fù)成功,那么這個日志就可以被提交(commit)笨触,這個 commit 可以理解為 ACID 的 D 懦傍,即持久化。當(dāng)日志被持久化到磁盤芦劣,后面的事情就好辦了粗俱。

而第三點則是為了節(jié)點的擴(kuò)展性。第四點是為了性能虚吟。相比較 leader 選舉和 日志復(fù)制寸认,不是那么的重要,可以說串慰,如果沒有成員變更和日志壓縮偏塞,也可以搞出一個可用的 Raft 分布式系統(tǒng),但沒有 leader 選舉和日志復(fù)制邦鲫,是萬萬不能的灸叼。

因此,本文和本項目將重點放在 leader 選舉和日志復(fù)制庆捺。

以上古今,就簡單說明了 Raft 的算法,關(guān)于 Raft 算法更多的文章滔以,請參考本人博客中的其他文章(包含官方各個版本論文和 PPT & 動畫 & 其他博客文章)捉腥,博客地址:thinkinjava.cn

實現(xiàn)的步驟

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

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

技術(shù)選型:

  • 一致性模塊,是 Raft 算法的核心實現(xiàn)剥槐,通過一致性模塊唱歧,保證 Raft 集群節(jié)點數(shù)據(jù)的一致性。這里我們需要自己根據(jù)論文描述去實現(xiàn)粒竖。
  • RPC 通信颅崩,可以使用 HTTP 短連接,也可以直接使用 TCP 長連接蕊苗,考慮到集群各個節(jié)點頻繁通信沿后,同時節(jié)點通常都在一個局域網(wǎng)內(nèi),因此我們選用 TCP 長連接朽砰。而 Java 社區(qū)長連接框架首選 Netty尖滚,這里我們選用螞蟻金服網(wǎng)絡(luò)通信框架 SOFA-Bolt(基于 Netty)喉刘,便于快速開發(fā)。
  • 日志模塊漆弄,Raft 算法中睦裳,日志實現(xiàn)是基礎(chǔ),考慮到時間因素撼唾,我們選用 RocksDB 作為日志存儲廉邑。
  • 狀態(tài)機(jī),可以是任何實現(xiàn)倒谷,其實質(zhì)就是將日志中的內(nèi)容進(jìn)行處理蛛蒙。可以理解為 Mysql binlog 中的具體數(shù)據(jù)渤愁。由于我們是要實現(xiàn)一個 KV 存儲牵祟,那么可以直接使用日志模塊的 RocksDB 組件。

以上抖格。我們可以看到诺苹,得益于開源世界,我們開發(fā)一個 Raft 存儲雹拄,只需要編寫一個“一致性模塊”就行了筝尾,其他模塊都有現(xiàn)成的輪子可以使用,真是美滋滋办桨。

接口設(shè)計:

上面我們說了 Raft 的幾個核心功能,事實上站辉,就可以理解為接口呢撞。所以我們定義以下幾個接口:

  1. Consensus, 一致性模塊接口
  2. LogModule饰剥,日志模塊接口
  3. StateMachine殊霞, 狀態(tài)機(jī)接口
  4. RpcServer & RpcClient, RPC 接口
  5. Node汰蓉,同時绷蹲,為了聚合上面的幾個接口,我們需要定義一個 Node 接口顾孽,即節(jié)點祝钢,Raft 抽象的機(jī)器節(jié)點。
  6. LifeCycle若厚, 最后拦英,我們需要管理以上組件的生命周期,因此需要一個 LifeCycle 接口测秸。

接下來疤估,我們需要詳細(xì)定義核心接口 Consensus灾常。我們根據(jù)論文定義了 2 個核心接口:

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

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

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

然后再看 LogModule 接口吧碾,這個自由發(fā)揮凰盔,考慮日志的特點,我定義了以下幾個接口:

void write(LogEntry logEntry);

LogEntry read(Long index);

void removeOnStartIndex(Long startIndex);

LogEntry getLast();

Long getLastIndex();

分別是寫倦春,讀户敬,刪,最后是兩個關(guān)于 Last 的接口睁本,在 Raft 中尿庐,Last 是一個非常關(guān)鍵的東西,因此我這里單獨定義了 2個方法呢堰,雖然看起來不是很好看 :)

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

    /**
     * 將數(shù)據(jù)應(yīng)用到狀態(tài)機(jī).
     *
     * 原則上,只需這一個方法(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);
    

第一個 apply 方法惹资,就是 Raft 論文常常提及的方法,即將日志應(yīng)用到狀態(tài)機(jī)中航闺,后面的幾個方法褪测,都是我為了方便獲取數(shù)據(jù)設(shè)計的,可以不用在意潦刃,甚至于侮措,這幾個方法不存在也不影響 Raft 的實現(xiàn),但影響 KV 存儲的實現(xiàn)福铅,試想:一個系統(tǒng)只有保存功能萝毛,沒有獲取功能,要你何用滑黔?笆包。

RpcClient 和 RPCServer 沒什么好講的环揽,其實就是 send 和 receive。

然后是 Node 接口庵佣,Node 接口也是 Raft 沒有定義的歉胶,我們依靠自己的理解定義了幾個接口:


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

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

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

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

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

首先,一個 Node 肯定需要配置文件巴粪,所以有一個 setConfig 接口通今,
然后,肯定需要處理“請求投票”和“附加日志”肛根,同時辫塌,還需要接收用戶,也就是客戶端的請求(不然數(shù)據(jù)從哪來派哲?)臼氨,所以有 handlerClientRequest 接口,最后芭届,考慮到靈活性储矩,我們讓每個節(jié)點都可以接收客戶端的請求,但 follower 節(jié)點并不能處理請求褂乍,所以需要重定向到 leader 節(jié)點持隧,因此,我們需要一個重定向接口逃片。

最后是生命周期接口屡拨,這里我們簡單定義了 2 個,有需要的話褥实,再另外加上組合接口:

    void init() throws Throwable;

    void destroy() throws Throwable;

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

Leader 選舉的實現(xiàn)

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

  1. 選舉者必須不是 leader绷跑。
  2. 必須超時了才能選舉拳恋,具體超時時間根據(jù)你的設(shè)計而定,注意,每個節(jié)點的超時時間不能相同砸捏,應(yīng)當(dāng)使用隨機(jī)算法錯開(Raft 關(guān)鍵實現(xiàn))谬运,避免無謂的死鎖隙赁。
  3. 選舉者優(yōu)先選舉自己,將自己變成 candidate。
  4. 選舉的第一步就是把自己的 term 加一梆暖。
  5. 然后像其他節(jié)點發(fā)送請求投票 RPC伞访,請求參數(shù)參照論文,包括自身的 term轰驳,自身的 lastIndex厚掷,以及日志的 lastTerm。同時级解,請求投票 RPC 應(yīng)該是并行請求的冒黑。
  6. 等待投票結(jié)果應(yīng)該有超時控制,如果超時了勤哗,就不等待了抡爹。
  7. 最后,如果有超過半數(shù)的響應(yīng)為 success俺陋,那么就需要立即變成 leader 豁延,并發(fā)送心跳阻止其他選舉。
  8. 如果失敗了腊状,就需要重新選舉诱咏。注意,這個期間缴挖,如果有其他節(jié)點發(fā)送心跳袋狞,也需要立刻變成 follower,否則映屋,將死循環(huán)苟鸯。

具體代碼,可參見 https://github.com/stateIs0/lu-raft-kv/blob/master/lu-raft-kv/src/main/java/cn/think/in/java/impl/DefaultNode.java#L546

上面說的棚点,其實是 Leader 選舉中早处,請求者的實現(xiàn),那么接收者如何實現(xiàn)呢瘫析?接收者在收到“請求投票” RPC 后砌梆,需要做以下事情:

  1. 注意,選舉操作應(yīng)該是串行的贬循,因為涉及到狀態(tài)修改咸包,并發(fā)操作將導(dǎo)致數(shù)據(jù)錯亂。也就是說杖虾,如果搶鎖失敗烂瘫,應(yīng)當(dāng)立即返回錯誤。
  2. 首先判斷對方的 term 是否小于自己奇适,如果小于自己坟比,直接返回失敗芦鳍。
  3. 如果當(dāng)前節(jié)點沒有投票給任何人,或者投的正好是對方温算,那么就可以比較日志的大小怜校,反之,返回失敗注竿。
  4. 如果對方日志沒有自己大茄茁,返回失敗。反之巩割,投票給對方裙顽,并變成 follower。變成 follower 的同時宣谈,異步的選舉任務(wù)在最后從 condidate 變成 leader 之前愈犹,會判斷是否是 follower,如果是 follower闻丑,就放棄成為 leader漩怎。這是一個兜底的措施。

具體代碼參見 https://github.com/stateIs0/lu-raft-kv/blob/master/lu-raft-kv/src/main/java/cn/think/in/java/impl/DefaultConsensus.java#L51

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

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

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

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

日志復(fù)制有 2 種形式胎署,1種是心跳吆录,一種是真正的日志,心跳的日志內(nèi)容是空的琼牧,其他部分基本相同径筏,也就是說,接收方在收到日志時障陶,如果發(fā)現(xiàn)是空的,那么他就是心跳聊训。

心跳

既然是心跳抱究,肯定就是個定時任務(wù),和選舉一樣带斑。在我們的實現(xiàn)中鼓寺,我們每 5 秒發(fā)送一次心跳勋拟。注意點:

  1. 首先自己必須是 leader 才能發(fā)送心跳。
  2. 必須滿足 5 秒的時間間隔妈候。
  3. 并發(fā)的向其他 follower 節(jié)點發(fā)送心跳敢靡。
  4. 心跳參數(shù)包括自身的 ID,自身的 term苦银,以便讓對方檢查 term啸胧,防止網(wǎng)絡(luò)分區(qū)導(dǎo)致的腦裂。
  5. 如果任意 follower 的返回值的 term 大于自身幔虏,說明自己分區(qū)了纺念,那么需要變成 follower,并更新自己的 term想括。然后重新發(fā)起選舉陷谱。

具體代碼查看:https://github.com/stateIs0/lu-raft-kv/blob/master/lu-raft-kv/src/main/java/cn/think/in/java/impl/DefaultNode.java#L695

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

  1. 無論成功失敗首先設(shè)置返回值逸邦,也就是將自己的 term 返回給 leader衷旅。
  2. 判斷對方的 term 是否大于自身,如果大于自身宪躯,變成 follower,防止異步的選舉任務(wù)誤操作夷都。同時更新選舉時間和心跳時間眷唉。
  3. 如果對方 term 小于自身,返回失敗囤官。不更新選舉時間和心跳時間冬阳。以便觸發(fā)選舉。

具體代碼參見:https://github.com/stateIs0/lu-raft-kv/blob/master/lu-raft-kv/src/main/java/cn/think/in/java/impl/DefaultConsensus.java#L109

說完了心跳党饮,再說說真正的日志附加肝陪。

簡單來說,當(dāng)用戶向 Leader 發(fā)送一個 KV 數(shù)據(jù)刑顺,那么 Leader 需要將 KV數(shù)據(jù)封裝成日志氯窍,并行的發(fā)送到其他的 follower 節(jié)點,只要在指定的超時時間內(nèi)蹲堂,有過半幾點返回成功狼讨,那么久提交(持久化)這條日志,返回客戶端成功柒竞,否者返回失敗政供。

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

  1. 每個節(jié)點都可能會接收到客戶端的請求衅檀,但只有 leader 能處理招刨,所以如果自身不是 leader,則需要轉(zhuǎn)發(fā)給 leader哀军。
  2. 然后將用戶的 KV 數(shù)據(jù)封裝成日志結(jié)構(gòu)沉眶,包括 term,index排苍,command沦寂,預(yù)提交到本地。
  3. 并行的向其他節(jié)點發(fā)送數(shù)據(jù)淘衙,也就是日志復(fù)制传藏。
  4. 如果在指定的時間內(nèi),過半節(jié)點返回成功彤守,那么就提交這條日志毯侦。
  5. 最后,更新自己的 commitIndex具垫,lastApplied 等信息侈离。

注意,復(fù)制不僅僅是簡單的將這條日志發(fā)送到其他節(jié)點筝蚕,這可能比我們想象的復(fù)雜卦碾,為了保證復(fù)雜網(wǎng)絡(luò)環(huán)境下的一致性,Raft 保存了每個節(jié)點的成功復(fù)制過的日志的 index起宽,即 nextIndex 洲胖,因此,如果對方之前一段時間宕機(jī)了坯沪,那么绿映,從宕機(jī)那一刻開始,到當(dāng)前這段時間的所有日志腐晾,都要發(fā)送給對方叉弦。

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

具體代碼參見:https://github.com/stateIs0/lu-raft-kv/blob/master/lu-raft-kv/src/main/java/cn/think/in/java/impl/DefaultNode.java#L244

再來看看日志接收者的實現(xiàn)步驟:

  1. 和心跳一樣樱拴,要先檢查對方 term凝颇,如果 term 都不對,那么就沒什么好說的了疹鳄。
  2. 如果日志不匹配,那么返回 leader芦岂,告訴他瘪弓,減小 nextIndex 重試。
  3. 如果本地存在的日志和 leader 的日志沖突了禽最,以 leader 的為準(zhǔn)腺怯,刪除自身的。
  4. 最后川无,將日志應(yīng)用到狀態(tài)機(jī)呛占,更新本地的 commitIndex,返回 leader 成功懦趋。

具體代碼參見:https://github.com/stateIs0/lu-raft-kv/blob/master/lu-raft-kv/src/main/java/cn/think/in/java/impl/DefaultConsensus.java#L109

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

注意仅叫,實現(xiàn)日志復(fù)制的前提是帜篇,必須有一個正確的日志存儲系統(tǒng),即我們的 RocksDB诫咱,我們在 RocksDB 的基礎(chǔ)上笙隙,使用一種機(jī)制,維護(hù)了 每個節(jié)點 的LastIndex坎缭,無論何時何地竟痰,都能夠得到正確的 LastIndex,這是實現(xiàn)日志復(fù)制不可獲取的一部分掏呼。

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

寫完了程序坏快,如何驗證是否正確呢?

當(dāng)然是寫驗證程序哄尔。

我們首先驗證 “Leader 選舉”假消。其實這個比較好測試。
  1. 在 idea 中配置 5 個 application 啟動項,配置 main 類為 RaftNodeBootStrap 類, 加入 -DserverPort=8775 -DserverPort=8776 -DserverPort=8777 -DserverPort=8778 -DserverPort=8779
    系統(tǒng)配置, 表示分布式環(huán)境下的 5 個機(jī)器節(jié)點.
  2. 依次啟動 5 個 RaftNodeBootStrap 節(jié)點, 端口分別是 8775岭接,8776富拗, 8777, 8778, 8779.
  3. 觀察控制臺, 約 6 秒后, 會發(fā)生選舉事件,此時,會產(chǎn)生一個 leader. 而 leader 會立刻發(fā)送心跳維持自己的地位.
  4. 如果leader 的端口是 8775, 使用 idea 關(guān)閉 8775 端口,模擬節(jié)點掛掉, 大約 15 秒后, 會重新開始選舉, 并且會在剩余的 4 個節(jié)點中,產(chǎn)生一個新的 leader. 并開始發(fā)送心跳日志鸣戴。

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

正常狀態(tài)下
  1. 在 idea 中配置 5 個 application 啟動項,配置 main 類為 RaftNodeBootStrap 類, 加入 -DserverPort=8775 -DserverPort=8776 -DserverPort=8777 -DserverPort=8778 -DserverPort=8779
  2. 依次啟動 5 個 RaftNodeBootStrap 節(jié)點, 端口分別是 8775,8776窄锅, 8777, 8778, 8779.
  3. 使用客戶端寫入 kv 數(shù)據(jù).
  4. 殺掉所有節(jié)點, 使用 junit test 讀取每個 rocksDB 的值, 驗證每個節(jié)點的數(shù)據(jù)是否一致.
非正常狀態(tài)下
  1. 在 idea 中配置 5 個 application 啟動項,配置 main 類為 RaftNodeBootStrap 類, 加入 -DserverPort=8775 -DserverPort=8776 -DserverPort=8777 -DserverPort=8778 -DserverPort=8779
  2. 依次啟動 5 個 RaftNodeBootStrap 節(jié)點, 端口分別是 8775创千,8776缰雇, 8777, 8778, 8779.
  3. 使用客戶端寫入 kv 數(shù)據(jù).
  4. 殺掉 leader (假設(shè)是 8775).
  5. 再次寫入數(shù)據(jù).
  6. 重啟 8775.
  7. 關(guān)閉所有節(jié)點, 讀取 RocksDB 驗證數(shù)據(jù)一致性.

Summary

本文并沒有貼很多代碼,如果要貼代碼的話追驴,閱讀體驗將不會很好械哟,并且代碼也不能說明什么,如果想看具體實現(xiàn)殿雪,可以到 github 上看看暇咆,順便給個 star :)

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

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

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

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

項目地址:https://github.com/stateIs0/lu-raft-kv

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鸦列,一起剝皮案震驚了整個濱河市租冠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌薯嗤,老刑警劉巖顽爹,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異骆姐,居然都是意外死亡镜粤,警方通過查閱死者的電腦和手機(jī)捏题,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肉渴,“玉大人公荧,你說我怎么就攤上這事⊥妫” “怎么了稚矿?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長捻浦。 經(jīng)常有香客問我,道長桥爽,這世上最難降的妖魔是什么朱灿? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮钠四,結(jié)果婚禮上盗扒,老公的妹妹穿的比我還像新娘。我一直安慰自己缀去,他們只是感情好侣灶,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著缕碎,像睡著了一般褥影。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上咏雌,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天凡怎,我揣著相機(jī)與錄音,去河邊找鬼赊抖。 笑死统倒,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的氛雪。 我是一名探鬼主播房匆,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼报亩!你這毒婦竟也來了浴鸿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤捆昏,失蹤者是張志新(化名)和其女友劉穎赚楚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骗卜,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡宠页,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年左胞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片举户。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡烤宙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出俭嘁,到底是詐尸還是另有隱情躺枕,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布供填,位于F島的核電站拐云,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏近她。R本人自食惡果不足惜叉瘩,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望粘捎。 院中可真熱鬧薇缅,春花似錦、人聲如沸攒磨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽娩缰。三九已至灸撰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拼坎,已是汗流浹背梧奢。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留演痒,地道東北人亲轨。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像鸟顺,于是被迫代替她去往敵國和親惦蚊。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容