MIT6.824 2020 Lab2 A Raft Leader Election


Preparation

  1. 實驗:http://nil.csail.mit.edu/6.824/2020/labs/lab-raft.html 的 Part 2A.

  2. 論文:

    1. 英文版:https://raft.github.io/raft.pdf
    2. 中文版: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 的時候,開始進行選舉:
    1. 遞增當前的 term;
    2. 投票給自己;
    3. 重置選舉超時計時器;
    4. 發送 RequestVote RPC 給其他的服務器。
  • 選舉的終止條件以及對應操作:
    1. 如果在選舉過程中收到大多數的選票,則將自身狀態變成 leader。
    2. 如果從新的 leader 接收到了 AppendEntries RPC(心跳檢測),則將自身狀態變成 follower。
    3. 如果選舉超時,則重新進行新一輪的選舉。

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

https://tva1.sinaimg.cn/large/008i3skNgy1gu5lxgle37j60h60i675802.jpg

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.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM