1. 程式人生 > 其它 >MIT 6.824 Lab2A Raft之領導者選舉

MIT 6.824 Lab2A Raft之領導者選舉

實驗準備

  1. 實驗程式碼:git://g.csail.mit.edu/6.824-golabs-2021/src/raft
  2. 如何測試:go test -run 2A -race
  3. 相關論文:Raft Extended Section 5.2
  4. 實驗指導:6.824 Lab 2: Raft (mit.edu)

實驗目標

實現Raft演算法中Leader Election(RequestVote RPC)和Heartbeats(AppendEntries RPC)。確保只有一個Leader被選中,且若無錯誤該Leader會一直唯一存在,當該Leader下線或發生其他錯誤導致發出的資料無法被成功接收,則會產生新的Leader來替代。

一些提示

  1. 參考論文的Figure 2實現相應的結構體和函式。
  2. 通過Make()建立一個後臺goroutine,用於一段時間(election timeout)沒收到其他節點的訊息時,通過RequestVote RPC發起選舉。
  3. 儘量保證不同節點的election timeout不會讓他們在同一時間發起選舉,避免所有節點只為自己投票,可以通過設定隨機的election timeout來實現。
  4. 測試要求Hearbeats頻率每秒不高於10次。
  5. 測試要求New Leader在Old Leader下線後5秒內出現,考慮到一次換屆多輪選舉的情況(提示3的情況),election timeout應當足夠短。
  6. 論文中對於election timeout設定在150ms - 300ms之間,前提是Heartbeat頻率遠遠超過150ms一次。由於提示4的限制,實驗中election timeout應該更大。
  7. 推薦使用time.Sleep()而不是time.Timertime.Ticker來實現定期或延遲行為。
  8. 不要忘記實現GetState()
  9. 使用rf.killed()判斷測試是否關閉了該節點。
  10. RPC相關結構欄位都應使用大寫字母開頭,這和Go語言的語法有關。

Raft簡介

日誌被理解為來自客戶端的請求序列,在一個叢集中,有唯一的一個節點用於接收客戶端請求,稱為"Leader Node",為了保證資料的安全性,"Leader Node"的日誌應該複製給若干個節點用於備份,稱為"Follower Node"。"Follower Node"的日誌需要和"Leader Node"保持一致,Raft就是一種為了管理日誌複製而產生的一致性演算法。

領導者選舉

Raft叢集通常有奇數個節點,設為N,叢集允許N/2個節點失效,在正常情況下,叢集有1個Leader和N-1個Follower組成,當Leader失效時,會產生除了Leader和Follower外的第三種身份:Candidate。

Follower在election timeout後,身份轉換為Candidate,在獲取(N-1)/2個其他節點的選票後,身份轉換為Leader。


主要結構

首先是Raft結構,具體的屬性在論文的Figure 2中已經給出,此外還需要額外的兩個屬性。

  1. role:當前節點的身份。
  2. lastRecv:上一次收到其他節點訊息的時間。

被註釋的欄位在Part A中可以忽略,且在本小節中,為了方便理解,請先忽略currentTerm欄位。

type Raft struct {
	mu          sync.Mutex
	peers       []*labrpc.ClientEnd	// 叢集中所有的節點
	// persister   *Persister
	me          int			// 當前節點在peers中的索引
	dead        int32		// 標記當前節點是否存活
    
	lastRecv    time.Time
	role        Role
    
	currentTerm int
	votedFor    int
	// log         []LogEntry
    
	// commitIndex int
	// lastApplied int
    
	// nextIndex   []int
	// matchIndex  []int
}

超時選舉

如果當前節點不是Leader,且超過election timeout未收到其他節點的訊息,則發起選舉。

此處設定election timeout在150ms - 300ms之間。

func (rf *Raft) electionTimeout() time.Duration {
    return time.Duration(150 + rand.Int31n(150)) * time.Millisecond
}

發起選舉後,身份轉換為Candidate,並通過RequestVote RPC獲取其餘節點的選票。在獲取(N-1)/2個其他節點的選票後,身份轉換為Leader。成為Leader後,需要立即向其餘節點發送心跳,宣告自己的存在。

程式碼中的註釋即論文Figure 2中的邏輯。

func (rf *Raft) elect() {
    for !rf.killed() {
        if rf.role == Leader || time.Since(rf.lastRecv) < rf.electionTimeout() {
            return
        }
        /* On conversion to candidate, start election. */
        rf.role = Candidate
        /* Vote for self. */
        rf.voteFor = rf.me
        voteCount := 1
        /* Reset election timer. */
        rf.lastRecv = time.Now()
        /* Send RequestVote RPCs to all other servers. */
        for i, peer := range rf.peers {
            if i == rf.me {
                continue
            }
            reply := RequestVoteReply{}
            peer.Call("Raft.RequestVote", &RequestVoteArgs{
                CandidateId: rf.me,
            }, &reply)

            if reply.VoteGranted {
                voteCount++
            }
        }
        /* If votes received from majority of servers: become leader. */
        if voteCount > len(rf.peers)/2 {
            rf.role = Leader
            rf.votedFor = -1
            rf.heartbeat()
        }
        time.Sleep(10 * time.Millisecond)
    }
}

Explain 1:如何理解rf.role == Leader

Follower和Candidate都可以參加選舉,Candidate可以參加的原因在於,選出一個Leader可能不止一輪選舉,假設非常不幸,所有節點都在同一時刻發起選舉,他們都把自己的選票投給了自己,那麼本輪選舉將無法選出Leader。

