上來先看了2b的要求文檔。主要就是要handle log了。
里面強烈推薦了3篇文章绞旅,我認真做了閱讀。發(fā)現(xiàn)了我之前實現(xiàn)的一些問題温艇。
其中一個是來自中文的翻譯
比如:如果在超過選取領導人時間之前沒有收到來自當前領導人的AppendEntries RPC或者沒有收到候選人的投票請求因悲,則自己轉(zhuǎn)換狀態(tài)為候選人
原文:If election timeout elapses without receiving AppendEntries RPC from current leader or granting vote to candidate: convert to candidate.
區(qū)別就是一個是授予了投票再喚醒,另一個是只要收到rpc就喚醒勺爱。
文章鏈接來自hint:
- Give yourself time to rewrite your implementation in light of lessons learned about structuring concurrent code. In later labs you'll thank yourself for having Raft code that's as clear and clean as possible. For ideas, you can re-visit our structure,locking and guide, pages.
實現(xiàn)的時候晃琳,發(fā)現(xiàn)除了是授予了投票,要喚醒。還有當選了LEADER也要喚醒卫旱。
重跑了下測試人灼,沒啥問題
第一步 閱讀論文5.3- 6之前
https://www.infoq.cn/article/raft-paper
第二步 FROM HINT
You will need to implement the election restriction (section 5.4.1 in the paper).
Raft 使用投票的方式來阻止沒有包含全部日志條目的服務器贏得選舉。一個候選人為了贏得選舉必須要和集群中的大多數(shù)進行通信顾翼,這就意味著每一條已經(jīng)提交的日志條目最少在其中一臺服務器上出現(xiàn)霞幅。如果候選人的日志至少和大多數(shù)服務器上的日志一樣新(up-to-date呻疹,這個概念會在下邊有詳細介紹)戳稽,那么它一定包含有全部的已經(jīng)提交的日志條目察署。RequestVote RPC 實現(xiàn)了這個限制:這個 RPC(遠程過程調(diào)用)包括候選人的日志信息,如果它自己的日志比候選人的日志要新拜姿,那么它會拒絕候選人的投票請求耗绿。
Raft 通過比較日志中最后一個條目的索引和任期號來決定兩個日志哪一個更新。如果兩個日志的任期號不同砾隅,任期號大的更新误阻;如果任期號相同,更長的日志更新晴埂。
這一步 應該上一章已經(jīng)實現(xiàn)了究反。
剩下一些HINT,在上一章基本都COVER住了儒洛,不用太多改動
比如
One way to fail the early Lab 2B tests is to hold un-needed elections, that is, an election even though the current leader is alive and can talk to all peers. This can prevent agreement in situations where the tester believes agreement is possible. Bugs in election timer management, or not sending out heartbeats immediately after winning an election, can cause un-needed elections.
第三步 實現(xiàn)START
根據(jù)方法描述精耐,實現(xiàn)如下
第四步 實現(xiàn)startAppendLog
搜索到//now only for heartbeat
然后開始把這個方法從只是發(fā)送HEART BEAT 到真的開始發(fā)日志。
論文重點
當這個條目被安全的復制之后(下面的部分會詳細闡述)琅锻,領導人會將這個條目應用到它的狀態(tài)機中并且會向客戶端返回執(zhí)行結(jié)果卦停。如果追隨者崩潰了或者運行緩慢或者是網(wǎng)絡丟包了,領導人會無限的重試 AppendEntries RPC(甚至在它向客戶端響應之后)直到所有的追隨者最終存儲了所有的日志條目恼蓬。
領導人跟蹤記錄它所知道的被提交條目的最大索引值惊完,并且這個索引值會包含在之后的 AppendEntries RPC 中(包括心跳 heartbeat 中),為的是讓其他服務器都知道這條條目已經(jīng)提交处硬。一旦一個追隨者知道了一個日志條目已經(jīng)被提交小槐,它會將該條目應用至本地的狀態(tài)機(按照日志順序)
- 如果在不同日志中的兩個條目有著相同的索引和任期號,則它們所存儲的命令是相同的荷辕。
- 如果在不同日志中的兩個條目有著相同的索引和任期號凿跳,則它們之間的所有條目都是完全一樣的。
第一條特性源于領導人在一個任期里在給定的一個日志索引位置最多創(chuàng)建一條日志條目疮方,同時該條目在日志中的位置也從來不會改變控嗜。第二條特性源于 AppendEntries 的一個簡單的一致性檢查。當發(fā)送一個 AppendEntries RPC 時骡显,領導人會把新日志條目緊接著之前的條目的索引位置和任期號都包含在里面疆栏。如果追隨者沒有在它的日志中找到相同索引和任期號的日志曾掂,它就會拒絕新的日志條目。這個一致性檢查就像一個歸納步驟:一開始空的日志的狀態(tài)一定是滿足日志匹配原則的承边,一致性檢查保證了當日志添加時的日志匹配原則遭殉。因此石挂,只要 AppendEntries 返回成功的時候博助,領導人就知道追隨者們的日志和它的是一致的了。
下面是最關鍵的一段話
為了使得追隨者的日志同自己的一致痹愚,領導人需要找到追隨者同它的日志一致的地方富岳,然后刪除追隨者在該位置之后的條目,然后將自己在該位置之后的條目發(fā)送給追隨者拯腮。這些操作都在 AppendEntries RPC 進行一致性檢查時完成窖式。領導人給每一個追隨者維護了一個nextIndex,它表示領導人將要發(fā)送給該追隨者的下一條日志條目的索引动壤。當一個領導人開始掌權時萝喘,它會將nextIndex初始化為它的最新的日志條目索引數(shù) +1(圖 -7 中的 11)。如果一個追隨者的日志和領導者的不一致琼懊,AppendEntries 一致性檢查會在下一次 AppendEntries RPC 時返回失敗阁簸。在失敗之后,領導人會將nextIndex遞減然后重試 AppendEntries RPC哼丈。最終nextIndex會達到一個領導人和追隨者日志一致的地方启妹。這時,AppendEntries 會返回成功醉旦,追隨者中沖突的日志條目都被移除了饶米,并且添加所缺少的上了領導人的日志條目。一旦 AppendEntries 返回成功车胡,追隨者和領導人的日志就一致了檬输,這樣的狀態(tài)會保持到該任期結(jié)束。
需要知道GO 切片知識
https://stackoverflow.com/questions/27055626/concisely-deep-copy-a-slice?rq=1
因為 //在失敗之后匈棘,領導人會將nextIndex遞減然后重試 AppendEntries RPC
所以要包在一個FOR循環(huán)里褪猛,失敗就重試,成功就RETURN,因為變成了循環(huán) 注意DEFER unlock
就不能用在循環(huán)體里了羹饰,不然就死鎖了伊滋。
下圖我們發(fā)現(xiàn)第一條我們實現(xiàn)了,后面都沒有完整實現(xiàn)队秩。第二條的前半段在之前Start()
里實現(xiàn)的笑旺。后半段把條目應用到狀態(tài)機后響應客戶端還沒實現(xiàn)。
領導人規(guī)則的第五條馍资,因為是要看MATCH INDEX筒主,而MATCH INDEX只在發(fā)送成功的時候會更新。所以為了減少不必要的CHECK,我們可以放在
reply.Success
里來做注意 這里中文是病句
因為原文為
If there exists an N such that N > commitIndex, a majority of matchIndex[i] ≥ N, and log[N].term == currentTerm: set commitIndex = N (§5.3, §5.4).
我們一點一點來乌妙,先處理reply.Success
第五步 實現(xiàn)updateCommitIndex
就是根據(jù)上面的英文來實現(xiàn)使兔,超過半數(shù)的話,就排序然后找中間偏大的那個藤韵,來比即可虐沥。
注意在找超過半數(shù)的時候,很容易忘記LEADER自己的INDEX沒有更新泽艘,造成臨界點剛好沒到半數(shù)欲险。
到此為止,我們基本實現(xiàn)了startAppendLog
匹涮,第一個//now only for heartbeat
變?yōu)榱苏娴臅嗀PPEND LOG的方法
下面開始寫HANDLER
第六步 實現(xiàn)AppendEntries
這里論文也有寫怎么做天试,介于中文翻譯的質(zhì)量確實不理想。這里貼個英文圖
因為這邊開始會RETURN了然低。所以我們要把收到APPEND ENTRIES 這個RPC的CHANNEL 喚醒的操作放到DEFER里去
在UTIL.GO 里定義MIN INT 方法
不知道你有沒有注意到我們一直在更新commitIndex
喜每,但是我們沒有更新lastApplied
這里還有一條所有服務器的規(guī)則
如果commitIndex > lastApplied,lastApplied自增雳攘,將log[lastApplied]應用到狀態(tài)機(5.3 節(jié))
第七步 思考何時updateLastApplied
為了減少調(diào)用次數(shù)带兜,我們只要思考何時會更新COMMITINDEX
發(fā)現(xiàn)2處
第八步 實現(xiàn)updateLastApplied
調(diào)試
BUG 1
按照老師的建議,我們先測2B最簡單的来农,發(fā)現(xiàn)出問題了鞋真,INDEX越界
排查了下。
這邊應該是=沃于,我之前寫成+=
BUG2
數(shù)組越界的錯沒了涩咖。下面的錯非常棘手。
他報了這個 config.go:465: one(100) failed to reach agreement
在仔細閱讀了https://thesquareplanet.com/blog/students-guide-to-raft/
我依然沒有任何收獲繁莹。
決定在關鍵位置打LOG檩互。大概率是因為APPEND ENTRIES 這個RPC 有邏輯沒做對。
所以我在發(fā)送RPC前后咨演,加了LOG
得到信息如下
感覺沒太大問題闸昨,不過可以知道REPLY 是SUCCESS的。說明HANDLER里面邏輯是正確的薄风。
因為最終服務器需要在APPLY CHANNEL饵较,讀信息
所以我們在CHANNEL出去的時候做一個打印,看下有沒有出去遭赂。
隨后因為走的分支是REPLY.SUCCESS
在這個里面把對應的參數(shù)也打印一下循诉。
打印結(jié)果,發(fā)現(xiàn)消息完全沒有出去的跡象撇他。
分析LOG可以得知
matchIndex 一開始是-1茄猫,后來變成了0
而我們初始化的時候就對matchIndex 初始化為了0.
再增加了一個元素后狈蚤,依然是0,而updateCommitIndex
的關鍵就是N > commitIndex, a majority of matchIndex[i] ≥ N划纽。
那么問題就是即使所有NODE 都收到了那個元素脆侮,然而matchIndex最大也是到0, 0是不會超過commitIndex 初始化的0
再回過去看commitIndex 和 matchIndex的定義
按照這個定義的理解勇劣,只有被提交的最高索引靖避。那么初始化怎么說也應該是-1呀。
我開始按照我的思路改成-1芭毙,APPLYMSG出去了筋蓖⌒对牛可是測試需要的INDEX是我返回的INDEX還要+1.我又不好改測試退敦。
一陣無語的時候我發(fā)現(xiàn)
這么重要的信息中文翻譯里竟然沒有
改動如下0改稱1,加上注釋
PASS 2B
Test Race
TEST.SH
因為有些并發(fā)的問題蚣抗,可能不是每次重現(xiàn)侈百,所以要盡可能多測來確定有沒有問題。
我把這個標準定為1000次翰铡, 可是1000次要跑很久钝域。所以我通過SHELL來處理這個問題。
SHELL 腳本如下锭魔,每處理完10次TEST打一個進度
#!/bin/bash
export PATH="$PATH:/usr/lib/go-1.9/bin"
rm res -rf
mkdir res
test_list=(
2A
2B)
for i in `seq 1000`
do
if [ $(($i % 10)) -eq 0 ]
then
echo $i
fi
for j in ${test_list[@]}
do
go test -run $j &> ./res/$j
if [ $? -ne 0 ]
then
echo "run failed."
exit 1
fi
done
done