MIT 6.824 分佈式課程Lab2 2A Raft領導者選舉和心跳機制

一、Raft選主流程

  1. 當新集羣啓動的時候,所有的機器A、B、C的默認狀態是Follower,所有的機器地址endpoint作爲初始化參數傳入進程。
  2. 如果收到心跳,則作爲Follower開始工作,選主結束。如果超過一段隨機選舉超時時間後(在一定範圍且大於心跳時間), 開始發起Election。隨機的目的是爲了保證不要同時發起Election,在少數情況下可能會發生同時發起選舉情況。
  3. 集羣初始化時沒有Leader,所以開始所以的機器沒有收到心跳,機器A開始發起選舉。首先機器A把自己的身份置爲Candidate,把Term自增1,並且自己投票給自己。
    - 當其他機器B接受到這個vote投票請求,如果發現Candidate的Term小於自己,則否決,並返回自己的Term。
    - 如果機器B發現Candidate的Term大於自己,就更新Term,並且重置投給誰votedFor爲A。
    - 如果機器B發現當前Term已經給別人投票過,就否決。
    - 如果 Candidate的日誌不是最新,就否決。
    - 贊成
    
  4. 機器A在發送vote請求後,如果發現大多數贊成,則跳到步驟7,成爲Leader。
  5. 如果發現返回的Term大於自己,就放棄本次Election,更新自己的term到返回的term,重置自己的狀態爲Follower,重新開始步驟3。
  6. 如果機器A在投票結束,沒有收到大多數贊成票,則返回步驟3。
  7. 機器A選主成功, 調整自身狀態爲Leader, 並且立即發送一次心跳(避免其他機器超時進入選主狀態)。
  8. 週期性發送心跳, 保證Leader地位。

跟隨者只響應來自其他服務器的請求。如果跟隨者接收不到消息,那麼他就會變成候選人併發起一次選舉。獲得集羣中大多數選票的候選人將成爲領導者。領導人一直都會是領導人直到自己宕機了。

二、代碼實現

  1. 根據論文填充Raft結構字段。
  2. 添加實現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參數是自己在端點數組中的索引。
  1. 實現領導選舉和心跳(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。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章