Preparation
-
實驗:http://nil.csail.mit.edu/6.824/2020/labs/lab-raft.html 的 Part 2A.
-
論文:
- 英文版:https://raft.github.io/raft.pdf
- 中文版:https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md
論文只要求看完 Section 5 即可。
其中個人認為主要需要看的幾個點在於:
- Figure 2 & 3.
- Section 5.1
- Section 5.2
- Section 5.4.1
Overview
Lab 2A 是實現 Leader Election。它主要關心各個角色的狀態切換,以及對於 AppendEntries RPC 和 RequestVote RPC 的請求響應。因為在 Lab 2A 的測試中並不會有日志相關的操作,所以我們也暫時不需要關心太多日志相關的內容。
Followers
- 響應所有來自 leader 和 candidate 的 RPC 請求。
- 如果在選舉時間超時前,沒有收到來自當前 leader 的 AppendEntries RPC(心跳檢測),或者沒有投票給 candidate,則將自己的狀態變成 candidate(這里之前有些誤解了,直到看了 Guide 里面的說法,才知道第二個條件實際對應的是在 RequestVote RPC 中,如果投票給 candidate,則重置選舉超時器)。
Candidates
- 當狀態變為 Candidate 的時候,開始進行選舉:
- 遞增當前的 term;
- 投票給自己;
- 重置選舉超時計時器;
- 發送 RequestVote RPC 給其他的服務器。
- 選舉的終止條件以及對應操作:
- 如果在選舉過程中收到大多數的選票,則將自身狀態變成 leader。
- 如果從新的 leader 接收到了 AppendEntries RPC(心跳檢測),則將自身狀態變成 follower。
- 如果選舉超時,則重新進行新一輪的選舉。
Leader
- 當狀態變為 Leader 的時候,立即發送 AppendEntries RPC(心跳檢測)給其他所有 server。
- (My Hint:當發送心跳檢測不能及時收到大多數 Follower 的響應時,將自己的狀態變成 Follower。
All Servers
- 在進行請求或者響應來自其他 server 的 RPC 時,若發現其他 server 的 term 大於當前 server 的 term,則將當前 server 的 term 更新為其他 server 的 term。
RequestVote RPC
- 如果
args.Term < rf.currentTerm
,則直接返回false
。 - 如果自己沒有投票給其他人或者投給了 candidateID,則重置選舉超時器並返回
true
。
AppendEntries RPC
- 如果
args.Term < rf.currentTerm
,則直接返回false
。 - 重置選舉超時器。
- 如果當前狀態是 candidate 並且發送者的 term 沒有過期,狀態變為 follower。
Implementation
Lab 2A 的代碼是放在 src/raft
里面,我們需要實現 raft.go
中的一部分。
我的具體實現放在 github 中 https://github.com/shadowdsp/mit6.824 .
Flow Chart
Data Structure
Raft
Raft 的數據結構我們可以看論文中 Figure 2 進行填充,並且補充一些在選舉時刻必要的變量。關於日志相關的屬性暫時用不到。
type State string
var (
Leader = State("Leader")
Candidate = State("Candidate")
Follower = State("Follower")
)
type Raft struct {
mu sync.Mutex // Lock to protect shared access to this peer's state
peers []*labrpc.ClientEnd // RPC end points of all peers
persister *Persister // Object to hold this peer's persisted state
me int // this peer's index into peers[]
dead int32 // set by Kill()
// Your data here (2A, 2B, 2C).
// Look at the paper's Figure 2 for a description of what
// state a Raft server must maintain.
// 1 follower, 2 candidate, 3 leader
state State
// Persistent state on server
currentTerm int
// votedFor initial state is -1
votedFor int
// follower election timeout timestamp
electionTimeoutAt time.Time
}
RPC
領導選舉主要涉及兩個 RPC:RequestVote 以及 AppendEntries,每個分別對應了請求 Args 和響應 Reply。為了方便 debug,也可以在請求或者響應里面加上 ServerID
。
type RequestVoteArgs struct {
// Your data here (2A, 2B).
Term int
CandidateID int
}
type RequestVoteReply struct {
// Your data here (2A).
Term int
VoteGranted bool
}
type AppendEntriesArgs struct {
Term int
}
type AppendEntriesReply struct {
Term int
// true if follower contained entry matching prevLogIndex and prevLogTerm
Success bool
}
Process
Raft 程序是由 Make
函數來啟動的。在 Make
中,我主要是初始化 raft 對象,然后調用 go rf.run(ctx)
來運行 raft 程序主體。
初始的時候,raft 的狀態為 Follower
,並且投票為 -1
表示還未投票。
rf := &Raft{
peers: peers,
persister: persister,
me: me,
state: Follower,
votedFor: -1,
}
// Your initialization code here (2A, 2B, 2C).
// initialize from state persisted before a crash
rf.readPersist(persister.ReadRaftState())
ctx := context.Background()
go rf.run(ctx)
rf.run()
主要是對 raft 狀態的進行判斷,並根據狀態執行不同的操作。
這里加了 time.Sleep(10ms)
是因為我跑了 100 個 test,在后面會發現有鎖沖突的情況。
func (rf *Raft) run(ctx context.Context) error {
for {
time.Sleep(10 * time.Millisecond)
state := rf.getState()
switch state {
case Follower:
... // check timeout and convert to cdd
break
case Candidate:
... // elect leader
break
case Leader:
... // send heartbeats
break
default:
panic(fmt.Sprintf("Server %v is in unknown state %v", rf.me, rf.state))
}
}
}
接下來就是按照 Figure2 中提到的,去填充每個 state 以及 RPC 的邏輯。
Test
當我們將程序寫完,使用 go test -run 2A
去執行測試。
強烈建議將 TestReElection2A
改成循環運行多次,我這里是運行 100 次,否則極大可能只是概率性地通過。概率性地通過意味着程序並不是正確的。
雖然我能通過 100 次也是加了一些 hack,例如在某些位置加了 sleep,以及調整了超時時間等,並不說明我的程序是完全正確的。
如果我的程序有什么問題,求指正,謝謝!!!
Problems
在測試的過程中,我陸續解決了一些問題,可能對你會有幫助。
實現 Figure2 - Rules for Servers - All servers 中的第二條規則時,不要忽略了 server 在收到 rpc 響應的時候也要檢查 reply.Term 去更新狀態。
這一點在看論文的時候不夠仔細,導致出錯。
Follower 心跳檢測的 timeout 和 candidate 選舉的 timeout 都是 electionTimeout。
最開始我是用兩個 timeout 去表示的,發現實現起來很奇怪,后面改成使用同一個。
並發編程需要注意死鎖以及 goroutine 泄漏。
死鎖這個還好,只要報錯基本能定位到哪里的問題。
Goroutine 泄漏體現於在 goroutine 中使用 channel,如果最后這個 channel 不會被關閉,那么這個 goroutine 會一直存活。
當 Leader 發出心跳檢測后,如果不能及時收到大多數節點的回復,需要變成 Follower。
我在測試 TestReElection2A
的過程中,發現跑了十幾次后,經常在 checkNoLeader()
掛了。這是測試三個 server 都出現網絡分區的情況。在此時,三個 server 都應該是 Follower state,因此需要加上這個機制。這里我的實現是,在 leader send heartbeats 時,對 rpc 的執行添加超時時間,使用 time.After()
去完成。
這里還有 MIT 助教寫的參考指南 https://thesquareplanet.com/blog/students-guide-to-raft/
Summary
Raft leader election 的理論相對容易,實現起來如果有問題,還是如同 Hint 里面說的,多看幾遍 Figure 2
: ).
If your code has trouble passing the tests, read the paper's Figure 2 again; the full logic for leader election is spread over multiple parts of the figure.