這時候將開啟第二輪選舉,因此不能限制只有Follower可以參與選舉。

傳送心跳

不攜帶日誌的日誌複製即心跳,Leader通過心跳重新整理其餘節點的election timeout。Hint 4限制了心跳頻率在每秒10次,因此這裡讓心跳一次後休眠100ms。

func (rf *Raft) heartbeatInterval() {
    return 100 * time.Millisecond
}

func (rf *Raft) heartbeat() {
    for !rf.killed() {
        if rf.role != Leader {
            return
        }
        for i, peer := range rf.peers {
            if i == rf.me {
                continue
            }
            reply := AppendEntriesReply{}
            peer.Call("Raft.AppendEntries", &AppendEntriesArgs{}, &reply)
        }
    }
    time.Sleep(rf.heartbeatInterval())
}

RPC全稱Remote Procedure Call,即遠端過程呼叫,通俗的講就是呼叫其他節點上的函式。例如peer.Call("Raft.AppendEntries", &args, &reply),就是呼叫了對應節點的AppendEntries函式,引數是args,返回值儲存在reply中。

heartbeat是主動,AppendEntries是被動。elect是主動,RequestVote是被動。

RequestVote

Candidate通過遠端呼叫RequestVote,向其他節點索要選票。論文的Figure 2中也給出了RequestVoteArgs和RequestVoteReply的定義。在Part A中不需要關注LastLogIndex和LastLogTerm兩個欄位。同樣的,為了方便理解,請先忽略Term的概念。

type RequestVoteArgs struct {
	Term         int
	CandidateId  int
	// LastLogIndex int
	// LastLogTerm  int
}

type RequestVoteReply struct {
	Term        int
	VoteGranted bool
}

在一輪投票中,每個節點只有一張選票,Candidate會投給自己,而Follower會投給第一個向他索要選票的Candidate。

程式碼中的註釋即論文Figure 2中的邏輯。

RequestVote中需要重新整理election timeout,一次換屆多輪選舉的情況。

func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
    reply.VoteGranted = false
    rf.lastRecv = time.Now()
    /* If votedFor is null or candidateId, grant vote. */
    if rf.votedFor == -1 || rf.votedFor == args.CandidateId {
        rf.votedFor = args.CandidateId
        reply.VoteGranted = true
    }
}

Explain 2:如何理解rf.votedFor == args.CandidateId

邏輯上來說這個條件是沒必要的,去掉這個條件依舊能通過所有測試。我猜測這個條件是為了防止回覆的網路包丟失,傳送方重傳,因此需要接收方再次投出選票。

AppendEntries

Leader通過AppendEntries遠端呼叫,重新整理其他節點的election timeout,保證在自己存活期間,不會有其他節點發起選舉。論文的Figure 2中也給出了AppendEntriesArgs和AppendEntriesReply的定義。

被註釋的欄位在Part A中不需要關注,同樣的,為了方便理解,請先忽略Term欄位。

type AppendEntriesArgs struct {
	Term         int
	// LeaderId     int
	// PrevLogIndex int
	// PrevLogTerm  int
	// Entries      []LogEntry
	// LeaderCommit int
}

type AppendEntriesReply struct {
	Term    int
	Success bool
}

Hint 2中,收到其他節點的訊息,就重新整理election timeout。因此RequestVote和AppendEntries中都需要更新rf.lastRecv

func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
    reply.Success = true
    rf.lastRecv = time.Now()
}

唯一的Leader

競選成功的條件為voteCount > len(rf.peers)/2且每個節點只有一張選票,這保證了最多隻有一個節點達到競選成功條件,保證了Leader的唯一性。

現在考慮唯一的Leader因為某些網路問題導致Leader的心跳無法發出,那麼剩餘的N-1個節點將會選出新的Leader,剩餘的N-1個節點可以繼續提供正常的服務。那麼如果Old Leader的網路問題因為某些原因恢復了,整個叢集將同時出現兩個Leader,這樣整個叢集的日誌的一致性就不能保證。

引入任期

Term(任期)解決了可能出現多個Leader的問題。

Term是一個單調遞增的整型值,所有節點的Term應保持一致,Term的自增只發生在從Follower到Candidate的轉換中,即只有選舉的時候,Term才會自增1。

再次考慮上面的問題,當Old Leader恢復後,由於剩餘的N-1個節點又經歷了至少一次選舉,因此剩餘的N-1個節點包括New Leader的Term都大於Old Leader的Term,Raft演算法規定,任意節點感知到Term更高的節點,將轉換為Follower;任意節點感知到Term更低的節點,將忽略對方的訊息,並告知對方自己的Term。

這樣,當Old Leader收到New Leader的更高Term的心跳時,會將自己的身份轉換為Follower,保證了Leader的唯一性。

什麼是感知到其他節點?

AppendEntries或RequestVote兩個RPC的請求或回覆中都包含Term,Old Leader感知到New Leader有兩種途徑。

  1. 收到New Leader的心跳,發現AppendEntriesArgs.Term更高。
  2. 向New Leader或其餘節點發送心跳,發現AppendEntriesReply.Term更高。

實驗總結

上面的圖片就是本文多次提及的論文的Figure 2,我用綠色的線框選了Part A需要實現的部分。

引入Term後的程式碼本文就不再給出了,參照圖片補充剩餘的實現就可以,需要注意的是,本文為了簡潔程式碼,省略了資料同步問題,-race可以暴露出你程式碼的data race問題,記得為臨界資源上鎖。

最後,為了證明我不是在亂寫,附上我的測試結果。