一、Raft選主流程
- 當新集羣啓動的時候,所有的機器A、B、C的默認狀態是Follower,所有的機器地址endpoint作爲初始化參數傳入進程。
- 如果收到心跳,則作爲Follower開始工作,選主結束。如果超過一段隨機選舉超時時間後(在一定範圍且大於心跳時間), 開始發起Election。隨機的目的是爲了保證不要同時發起Election,在少數情況下可能會發生同時發起選舉情況。
- 集羣初始化時沒有Leader,所以開始所以的機器沒有收到心跳,機器A開始發起選舉。首先機器A把自己的身份置爲Candidate,把Term自增1,並且自己投票給自己。
- 當其他機器B接受到這個vote投票請求,如果發現Candidate的Term小於自己,則否決,並返回自己的Term。 - 如果機器B發現Candidate的Term大於自己,就更新Term,並且重置投給誰votedFor爲A。 - 如果機器B發現當前Term已經給別人投票過,就否決。 - 如果 Candidate的日誌不是最新,就否決。 - 贊成
- 機器A在發送vote請求後,如果發現大多數贊成,則跳到步驟7,成爲Leader。
- 如果發現返回的Term大於自己,就放棄本次Election,更新自己的term到返回的term,重置自己的狀態爲Follower,重新開始步驟3。
- 如果機器A在投票結束,沒有收到大多數贊成票,則返回步驟3。
- 機器A選主成功, 調整自身狀態爲Leader, 並且立即發送一次心跳(避免其他機器超時進入選主狀態)。
- 週期性發送心跳, 保證Leader地位。
跟隨者只響應來自其他服務器的請求。如果跟隨者接收不到消息,那麼他就會變成候選人併發起一次選舉。獲得集羣中大多數選票的候選人將成爲領導者。領導人一直都會是領導人直到自己宕機了。
二、代碼實現
- 根據論文填充Raft結構字段。
- 添加實現Raft的接口,必須支持下面的接口,這些接口會在測試例子和你們最終的key/value服務器中使用。
// create a new Raft server instance:
rf := Make(peers, me, persister, applyCh)
// start agreement on a new log entry:
rf.Start(command interface{}) (index, term, isleader)
// ask a Raft for its current term, and whether it thinks it is leader
rf.GetState() (term, isLeader)
// each time a new entry is committed to the log, each Raft peer
// should send an ApplyMsg to the service (or tester).
type ApplyMsg
一個服務通過調用Make(peers,me,…)創建一個Raft端點。peers參數是通往其他Raft端點處於連接狀態下的RPC連接。me參數是自己在端點數組中的索引。
- 實現領導選舉和心跳(empty AppendEntries calls). 這應該是足夠一個領導人當選,並在出錯的情況下保持領導者。
- 實現RequestVoteArgs和RequestVoteReply結構體,然後修改Make()函數創建一個後臺的goroutine,當長時間接收不到其他節點的信息時開始選舉(通過對外發送RequestVote請求)。爲了能讓選舉工作,你們需要實現RequestVote()請求的處理函數,這樣服務器們就可以給其他服務器投票。
- 爲了實現心跳,你們將會定義一個AppendEntries結構(雖然你們可能用不到全部的從參數),有領導人定期發送出來。你們同時也需要實現AppendEntries請求的處理函數,重置選舉超時,當有領導選舉產生的時候其他服務器就不會想成爲領導。
- 確保定時器在不同的Raft端點沒有同步。尤其是確保選舉的超時不是同時觸發的,否則全部的端點都會要求會自己投票,然後沒有服務器能夠成爲領導。
- 當我們的代碼可以完成領導選舉之後,我們想要使用Raft保存一致,複雜日誌操作。爲了做到這些,我們需要通過Start()讓服務器接受客戶端的操作,然後將操作插入到日誌中。在Raft中,只有領導者被允許追加日誌,然後通過AppendEntries調用通過其他服務器增加新條目。
1、 Raft結構字段填充
首先根據論文來實現Raft結構體,應該包含如下信息:
所有服務器上持久存在的狀態
參數 | 含義 |
---|---|
currentTerm | 服務器最後一次知道的任期號(初始化爲 0,持續遞增) |
votedFor | 在當前獲得選票的候選人的 Id |
log[] | 日誌條目集;每一個條目包含一個用戶狀態機執行的指令和收到時的任期號 |
所有服務器上經常變的狀態
字段 | 含義 |
---|---|
commitIndex | 已知的最大的已經被提交的日誌條目的索引值 |
lastApplied | 最後被應用到狀態機的日誌條目索引值(初始化爲 0,持續遞增 |
在領導人裏經常改變的狀態 (選舉後重新初始化)
字段 | 含義 |
---|---|
nextIndex[] | 對於每一個服務器,需要發送給他的下一個日誌條目的索引值(初始化爲領導人最後索引值加一) |
matchIndex[] | 對於每一個服務器,已經複製給他的日誌的最高索引值 |
Raft結構
type Raft struct {
mu sync.Mutex
peers []*labrpc.ClientEnd
persister *Persister
me int // index into peers[]
// Your data here.
// Look at the paper's Figure 2 for a description of what
// state a Raft server must maintain.
currentTerm int
votedFor int
log []logEntries
commitIndex int
lastApplied int
nextIndex []int
matchIndx []int
state Role
leader int
appendEntriesCh chan bool
voteGrantedCh chan bool
leaderCh chan bool
applyMsgCh chan ApplyMsg
heatbeatTimeout time.Duration
electionTimeout time.Duration
}
通過Make函數初始化創建一個Raft對象
一個服務通過調用Make(peers,me,…)創建一個Raft端點。peers參數是通往其他Raft端點處於連接狀態下的RPC連接,me參數是自己在端點數組中的索引。
狀態機中狀態轉移圖:
所有服務器:
如果接收到的 RPC 請求中,任期號T > currentTerm,那麼就令 currentTerm 等於 T,並切換狀態爲跟隨者。
跟隨者(Follow):
- 響應來自候選人和領導者的請求。
- 如果在超過選舉超時時間的情況之前都沒有收到領導人的心跳,或者是候選人請求投票的,就自己變成候選人。
候選人(Candidate):
在轉變成候選人後就立即開始選舉過程
- 自增當前的任期號(currentTerm)。
- 給自己投票。
- 重置選舉超時計時器。
- 發送請求投票的 RPC 給其他所有服務器。
- 如果接收到大多數服務器的選票,那麼就變成領導人。
- 如果接收到來自新的領導人的附加日誌 RPC,轉變成跟隨者。
- 如果選舉過程超時,再次發起一輪選舉。
領導人(Leader):
- 一旦成爲領導人:發送空的附加日誌 RPC(心跳)給其他所有的服務器;在一定的空餘時間之後不停的重複發送,以阻止跟隨者超時。
// create a new Raft server instance:
func Make(peers []*labrpc.ClientEnd, me int,
persister *Persister, applyCh chan ApplyMsg) *Raft {
rf := &Raft{}
rf.peers = peers
rf.persister = persister
rf.me = me
// Your initialization code here.
rf.currentTerm = STARTTERM
rf.votedFor = VOTENULL
rf.log = make([]logEntries,0)
rf.log = append(rf.log,logEntries{0,0})
rf.commitIndex = 0
rf.lastApplied = 0
rf.state = Follow
rf.nextIndex = make([]int,len(rf.peers))
rf.matchIndx = make([]int,len(rf.peers))
rf.appendEntriesCh = make(chan bool,1)
rf.voteGrantedCh = make(chan bool,1)
rf.leaderCh = make(chan bool,1)
rf.applyMsgCh = applyCh
rf.heatbeatTimeout = time.Duration(HEATBEATTIMEOUT) * time.Millisecond
// initialize from state persisted before a crash
rf.readPersist(persister.ReadRaftState())
go func(){
for {
rf.mu.Lock()
state := rf.state
rf.mu.Unlock()
electionTimeout := HEATBEATTIMEOUT * 2 + rand.Intn(HEATBEATTIMEOUT)
rf.mu.Lock()
rf.electionTimeout = time.Duration(electionTimeout) * time.Millisecond
rf.mu.Unlock()
switch state {
case Follow:
select {
/*
如果在超過選舉超時時間的情況之前都沒有收到領導人的心跳,或者是候選人請求投票的,就自己變成候選人
*/
case <- rf.appendEntriesCh:
case <- rf.voteGrantedCh:
case <- time.After(rf.electionTimeout):
rf.coverToCandidate()
}
case Candidate:
go rf.leaderElection()
select {
case <- rf.appendEntriesCh:
case <- rf.voteGrantedCh:
case <- rf.leaderCh:
case <- time.After(rf.electionTimeout):
rf.coverToCandidate()
}
case Leader:
go rf.broadcastHeartbeat()
time.Sleep(rf.heatbeatTimeout)
}
}
}()
return rf
}
2、領導選舉
請求投票 RPC和返回參數
要開始一次選舉過程,跟隨者先要增加自己的當前任期號並且轉換到候選人狀態。然後他會並行的向集羣中的其他服務器節點發送請求投票的 RPCs 來給自己投票。所以先要實現RequestVoteArgs和RequestVoteReply結構體,即請求投票RPC的參數和反饋結果。
請求投票RPC參數RequestVoteArgs
由候選人負責調用用來徵集選票
參數 | 含義 |
---|---|
term | 候選人的任期號 |
candidateId | 請求選票的候選人的 Id |
lastLogIndex | 候選人的最後日誌條目的索引值 |
lastLogTerm | 候選人最後日誌條目的任期號 |
type RequestVoteArgs struct {
// Your data here.
Term int
CandidateId int
LastLogIndex int
LastLogTerm int
}
請求投票RPC參數返回值RequestVoteReply
參數 | 含義 |
---|---|
term | 當前任期號,以便於候選人去更新自己的任期號 |
voteGranted | 候選人贏得了此張選票時爲真 |
type RequestVoteReply struct {
// Your data here.
Term int
VoteGranted bool
}
發起投票實現
爲了提高性能,需要並行發送RPC。可以迭代peers,爲每一個peer單獨創建一個goroutine發送RPC。Raft Structure Adivce建議:
在同一個goroutine裏進行RPC回覆(reply)處理是最簡單的,而不是通過(over)channel發送回覆消息。
所以,爲每個peer創建一個gorotuine同步發送RPC並進行RPC回覆處理。另外,爲了保證由於RPC發送阻塞而阻塞的goroutine不會阻塞RequestVote RPC的投票統計,需要在每個發送RequestVote RPC的goroutine中實時統計獲得的選票數,達到多數後就立即切換爲Leader狀態,並立即發送一次心跳,阻止其他peer因選舉超時而發起新的選舉。而不能在等待所有發送goroutine處理結束後再統計票數,這樣阻塞的goroutine,會阻塞領導者的產生,且在阻塞的過程中,容易使得達到了選舉超時時間,會進入新的一個週期再次進行發出投票請求。
func (rf *Raft) leaderElection() {
rf.mu.Lock()
if rf.state != Candidate {
rf.mu.Unlock()
return
}
rf.mu.Unlock()
rf.mu.Lock()
args := RequestVoteArgs{
Term:rf.currentTerm,
CandidateId:rf.me,
LastLogIndex:rf.getLastIndex(),
LastLogTerm:rf.getLastTerm(),
}
rf.mu.Unlock()
winThreshold := int64(len(rf.peers)/2 + 1)
voteCount := int64(1)
for i := 0; i < len(rf.peers); i++ {
if i == rf.me {
continue
}
go func(index int,args RequestVoteArgs) {
reply := &RequestVoteReply{}
ok := rf.sendRequestVote(index,args,reply)
if !ok {
rf.mu.Lock()
defer rf.mu.Unlock()
DPrintf("sendRequestVote fail,request term:%d,candidate id: %d,",args.Term,args.CandidateId)
return
}
rf.mu.Lock()
if args.Term != rf.currentTerm {
rf.mu.Unlock()
return
}
rf.mu.Unlock()
if reply.VoteGranted == false {
if reply.Term > rf.currentTerm {
rf.coverToFollow(reply.Term)
}
}else{
atomic.AddInt64(&voteCount,1)
if atomic.LoadInt64(&voteCount) >= winThreshold && rf.state == Candidate {
DPrintf("server %d win the vote",rf.me)
rf.coverToLeader()
chanSet(rf.leaderCh)
}
}
}(i,args)
}
}
接收者實現:
如果term < currentTerm返回 false。
如果 votedFor 爲空或者就是 candidateId,並且候選人的日誌也自己一樣新,那麼就投票給他。
func (rf *Raft)RequestVote(args RequestVoteArgs, reply *RequestVoteReply) {
// Your code here.
voteGranted := false
/*
如果term < currentTerm返回 false
*/
rf.mu.Lock()
if rf.currentTerm > args.Term {
reply.Term = rf.currentTerm
reply.VoteGranted = voteGranted
rf.mu.Unlock()
return
}
rf.mu.Unlock()
/*
如果接收到的 RPC 請求中,任期號T > currentTerm,那麼就令 currentTerm 等於 T,並切換狀態爲跟隨者
T > currentTerm時,當此服務器狀態爲Candidate時,在發起選舉時給自己投票,會將voteFor設置爲自己的id,切換到Follow時,重置voteFor。
T > currentTerm時,當次服務器狀態爲Follow時,則此時服務器沒有給其他節點投過票,如果投過票則currentTerm更新爲最新的,此時重置voteFor也沒問題。
*/
if rf.currentTerm < args.Term {
rf.coverToFollow(args.Term)
}
/*
如果 votedFor 爲空或者就是 candidateId,並且候選人的日誌也自己一樣新,那麼就投票給他
*/
rf.mu.Lock()
if (rf.votedFor == VOTENULL || rf.votedFor == args.CandidateId) && ((rf.currentTerm <= args.Term) || ((rf.getLastIndex() <= args.LastLogIndex) && (rf.currentTerm== args.Term)) ) {
voteGranted = true
rf.votedFor = args.CandidateId
rf.state = Follow
rf.leader = args.CandidateId
chanSet(rf.voteGrantedCh)
}
reply.VoteGranted = voteGranted
reply.Term = rf.currentTerm
rf.mu.Unlock()
}
3、心跳實現
- 發送請求
當狀態轉變爲Leader後,馬上給其他所有的節點發送心跳請求。收到請求的回覆,如果回覆的Term > currentTerm,說明存在一個更大的任期,則轉換成Follow狀態,重置voteFor,更新週期。 - 接收者實現
當接收到的Term < currentTerm時,則返回失敗。否則返回成功,接收到的節點將自己狀態轉換爲Follow。