前言
本文旨在講述如何使用 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 種回答:
- 如果是無狀態(tài)服務(wù)局蚀,那么毫不影響使用麦锯。
- 如果是有狀態(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 個方面來回答:
- 有的時候 ZK 和 etcd 并不能解決我們的問題,或者像上面說的张漂,引入其他的中間件部署起來太麻煩也太重晶默。
- 完全處于好奇,好奇為什么 Raft 可以保證一致性(這通澈皆埽可以通過汗牛充棟的文章來得到解答)磺陡?但是到底該怎么實現(xiàn)?
- 分布式開發(fā)的要求漠畜,作為開發(fā)分布式系統(tǒng)的程序員币他,如果能夠更深刻的理解分布式系統(tǒng)的核心算法,那么對如何合理設(shè)計一個分布式系統(tǒng)將大有益處盆驹。
好圆丹,有了以上 3 個原因,我們就有足夠的動力來造輪子了躯喇,接下來就是如何造的問題了。
編寫前的 Raft 理論基礎(chǔ)
任何實踐都是理論先行硝枉。如果你對 Raft 理論已經(jīng)非常熟悉廉丽,那么可以跳過此節(jié),直接看實現(xiàn)的步驟妻味。
Raft 為了算法的可理解性正压,將算法分成了 4 個部分。
- leader 選舉
- 日志復(fù)制
- 成員變更
- 日志壓縮
同 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 的幾個核心功能,事實上站辉,就可以理解為接口呢撞。所以我們定義以下幾個接口:
- Consensus, 一致性模塊接口
- LogModule饰剥,日志模塊接口
- StateMachine殊霞, 狀態(tài)機(jī)接口
- RpcServer & RpcClient, RPC 接口
- Node汰蓉,同時绷蹲,為了聚合上面的幾個接口,我們需要定義一個 Node 接口顾孽,即節(jié)點祝钢,Raft 抽象的機(jī)器節(jié)點。
- 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)之前,需要確定幾個點:
- 選舉者必須不是 leader绷跑。
- 必須超時了才能選舉拳恋,具體超時時間根據(jù)你的設(shè)計而定,注意,每個節(jié)點的超時時間不能相同砸捏,應(yīng)當(dāng)使用隨機(jī)算法錯開(Raft 關(guān)鍵實現(xiàn))谬运,避免無謂的死鎖隙赁。
- 選舉者優(yōu)先選舉自己,將自己變成 candidate。
- 選舉的第一步就是把自己的 term 加一梆暖。
- 然后像其他節(jié)點發(fā)送請求投票 RPC伞访,請求參數(shù)參照論文,包括自身的 term轰驳,自身的 lastIndex厚掷,以及日志的 lastTerm。同時级解,請求投票 RPC 應(yīng)該是并行請求的冒黑。
- 等待投票結(jié)果應(yīng)該有超時控制,如果超時了勤哗,就不等待了抡爹。
- 最后,如果有超過半數(shù)的響應(yīng)為 success俺陋,那么就需要立即變成 leader 豁延,并發(fā)送心跳阻止其他選舉。
- 如果失敗了腊状,就需要重新選舉诱咏。注意,這個期間缴挖,如果有其他節(jié)點發(fā)送心跳袋狞,也需要立刻變成 follower,否則映屋,將死循環(huán)苟鸯。
上面說的棚点,其實是 Leader 選舉中早处,請求者的實現(xiàn),那么接收者如何實現(xiàn)呢瘫析?接收者在收到“請求投票” RPC 后砌梆,需要做以下事情:
- 注意,選舉操作應(yīng)該是串行的贬循,因為涉及到狀態(tài)修改咸包,并發(fā)操作將導(dǎo)致數(shù)據(jù)錯亂。也就是說杖虾,如果搶鎖失敗烂瘫,應(yīng)當(dāng)立即返回錯誤。
- 首先判斷對方的 term 是否小于自己奇适,如果小于自己坟比,直接返回失敗芦鳍。
- 如果當(dāng)前節(jié)點沒有投票給任何人,或者投的正好是對方温算,那么就可以比較日志的大小怜校,反之,返回失敗注竿。
- 如果對方日志沒有自己大茄茁,返回失敗。反之巩割,投票給對方裙顽,并變成 follower。變成 follower 的同時宣谈,異步的選舉任務(wù)在最后從 condidate 變成 leader 之前愈犹,會判斷是否是 follower,如果是 follower闻丑,就放棄成為 leader漩怎。這是一個兜底的措施。
到這里嗦嗡,基本就能夠?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ā)送一次心跳勋拟。注意點:
- 首先自己必須是 leader 才能發(fā)送心跳。
- 必須滿足 5 秒的時間間隔妈候。
- 并發(fā)的向其他 follower 節(jié)點發(fā)送心跳敢靡。
- 心跳參數(shù)包括自身的 ID,自身的 term苦银,以便讓對方檢查 term啸胧,防止網(wǎng)絡(luò)分區(qū)導(dǎo)致的腦裂。
- 如果任意 follower 的返回值的 term 大于自身幔虏,說明自己分區(qū)了纺念,那么需要變成 follower,并更新自己的 term想括。然后重新發(fā)起選舉陷谱。
然后是心跳接收者的實現(xiàn),這個就比較簡單了线婚,接收者需要做幾件事情:
- 無論成功失敗首先設(shè)置返回值逸邦,也就是將自己的 term 返回給 leader衷旅。
- 判斷對方的 term 是否大于自身,如果大于自身宪躯,變成 follower,防止異步的選舉任務(wù)誤操作夷都。同時更新選舉時間和心跳時間眷唉。
- 如果對方 term 小于自身,返回失敗囤官。不更新選舉時間和心跳時間冬阳。以便觸發(fā)選舉。
說完了心跳党饮,再說說真正的日志附加肝陪。
簡單來說,當(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ù),具體步驟如下:
- 每個節(jié)點都可能會接收到客戶端的請求衅檀,但只有 leader 能處理招刨,所以如果自身不是 leader,則需要轉(zhuǎn)發(fā)給 leader哀军。
- 然后將用戶的 KV 數(shù)據(jù)封裝成日志結(jié)構(gòu)沉眶,包括 term,index排苍,command沦寂,預(yù)提交到本地。
- 并行的向其他節(jié)點發(fā)送數(shù)據(jù)淘衙,也就是日志復(fù)制传藏。
- 如果在指定的時間內(nèi),過半節(jié)點返回成功彤守,那么就提交這條日志毯侦。
- 最后,更新自己的 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)鍵所在巨柒。
再來看看日志接收者的實現(xiàn)步驟:
- 和心跳一樣樱拴,要先檢查對方 term凝颇,如果 term 都不對,那么就沒什么好說的了疹鳄。
- 如果日志不匹配,那么返回 leader芦岂,告訴他瘪弓,減小 nextIndex 重試。
- 如果本地存在的日志和 leader 的日志沖突了禽最,以 leader 的為準(zhǔn)腺怯,刪除自身的。
- 最后川无,將日志應(yīng)用到狀態(tài)機(jī)呛占,更新本地的 commitIndex,返回 leader 成功懦趋。
到這里晾虑,日志復(fù)制的部分就講完了。
注意仅叫,實現(xiàn)日志復(fù)制的前提是帜篇,必須有一個正確的日志存儲系統(tǒng),即我們的 RocksDB诫咱,我們在 RocksDB 的基礎(chǔ)上笙隙,使用一種機(jī)制,維護(hù)了 每個節(jié)點 的LastIndex坎缭,無論何時何地竟痰,都能夠得到正確的 LastIndex,這是實現(xiàn)日志復(fù)制不可獲取的一部分掏呼。
驗證“Leader 選舉”和“日志復(fù)制”
寫完了程序坏快,如何驗證是否正確呢?
當(dāng)然是寫驗證程序哄尔。
我們首先驗證 “Leader 選舉”假消。其實這個比較好測試。
- 在 idea 中配置 5 個 application 啟動項,配置 main 類為 RaftNodeBootStrap 類, 加入 -DserverPort=8775 -DserverPort=8776 -DserverPort=8777 -DserverPort=8778 -DserverPort=8779
系統(tǒng)配置, 表示分布式環(huán)境下的 5 個機(jī)器節(jié)點. - 依次啟動 5 個 RaftNodeBootStrap 節(jié)點, 端口分別是 8775岭接,8776富拗, 8777, 8778, 8779.
- 觀察控制臺, 約 6 秒后, 會發(fā)生選舉事件,此時,會產(chǎn)生一個 leader. 而 leader 會立刻發(fā)送心跳維持自己的地位.
- 如果leader 的端口是 8775, 使用 idea 關(guān)閉 8775 端口,模擬節(jié)點掛掉, 大約 15 秒后, 會重新開始選舉, 并且會在剩余的 4 個節(jié)點中,產(chǎn)生一個新的 leader. 并開始發(fā)送心跳日志鸣戴。
然后驗證 日志復(fù)制啃沪,分為 2 種情況:
正常狀態(tài)下
- 在 idea 中配置 5 個 application 啟動項,配置 main 類為 RaftNodeBootStrap 類, 加入 -DserverPort=8775 -DserverPort=8776 -DserverPort=8777 -DserverPort=8778 -DserverPort=8779
- 依次啟動 5 個 RaftNodeBootStrap 節(jié)點, 端口分別是 8775,8776窄锅, 8777, 8778, 8779.
- 使用客戶端寫入 kv 數(shù)據(jù).
- 殺掉所有節(jié)點, 使用 junit test 讀取每個 rocksDB 的值, 驗證每個節(jié)點的數(shù)據(jù)是否一致.
非正常狀態(tài)下
- 在 idea 中配置 5 個 application 啟動項,配置 main 類為 RaftNodeBootStrap 類, 加入 -DserverPort=8775 -DserverPort=8776 -DserverPort=8777 -DserverPort=8778 -DserverPort=8779
- 依次啟動 5 個 RaftNodeBootStrap 節(jié)點, 端口分別是 8775创千,8776缰雇, 8777, 8778, 8779.
- 使用客戶端寫入 kv 數(shù)據(jù).
- 殺掉 leader (假設(shè)是 8775).
- 再次寫入數(shù)據(jù).
- 重啟 8775.
- 關(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 有興趣,歡迎一起交流溝通 :)