MIT 6.824 lab 2A 记录
前提要求
本部分中,我们需要实现 Raft 的 leader 选举和心跳检测(通过发送 AppendEntries
RPC 请求但是不携带日志条目)。在 2A 部分中,我们的目标是选举一个 leader,并且在没有发生故障的情况下使其继续保持 leader,如果发生故障或者老 leader 发送 / 接受的数据包丢失则让新的 leader 接替。最终运行 go test -run 2A
来测试代码。
相关提示:
- 这次的 Lab 中并没有简单的方法直接让我们的 Raft 实现运行起来;我们需要通过测试代码来运行,即
go test -run 2A
。 - 严格按照论文中的图 2 完成 Lab。在这个部分中我们只需要关心发送和接收 Request Vote 相关的 RPC、与选举相关的服务器规则以及与 leader 选举相关的状态。
- 将论文图 2 中用于 leader 选举的状态加入到在
raft.go
中的Raft
结构体中。我们可能还定义一个结构体来保存有关每个日志条目的信息。 - 填充
RequestVoteArgs
和RequestVoteReply
结构体,尝试修改Make()
来创建一个后台的 goroutine,使用这个协程在一段时间没有从另一个节点收到消息时,发送RequestVote
RPC 请求来定期启动 leader 选举。通过这种方式,节点就会了解到如果有 leader 那么当前的 leader 是谁,要么就会自己成为 leader。之后实现RequestVote()
RPC 处理函数,以便服务器投票给别人。 - 为了实现心跳机制,我们需要定义一个名为
AppendEntries
的 RPC 结构体(尽管你可能暂时还不需要所有的参数),并让 leader 定期发送他们。我们需要写一个AppendEntries
的 RPC 处理函数,通过函数来重置选举超时时间,以便其他服务器当选时,其他服务器不会继续竞选 leader。 - 请确保不同的节点不会总在同一时刻发生选举超时,否则有可能所有节点都仅投票给自己,导致没有节点被竞选为 leader。
- 测试要求 leader 发送心跳 RPC 的速度不得超过 10 次每秒。
- 测试要求我们的 Raft 能够在旧 leader 发生故障(前提是大多数节点仍然能相互通信)5 秒内选举出一个新 leader。但是需要注意的是,如果发生分裂投票 split vote(当发送的所有数据包都丢失了或者候选人不巧地选择了相同的随机的回票时间是有可能发生的),leader 选举可能需要多轮投票。因此我们必须要选择一个足够短的超时时间(心跳间隔也是如此),确保即使选举需要多轮,也能够在 5 秒内完成。
- 在论文 5.2 节中提到了选举的超时时间应当在 150 到 300 毫秒之间,前提是 leader 发送心跳的频率远高于每 150 毫秒一次。由于测试程序限制我们一秒最多 10 次心跳,因此我们需要使用比论文中 150 到 300 毫秒更大的选举超时时间,但是也不要设置的太大了,应该很有可能会导致无法在 5 秒内选举出一个新的 leader。
- 学会使用 Go 中的 rand,会在 Lab 中很有用。
- 我们需要完成代码,实现周期性地或者延时执行某些操作。最简单的办法是创建一个 goroutine,在协程的循环中调用
time.Sleep()
(可以参考在Make()
中创建的 goroutineticker()
)。不要使用time.Timer
或time.Ticker
,这两个并不好用,容易出错。 - 指南页面 Guidance page 中有一些如何开发和调试的小建议。
- 如果你的代码不能正常通过测试,请再次阅读论文的图 2;leader 选举的完整逻辑在图中多个部分被提及。
- 不要忘记实现
GetState()
。 - 测试程序会在永久关闭一个实例时调用 Raft 的
rf.Kill()
。我们可以使用rf.killed()
来查看Kill()
是否被调用过了。我们可能需要在所有的循环中都这样做,以避免死亡的 Raft 实例打印出混乱的信息。 - Go RPC 仅发送以大写字母为首的结构体字段。子结构体中也必须具有大写字段名称(例如数组中的日志记录字段)。
labgob
包会警告这一点,不要忽略警告。
Raft结构体的实现和一些辅助函数
go
// serverRole
type ServerRole int
const (
ROLE_Follwer ServerRole = 1
ROLE_Candidate ServerRole = 2
ROLE_Leader ServerRole = 3
)
// A Go object implementing a single Raft peer.
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 ()是否死亡,1表示死亡,0表示还活着
// 2A
// state NodeState // 节点状态
currentTerm int // 当前任期
votedFor int // 给谁投过票
votedCnt int // 得票总数
currentRole ServerRole // 当前role
electionTimer *time.Timer // 选举时间
heartbeatTimer *time.Timer // 心跳时间
heartbeatFlag int // follwer sleep 期间
// Your data here (2A, 2B, 2C).
// Look at the paper's Figure 2 for a description of what
// state a Raft server must maintain.
}
// 获取下次超时时间
func getRandomTimeout() time.Duration {
// 300 ~ 450 ms 的误差
return time.Duration(300+rand.Intn(150)) * time.Millisecond
}
心跳和选举RPC结构体
go
// example RequestVote RPC arguments structure.
// field names must start with capital letters!
type RequestVoteArgs struct {
// Your data here (2A, 2B).
Term int // candidate's term
CandidateId int // candidate global only id
}
// example RequestVote RPC reply structure.
// field names must start with capital letters!
type RequestVoteReply struct {
// Your data here (2A).
Term int // candidate's term
CandidateId int // candidate global only id
VoteGranted bool // true 表示拿到票了
}
type AppendEntriesArgs struct {
Term int
}
type AppendEntriesReply struct {
Term int
}
切换Role
go
// 切换 role
func (rf *Raft) switchRole(role ServerRole) {
// 如果相同直接return
if rf.currentRole == role {
return
}
old := rf.currentRole
rf.currentRole = role
// 投票 重置为-1
if role == ROLE_Follwer {
rf.votedFor = -1
}
fmt.Printf("[SwitchRole] id=%d role=%d term=%d change to %d \n", rf.me, old, rf.currentTerm, role)
}
这里要补充一旦切到follow应该吧VotedFor重置为-1表示还没投
一些初始化工作
go
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 (2A, 2B, 2C).
rf.mu.Lock()
rf.currentTerm = 1
rf.votedFor = -1
rf.currentRole = ROLE_Follwer
rf.heartbeatTimer = time.NewTimer(100 * time.Millisecond)
rf.electionTimer = time.NewTimer(getRandomTimeout())
rf.mu.Unlock()
DPrintf("starting ... %d \n", me)
// initialize from state persisted before a crash
rf.readPersist(persister.ReadRaftState())
// start ticker goroutine to start elections
go rf.ticker()
return rf
}
go
// return currentTerm and whether this server
// believes it is the leader.
func (rf *Raft) GetState() (int, bool) {
rf.mu.Lock()
term := rf.currentTerm
isleader := rf.currentRole == ROLE_Leader
rf.mu.Unlock()
return term, isleader
}
ticker
go
func (rf *Raft) ticker() {
for rf.killed() == false {
// Your code here to check if a leader election should
// be started and to randomize sleeping time using
// time.Sleep().
// 心跳
select {
// leader的心跳时间到了
case <-rf.heartbeatTimer.C:
if rf.currentRole == ROLE_Leader {
rf.mu.Lock()
// leader的心跳方法
rf.leaderHeartBeat()
// 重置定时器
rf.heartbeatTimer.Reset(time.Millisecond * 100)
rf.mu.Unlock()
}
// 选举时间到了
case <-rf.electionTimer.C:
rf.mu.Lock()
switch rf.currentRole {
// follower开始投票
case ROLE_Follwer:
// follow转为 candidate参与选举
rf.switchRole(ROLE_Candidate)
rf.StartElection()
// candidate参与选举
case ROLE_Candidate:
rf.StartElection()
}
rf.mu.Unlock()
}
}
}
这里就是检测leader的心跳和选举两个定时器,然后开始选主时,所有节点的状态由Follower转化为Candidate,并向其他节点发送选举请求。
leader发送心跳
go
// leader发送心跳,检查任期号
func (rf *Raft) leaderHeartBeat() {
for server, _ := range rf.peers {
// 先排除自己
if server == rf.me {
continue
}
go func(s int) { // 给follow发心跳
args := AppendEntriesArgs{}
reply := AppendEntriesReply{}
// 加一下锁
rf.mu.Lock()
args.Term = rf.currentTerm
rf.mu.Unlock()
ok := rf.sendAppendEntries(s, &args, &reply)
if !ok {
fmt.Printf("[SendHeartbeat] id=%d send heartbeat to %d failed \n", rf.me, s)
return
}
rf.mu.Lock()
// leader收到回复的版本号比他自己还大,直接变follow
if reply.Term > args.Term {
rf.switchRole(ROLE_Follwer)
rf.currentTerm = reply.Term
// TODO rf.votedFor = -1
}
rf.mu.Unlock()
}(server)
}
}
func (rf *Raft) sendAppendEntries(server int, args *AppendEntriesArgs, reply *AppendEntriesReply) bool {
ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)
return ok
}
// 发送心跳对应三个角色的执行
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
// 0.优先处理curterm<args.term,直接转化为follow
if rf.currentTerm < args.Term {
rf.switchRole(ROLE_Follwer)
rf.currentTerm = args.Term
rf.heartbeatFlag = 1
// TODO 差异一 没有补 -1
} else
// candidate在相同任期收到,则转化为follow
if rf.currentRole == ROLE_Candidate && rf.currentTerm == args.Term {
rf.switchRole(ROLE_Follwer)
rf.currentTerm = args.Term
rf.heartbeatFlag = 1
// TODO 差异一 没有补 -1
} else if rf.currentRole == ROLE_Follwer {
// follow
rf.heartbeatFlag = 1
}
// leader不处理
reply.Term = rf.currentTerm
}
leader发送心跳就是遍历除了自己的所有节点发送AppendEntries,分节点首先会检查任期号,如果发现自己的任期小于leader,会直接转化为follow,然后同步一样的任期并且把心跳标记一次。然后分情况,如果candidtae在相同任期收到了leader的心跳,会转化为follow,如果是follow收到就标记一次心跳。发送完之后leader会检查回复的版本号,如果比他自己还大,直接变follow
candidta选举
go
// candidta发送给其他的follow去拉票
func (rf *Raft) StartElection() {
// 重置票数和超时时间
rf.currentTerm += 1
rf.votedCnt = 1
rf.electionTimer.Reset(getRandomTimeout())
rf.votedFor = rf.me
rf.persist()
// 遍历每个节点
for server, _ := range rf.peers {
// 先跳过自己
if server == rf.me {
continue
}
// 接下来使用goroutine发送rpc
go func(s int) {
rf.mu.Lock()
args := RequestVoteArgs{
Term: rf.currentTerm,
CandidateId: s,
}
reply := RequestVoteReply{}
rf.mu.Unlock()
ok := rf.sendRequestVote(s, &args, &reply)
if !ok {
fmt.Printf("[StartElection] id=%d request %d vote failed ...\n", rf.me, s)
} else {
fmt.Printf("[StartElection] %d send vote req succ to %d\n", rf.me, s)
}
rf.mu.Lock()
// 处理回复任期更大的问题,直接降级为Follow
if rf.currentTerm < reply.Term {
rf.switchRole(ROLE_Follwer)
rf.currentTerm = reply.Term
rf.mu.Unlock()
return
}
if reply.VoteGranted {
rf.votedCnt++
}
// 这里在缓存一下cnt的值
cnt := rf.votedCnt
role := rf.currentRole
rf.mu.Unlock()
// 票数过半,选举成功
if cnt*2 > len(rf.peers) {
// 这里有可能处理 rpc 的时候,收到 rpc,变成了 follower,所以再校验一遍
rf.mu.Lock()
if rf.currentRole == ROLE_Candidate {
rf.switchRole(ROLE_Leader)
fmt.Printf("[StartElection] id=%d election succ, votecnt %d \n", rf.me, cnt)
role = rf.currentRole
}
rf.mu.Unlock()
if role == ROLE_Leader {
rf.leaderHeartBeat() // 先主动 send heart beat 一次
}
}
}(server)
}
}
投票首先会将任期++,给自己投一票,重置投票时间,遍历每个节点发起投票rpc,同样检查回复的任期。如果票数过半,选举成功,转化为leader并且主动进行一次心跳
follower投票
go
func (rf *Raft) sendRequestVote(server int, args *RequestVoteArgs, reply *RequestVoteReply) bool {
ok := rf.peers[server].Call("Raft.RequestVote", args, reply)
return ok
}
// example RequestVote RPC handler.
func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
// Your code here (2A, 2B).
rf.mu.Lock()
defer rf.mu.Unlock()
// 任期不对,先转化成follow
if rf.currentTerm < args.Term {
rf.switchRole(ROLE_Follwer)
rf.currentTerm = args.Term
rf.votedFor = -1
}
switch rf.currentRole {
case ROLE_Follwer:
// 先看这个follow有没有投票过
if rf.votedFor == -1 {
rf.votedFor = args.CandidateId
reply.VoteGranted = true
} else {
reply.VoteGranted = false
}
case ROLE_Candidate, ROLE_Leader:
reply.VoteGranted = false
}
reply.Term = rf.currentTerm
}
首先先检查任期,然后检查role,如果是candidate或者lead就标记一下没投,是follow先检查有没有投给别人,没有就投给他