MIT 6.824 lab 2A 记录

MIT 6.824 lab 2A 记录

前提要求

本部分中,我们需要实现 Raft 的 leader 选举和心跳检测(通过发送 AppendEntriesRPC 请求但是不携带日志条目)。在 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 结构体中。我们可能还定义一个结构体来保存有关每个日志条目的信息。
  • 填充 RequestVoteArgsRequestVoteReply 结构体,尝试修改 Make() 来创建一个后台的 goroutine,使用这个协程在一段时间没有从另一个节点收到消息时,发送 RequestVoteRPC 请求来定期启动 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.Timertime.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先检查有没有投给别人,没有就投给他

参考

www.cnblogs.com/lawliet12/p...

blog.rayzhang.top/2022/11/09/...

相关推荐
斯普信专业组1 小时前
深度解析FastDFS:构建高效分布式文件存储的实战指南(上)
分布式·fastdfs
jikuaidi6yuan2 小时前
鸿蒙系统(HarmonyOS)分布式任务调度
分布式·华为·harmonyos
天冬忘忧3 小时前
Kafka 生产者全面解析:从基础原理到高级实践
大数据·分布式·kafka
天冬忘忧4 小时前
Kafka 数据倾斜:原因、影响与解决方案
分布式·kafka
隔着天花板看星星4 小时前
Kafka-Consumer理论知识
大数据·分布式·中间件·kafka
隔着天花板看星星4 小时前
Kafka-副本分配策略
大数据·分布式·中间件·kafka
金刚猿4 小时前
简单理解下基于 Redisson 库的分布式锁机制
分布式·分布式锁·redisson
我一直在流浪4 小时前
Kafka - 消费者程序仅消费一半分区消息的问题
分布式·kafka
张彦峰ZYF6 小时前
投资策略规划最优决策分析
分布式·算法·金融
processflow流程图8 小时前
分布式kettle调度平台v6.4.0新功能介绍
分布式