好的實現(xiàn)埋合,看看別人怎么寫的卧须,github
大多數(shù)Raft的實現(xiàn)都是整體設(shè)計,包括存儲處理撰糠,消息序列化和網(wǎng)絡(luò)傳輸酥馍,但是本raft庫在實現(xiàn)的時候只實現(xiàn)了最核心的算法,換來了靈活性和性能阅酪,網(wǎng)絡(luò)和disk IO部分都由使用者實現(xiàn)旨袒,使用者需要實現(xiàn)自己的消息傳送層,同時术辐,需要自己實現(xiàn)持久化部分來存儲Raft log和state砚尽。
為了實現(xiàn)Raft庫的可測性,庫在實現(xiàn)的時候?qū)aft建模為一個狀態(tài)機辉词,輸入是消息必孤,可能是本地時間的更新或者網(wǎng)絡(luò)消息,狀態(tài)機的輸出是一個3元組:{[]Messages, []LogEntries, NextState}瑞躺。
第一步是使用敷搪,怎么使用raft來搭建自己的key-value系統(tǒng)
etcd-raft代碼走讀
上面是raft中一個node做的事,Node代表raft集群中的一個節(jié)點幢哨,剛開始node是follower赡勘,然后隨著
tickc
的進行,開始進入選舉捞镰,raft在變?yōu)閒ollower的時候做了下面幾件事:初始化了tick函數(shù)
tickElection
闸与,用來開始選舉,做判斷后岸售,調(diào)用Step
判斷消息類型為
MsgHup
践樱,于是進入campaign
選舉函數(shù)中做的事情
轉(zhuǎn)換成candidate時,開始一個選舉:
- 遞增currentTerm;投票給自己;
- 重置election timer;
- 向所有的服務(wù)器發(fā)送 RequestVote RPC請求
接著看下send函數(shù)
send函數(shù)中將消息存儲了
msgs
中,在哪兒消費呢凸丸?通過讀取newReady來返回Ready此時又返回到
node.run
中拷邢,此時因為會進入case readyc <- rd
分支在里面做的事情
msgs
因為已經(jīng)讀取過了,設(shè)置為空屎慢,并且會賦值advancec
解孙,進行到這readyc
中已經(jīng)有一個數(shù)據(jù)坑填, 而此channel會在函數(shù)Ready
中返回給外面,下面接著看誰會去讀Ready
func (n *node) Ready() <-chan Ready { return n.readyc }
讀取Ready的是應(yīng)用程序弛姜,看下Ready()
函數(shù)的說明
//=> 讀取到當(dāng)前狀態(tài)脐瑰,當(dāng)從Ready()取出狀態(tài)后,需要調(diào)用 Advance
//=> 注意:只有當(dāng)所有提交的entries都應(yīng)用后廷臼,才會調(diào)用下一個 Ready 的狀態(tài)
我們回到之前的選舉上苍在,讀取到的Ready里面包含了Vote消息,我們會調(diào)用網(wǎng)絡(luò)層發(fā)送消息出去荠商,并且調(diào)用Advance()
寂恬,而此時其他Node接收到網(wǎng)絡(luò)層消息后,會調(diào)用Step()
函數(shù)莱没,在成為candidate的時候初肉,我們設(shè)置了step函數(shù)為stepCandidate
,
自后調(diào)用了node的send函數(shù)饰躲,此時是拒絕的牙咏,因為已經(jīng)是candidate狀態(tài)了,而如果是follower嘹裂,其處理函數(shù)是
stepFollower
,其規(guī)則就是之前說到的:
如果本地的voteFor為空或者為candidateId,并且候選者的日志至少與接受者的日志一樣新,則投給其選票
進行到這妄壶,我們看到了follower在收到vote rpc后的處理,下面是candidate的處理了寄狼。
回到之前調(diào)用Ready()
丁寄,接著應(yīng)該調(diào)用Advance
,
里面會設(shè)置
advancec
,好了泊愧,運行到這伊磺,我們又要回到node.run
中了
此時的狀態(tài)是:candidate,advancec
中有數(shù)據(jù)删咱,接著來看candidate在發(fā)送出vote rpc奢浑,接收到響應(yīng)的處理,網(wǎng)絡(luò)層的Send
函數(shù)是:
Send會調(diào)用
Peer.send
腋腮,函數(shù)注釋說:此函數(shù)是非阻塞的,不保證請求一定能被peer收到一般常理我們發(fā)送后壤蚜,等待響應(yīng)后再處理即寡,但是找了很久也沒找到常理函數(shù),這個時候袜刷,我們再去看下follower對于投票的處理
發(fā)現(xiàn)在響應(yīng)上也是通過發(fā)送一個消息來響應(yīng)的聪富,因此我們此時可以看到peer之間的交互不是傳統(tǒng)意義上的request-response模型了。
我們?nèi)タ磳τ?code>MsgVoteResp的處理著蟹,其入口都是通過調(diào)用node.Step
函數(shù)墩蔓,此時如果得到大多數(shù)票選梢莽,則成為leader
看becomeLeader函數(shù)
在leader函數(shù)中,最重要的就是發(fā)送命令了奸披,我們看看這個過程
這是通過node.Propose
函數(shù)實現(xiàn)的
到最后又是通過step函數(shù)
里面挨個調(diào)用send函數(shù)
func (r *raft) bcastAppend() {
for id := range r.prs {
if id == r.id {
continue
}
r.sendAppend(id)
}
}
看完發(fā)送端昏名,接著看follower的接收端處理
細看handleAppendEntries函數(shù),就是去做raft協(xié)議中規(guī)定的操作了
在
maybeAppend
中阵面,會去嘗試更新committed index
轻局,然后接著看AppendResp的處理去檢查各個peer的matchIndex,然后嘗試更新commitIndex
下一個問題样刷,接著去看commitIndex > lastAppied后仑扑,在哪兒去應(yīng)用log到狀態(tài)機的
這是通過node.run
中readyc
和advancec
來實現(xiàn)的
上面就是etcd中raft的大致流程,有一個機遇raft實現(xiàn)的簡單key-value系統(tǒng)置鼻,github地址:https://github.com/zhuanxuhit/distributed-system/tree/master/etcd-raft
讀完代碼后镇饮,最大的一個感受是整個node在實現(xiàn)的時候都是無鎖的,其技巧是通過go的channel
將所有請求串行化箕母,然后另一個特點是根據(jù)不同的狀態(tài)储藐,設(shè)置不同的處理函數(shù),整個實現(xiàn)非常的清晰司蔬,因為每個狀態(tài)針對每個請求的處理都是非常明確的邑茄。