raft 系列解讀(2) 之 測試用例
基于mit的6.824課程,github代碼地址:https://github.com/zhuanxuhit/distributed-system
case1:TestInitialElection
測試中3個server,然后啟動,驗(yàn)證在同一個任期(term)內(nèi)是否只有一個leader,并且在2 * RaftElectionTimeout
后,由于心跳的存在,不會發(fā)生重選。
在代碼實(shí)現(xiàn)中,主要有以下幾點(diǎn):
- 實(shí)現(xiàn)
AppendEnties
和RequestVote
兩個rpc部分功能 - 實(shí)現(xiàn)
Make
新建Raft
我們來看下其中主要的關(guān)鍵點(diǎn):
程序整體組織上是在Make
中啟動了一個goroutine,是一個無限循環(huán),根據(jù)不同的狀態(tài)進(jìn)行不同的處理,結(jié)構(gòu)如下:
follower
先講第一個狀態(tài)follower
的處理
所有的server重啟后第一個狀態(tài)都是follower
,如果在election timeout
時間內(nèi),既沒有收到leader的heartbeat
,也沒有收到RequestVote
請求,那么開啟選舉過程,此時狀態(tài)將轉(zhuǎn)換為candidate
,代碼如下:
rf.resetElectionTimeout()
// 等待心跳,如果心跳未到,但是選舉超時了,則開始新一輪選舉
select {
case <-rf.heartbeatChan:
case <-time.After(rf.randomizedElectionTimeout):
// 開始重新選舉
log.Println("election timeout:", rf.randomizedElectionTimeout)
if rf.status != STATUS_FOLLOWER {
// panic
log.Fatal("status not right when in follower and after randomizedElectionTimeout:", rf.randomizedElectionTimeout)
}
rf.convertToCandidate()
}
candidate
接著開始第二個狀態(tài)candidate
的處理:
- 第一步,新增本地任期和投票
- 第二步,重置 election timer 并開始廣播
- 第三步等待結(jié)果
- 1)他自己贏得了選舉;
- 2)收到AppendEntries得知另外一個服務(wù)器確立他為Leader,轉(zhuǎn)變?yōu)閒ollower
- 一個周期時間過去但是沒有任何人贏得選舉,開始新的選舉
結(jié)構(gòu)大致如下:
leader
如果此時贏得了選舉,則進(jìn)入第3個狀態(tài)leader
的處理:目前l(fā)eader只實(shí)現(xiàn)了一個功能,周期性的發(fā)送心跳,功能非常簡單,此處不再貼代碼了。
rpc
剩下就是兩個rpc的發(fā)送和接收處理了,其中需要特別注意的點(diǎn)如下:
- 所有rpc處理中:如果收到的請求或者響應(yīng)中,包含的term大于當(dāng)前的currentTerm,設(shè)置currentTerm=term,然后變?yōu)閒ollower
- 所有rpc處理中:判斷任期是否小于currentTerm,小于的都丟棄
在完成第一個測試的過程中:AppendEnties只需要處理心跳請求即可。
最后給出代碼的地址:https://github.com/zhuanxuhit/distributed-system,tag是:lab3-raft-case1
case2:TestReElection
有3個server,選舉出來一個leader后,模擬leader故障,重新選舉出一個leader,然后再模擬older leader故障恢復(fù)重新加入,此時也只會有一個leader,再模擬3個2個都故障了,那理論上就不會有l(wèi)eader出現(xiàn)了,此時再逐個加入故障的server,都只會有一個leader
直接運(yùn)行測試
go test -v -run ReElection
- leader故障,新的leader選出來
- 老的leader加入,不影響只有一個leader
- 兩個server故障,不會有新的leader
- 恢復(fù)一個server,出現(xiàn)leader
- 再次恢復(fù)一個server,出現(xiàn)leader
先看第1個,出現(xiàn)的調(diào)試信息:
2016/10/10 18:44:46 follower: 0 election timeout: 1.287113937s
2016/10/10 18:44:46 now I begin to candidate,index: 0
2016/10/10 18:44:47 follower: 2 election timeout: 1.54916732s
2016/10/10 18:44:47 now I begin to candidate,index: 2
可以看到0開始選舉后,不知道為什么2沒有投票,去看代碼,發(fā)現(xiàn)問題是:
- 當(dāng)發(fā)現(xiàn)遠(yuǎn)端term大于本地term后,直接轉(zhuǎn)換為follower,并更新當(dāng)前的currentTerm和voteFor
修改后即可通過測試,接著馬上又出現(xiàn)另一個問題:
2016/10/10 18:54:50 candidate: 0 'slog is not at least as up-to-date as receiver’s log
但是我們現(xiàn)在做的是沒有日志的,查看代碼發(fā)現(xiàn)問題是:
- (args.LastLogIndex < rf.commitIndex || args.LastLogTerm < currentTerm),因?yàn)閏urrentTerm增加了,但是LastLogTerm是0,所以要考慮
rf.commitIndex == 0
表示還沒有日志,則沒必要檢查
修改完后,再次運(yùn)行case,這次是兩個server故障,不會有新的leader出問題了,選舉不出來,接著查原因:
在處理投票的時候,往heartbeatChan
寫的時候阻塞了,rf.heartbeatChan = make(chan bool, 1)
是有一個緩沖的channel,那為什么會阻塞呢,我們看下有幾個地方會寫,幾個地方會去讀
有兩個地方會去寫:
- AppendEnties中收到心跳會去寫,當(dāng)去寫的時候,說明是已經(jīng)有l(wèi)eader了,自己會轉(zhuǎn)變?yōu)閒ollower
- RequestVote中收到投票也會去寫
讀的地方也有兩個
- 在狀態(tài)follower中,去讀
heartbeatChan
,如果選舉超時內(nèi)沒收到心跳,則開始candidate - 在candidate狀態(tài),去讀去讀
heartbeatChan
,表示已經(jīng)有新的leader產(chǎn)生了
于是就發(fā)現(xiàn)了問題:
- 在實(shí)現(xiàn)leader任務(wù)的時候,沒有一個點(diǎn)去觸發(fā)退出心跳
- 選舉失敗,應(yīng)該等待超時,然后重新開始新一輪選舉,而不是馬上開始新一輪選舉,這樣子造成彼此都不成功
修改代碼后,通過case2
case3:TestBasicAgree
這個case開始要做提交了,實(shí)現(xiàn)Start()
函數(shù)了,這個case主要測試是:有5個server,沒提交前檢查沒有提交的log,然后提交后,測試該log是否已經(jīng)被每個server都存儲了。
在實(shí)現(xiàn)start中,其做的步驟是:
// 客戶端的一次日志請求操作觸發(fā)
// 1)Leader將該請求記錄到自己的日志之中;
// 2)Leader將請求的日志以并發(fā)的形式,發(fā)送AppendEntries RCPs給所有的服務(wù)器;
// 3)Leader等待獲取多數(shù)服務(wù)器的成功回應(yīng)之后(如果總共5臺,那么只要收到另外兩臺回應(yīng)),
// 將該請求的命令應(yīng)用到狀態(tài)機(jī)(也就是提交),更新自己的commitIndex 和 lastApplied值;
// 4)Leader在與Follower的下一個AppendEntries RPCs通訊中,
// 就會使用更新后的commitIndex,Follower使用該值更新自己的commitIndex;
// 5)Follower發(fā)現(xiàn)自己的 commitIndex > lastApplied
// 則將日志commitIndex的條目應(yīng)用到自己的狀態(tài)機(jī)(這里就是Follower提交條目的時機(jī))
實(shí)現(xiàn)的關(guān)鍵點(diǎn):在Start函數(shù)中,一旦判斷出當(dāng)前server是leader,馬上開啟一個goroutine,開始異步進(jìn)行agree工作,然后立即返回,代碼如下:
此處第4步和第5步需要在另外的地方完成,一個是heartbeat中,另一個是follower在處理AppendEntries過程中
還有就是在成為leader的時候,需要初始化nextIndex,matchIndex
而在發(fā)送heartbeat中,判斷l(xiāng)og的最大index ≥ nextIndex,如果大于,需要發(fā)送從nextIndex開始的log,在發(fā)送完后需要判斷成功與否,成功則更新
nextIndex,matchIndex
,失敗則減少nextIndex
,并重試還有最重要的一點(diǎn):為了通過測試,記住要在日志提交后,發(fā)送消息ApplyMsg
給applymsg
,這樣才能通過測試
好了到此為止,寫的代碼剛好通過第三個測試,繼續(xù)下一關(guān)的!
case4:TestFailAgree
測試的內(nèi)容是:有3個server,其中一個follower故障,發(fā)的命令只有2個能收到,當(dāng)恢復(fù)故障后,發(fā)的命令都能收到
出現(xiàn)的問題:由于每個command真正提交都是通過goroutine來執(zhí)行的,因此每個goroutine之間并發(fā)執(zhí)行,怎么保證前一個agree了,下一個才能agree成功呢?
現(xiàn)在出現(xiàn)的問題是:
map[3:103 5:104 1:101 2:102],亂序,即4還沒有提交了,5就提交成功了
現(xiàn)在的問題是:誰也不服誰,當(dāng)follower恢復(fù)后,大家都競選,但是沒有一個成功,查明原因后發(fā)現(xiàn)是因?yàn)闆]有處理一個概念:
>如果候選人的日志至少和大多數(shù)的服務(wù)器節(jié)點(diǎn)一樣新
這個一樣新通過:比較兩份日志中最后一條日志條目的索引值和任期號定義誰的日志比較新。如果兩份日志最后的條目的任期號不同,那么任期號大的日志更加新。如果兩份日志最后的條目任期號相同,那么日志比較長的那個就更加新。
進(jìn)行到這,發(fā)現(xiàn)已經(jīng)很難調(diào)試了,代碼太亂,邏輯混亂,于是準(zhǔn)備開始重構(gòu)
現(xiàn)有代碼的問題:
- 臨界區(qū)的混亂,到底哪里加鎖,哪里不加
- 各個goroutine之間交互的混亂
- 代碼功能組織的問題
重構(gòu)的代碼最重要的一點(diǎn)是:抽象出了狀態(tài)機(jī),在里面去更新
case5:FailNoAgree
測試內(nèi)容是:5個server,3個follow故障,此時提交的命令將不會Committed,然后恢復(fù)3個follower,此時發(fā)送第3個命令,會忘記第2個沒有確認(rèn)的命令,此時第3個命令的index應(yīng)該還是2
現(xiàn)在出現(xiàn)的問題是:
follow的日志沒更新,但是leader的nextIndex確更新了!
2016/10/13 10:44:20 leader is 4
2016/10/13 10:44:22 server:0,currentTerm:3,role:candidate
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]
2016/10/13 10:44:22 server:1,currentTerm:3,role:candidate
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]
2016/10/13 10:44:22 server:2,currentTerm:3,role:candidate
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]
2016/10/13 10:44:22 server:3,currentTerm:2,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1} {2 20 2}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]
2016/10/13 10:44:22 server:4,currentTerm:2,role:leader
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1} {2 20 2}]
nextIndex is:[2 2 2 3 3]
matchIndex is:[1 1 1 2 0]
2016/10/13 10:44:22 恢復(fù)3個server
2016/10/13 10:44:25 LeaderId: 4 has big term: 5 than follower: 3 currentTerm: 4
2016/10/13 10:44:25 server 3 len(rf.log) 3 args.PrevLogIndex 1
2016/10/13 10:44:26 重新選舉后leader is 4
2016/10/13 10:44:26 server:0,currentTerm:5,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]
2016/10/13 10:44:26 server:1,currentTerm:5,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]
2016/10/13 10:44:26 server:2,currentTerm:5,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]
2016/10/13 10:44:26 server:3,currentTerm:5,role:follower
commitIndex:2,lastApplied:2
log is:[{0 <nil> 0} {2 10 1} {2 20 2}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]
2016/10/13 10:44:26 server:4,currentTerm:5,role:leader
commitIndex:2,lastApplied:2
log is:[{0 <nil> 0} {2 10 1} {2 20 2}]
nextIndex is:[3 3 3 3 3]
matchIndex is:[2 2 2 2 0]
看重新選舉后,leader4:matchIndex is:[2 2 2 2 0],但是其他的follower確沒有收到新的日志,怎么回事呢?看代碼什么情況下回去更新matchIndex呢?
問題在于發(fā)送心跳的時候返回了reply=true了,確沒有去檢查日志是否是最新的
此處記住appendEntries如果返回true,則一定表示是日志一樣新了!
true if follower contained entry matching prevLogIndex and prevLogTerm
case6:ConcurrentStarts
這個case測試的是:
同時發(fā)送5個命令,然后測試5個命令能夠被順序的提交
測試中的修改是:
將紅色框中的內(nèi)容移動到了鎖里面,為了防止并發(fā)訪問的時候,index得到相同。
case7:Rejoin
測試重新加入直接通過了,之前的代碼就能實(shí)現(xiàn)
測試內(nèi)容是:3個server,leader故障,然后向故障的leader發(fā)送命令,同時向新選舉出來的leader發(fā)送命令,大致如下圖,最后能統(tǒng)一
case8:Backup
類似case7:不同在于此處有5個server,然后命令更多,測試也是網(wǎng)絡(luò)分區(qū)后出現(xiàn)多l(xiāng)eader,然后恢復(fù)網(wǎng)絡(luò)后,再重新同步數(shù)據(jù)
不用修改,直接通過
case9:Count
case9主要是性能測試,測試rpc的次數(shù)不能太多
case10-12:Persist1-3
持久化的邏輯一直沒有加上,此處加上的
先看需要持久化哪些數(shù)據(jù),然后持久化的時機(jī)是什么時候?
需要持久化哪些日志?
e.Encode(rf.currentTerm) // 當(dāng)前任期
e.Encode(rf.log) // 收到的日志
e.Encode(rf.votedFor) // 投票的
e.Encode(rf.commitIndex) // 已經(jīng)確認(rèn)的一致性日志,之后的日志表示還沒有確認(rèn)是否可以同步,一旦確認(rèn)的日志都不會改變了
既然這幾個需要同步,那就是發(fā)生改變的時候把數(shù)據(jù)持久化下來就可以了
需要調(diào)用persist()
函數(shù)的地方有:
- leader向各個follower發(fā)送完日志,確認(rèn)提交的時候
- follower處理AppendEnties有新日志或者commiIndex更新的時候
case13:Figure8
測試主要測試的是下面的這張圖:
描述的問題是:為什么領(lǐng)導(dǎo)人無法通過老的日志的任期號來判斷其提交狀態(tài)。
- (a) S1 是領(lǐng)導(dǎo)者,部分的復(fù)制了索引位置 2 的日志條目
- (b) S1 崩潰了,然后 S5 在任期 3 里通過 S3、S4 和自己的選票贏得選舉,然后從客戶端接收了一條不一樣的日志條目放在了索引2 處
- (c) S5 又崩潰了;S1 重新啟動,選舉成功,開始復(fù)制日志。在這時,來自任期 2 的那條日志已經(jīng)被復(fù)制到了集群中的大多數(shù)機(jī)器上,但是還沒有被提交
- (d) S1 又崩潰了,S5 可以重新被選舉成功(通過來自 S2,S3 和 S4 的選票),然后覆蓋了他們在索引 2 處的日志。但是,在崩潰之前,如果 S1 在自己的任期里復(fù)制了日志條目到大多數(shù)機(jī)器上
- (e) 然后這個條目就會被提交(S5 就不可能選舉成功)。 在這個時候,之前的所有日志就會被正常提交處理
Raft采用計算副本數(shù)的方式,使得永遠(yuǎn)不會提交前前 面紀(jì)元的日志條目,
現(xiàn)在出現(xiàn)的問題是commit了不同的值?
即在沒有達(dá)成一致的情況下就就行了提交!
Test: Figure 8 ...
2016/10/13 20:38:35 server:0,currentTerm:2,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {1 1752890841475247006 1}]
nextIndex is:[1 1 1 1 1]
matchIndex is:[0 0 0 0 0]
2016/10/13 20:38:35 server:2,currentTerm:2,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {1 1752890841475247006 1}]
nextIndex is:[1 1 1 1 1]
matchIndex is:[0 0 0 0 0]
2016/10/13 20:38:35 server:4,currentTerm:2,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {1 1752890841475247006 1}]
nextIndex is:[1 1 1 1 1]
matchIndex is:[0 0 0 0 0]
2016/10/13 20:38:35 apply error: commit index=2 server=1 4541014630978635374 != server=3 8558661384468427932
到這就得加上之前忘記的一個策略
如果存在以個N滿足 N>commitIndex,多數(shù)的matchIndex[i] >= N,并且 log[N].term == currentTerm:設(shè)置commitIndex = N
主要是指:leader只會提交本紀(jì)元的日志
case14:UnreliableAgree
模擬網(wǎng)絡(luò)不可靠,在不可靠的情況下cfg.setunreliable(false)
,則有概率還是丟棄請求,在這種情況下測試協(xié)議最后還能達(dá)成一致
case15:Figure8Unreliable
通過設(shè)置cfg.setlongreordering(true)
,在labrpc中會直接睡眠一段時間,模擬這次情況下協(xié)議還是達(dá)成一致
ms := 200 + rand.Intn(1 + rand.Intn(2000))
time.Sleep(time.Duration(ms) * time.Millisecond)
2016/10/14 14:51:11 server:4,currentTerm:31,role:follower
commitIndex:3,lastApplied:3
2016/10/14 14:51:11 server:3,currentTerm:31,role:follower
commitIndex:3,lastApplied:3
2016/10/14 14:51:11 server:2,currentTerm:31,role:follower
commitIndex:3,lastApplied:3
2016/10/14 14:51:11 server:1,currentTerm:31,role:follower
commitIndex:3,lastApplied:3
2016/10/14 14:51:11 server:0,currentTerm:31,role:leader
commitIndex:3,lastApplied:3
nextIndex is:[186 53 58 51 62]
matchIndex is:[185 0 0 0 0]
2016/10/14 16:09:45 check log type: raft.AppendEntiesArgs value: {6 1 1 1 1 [{1 4411 2} {2 9540 3} {4 3863 4} {6 2769 5}]}
2016/10/14 16:09:45 error log indexserver:0,currentTerm:6,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {1 606 1} {1 4411 2} {4 3863 4} {6 2769 5}]
nextIndex is:[84 0 0 3 2]
matchIndex is:[83 1 1 2 1]
錯誤日志,由于沒有很好的傳遞日志,代碼bug
case16-17:TestReliableChurn,UnreliableChurn
測試通過
下一篇的計劃是結(jié)合代碼再次看下關(guān)鍵實(shí)現(xiàn)