mit6.824 lab2a 全代码


看参考贴大佬们都是一两天就把lab2a做出来了,我用了两周(哭晕在厕所),知道自己菜,没想到能菜到这种程度。

之前一直参考一篇简单的经验贴,有代码结构,没有锁,没有term改变,上手是很快就上手了,但是事后全是坑,调了两周,哎。(找经验贴一定要找全代码的,而且要看有过了测试结果的),但是确实是让我对代码的整体结构有了了解。

最后一天换了一篇经验贴参考,一天完事。撒花

lab2a我觉得三大难点:锁,整体架构,细节多

1 参考资料

2 代码

  • 首先是用到的结构体(或参数,分为4部分):
//1: 选举心跳时间
var HeartBeatTimeout = 100 * time.Millisecond

//2: raft结构
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 *raft.Persister     // Object to hold this peer's persisted state
	me        int                 // this peer's index into peers[]
	dead      int32               // set by Kill()

	status            string    //状态
	term              int       //第几轮投票
	voteFor           int       //为谁投票,-1表示还没投票
	voteCount         int       //获得总票数,初始为0

	overtime time.Duration //任期倒计时总长
	timer    *time.Ticker //实现倒计时功能

	// Your data here (2A, 2B, 2C).
	// Look at the paper's Figure 2 for a description of what
	// state a Raft server must maintain.

}


//3:RequestVote的参数
type RequestVoteArgs struct {
	// Your data here (2A, 2B).
	Term        int
	CandidateId int
}



type RequestVoteReply struct {
	// Your data here (2A).
	Term        int
	VoteGranted bool
}

//4: AppendEntries的参数
type AppendEntriesArgs struct {
	Term     int
	LeaderId int
}

type AppendEntriesReply struct {
	Term    int
	Success bool
}


这里的倒计时是用time.Ticker+select语句实现的(实验里说不建议),不太了解的百度先看一下

  • 然后是入口函数make()
func Make(peers []*labrpc.ClientEnd, me int,
	persister *raft.Persister, applyCh chan ApplyMsg) *Raft {
	rf := &Raft{}
	rf.peers = peers
	rf.persister = persister
	rf.me = me

	// Your initialization code here (2A, 2B, 2C)

	rf.status = "follower"
	rf.term = 0

	//初始化选举时间倒计时
	rf.overtime = time.Duration(150+rand.Intn(200)) * time.Millisecond
	rf.timer = time.NewTicker(rf.overtime)

	rf.voteFor = -1
	rf.voteCount = 0

	// initialize from state persisted before a crash
	//rf.readPersist(persister.ReadRaftState())

	// start ticker goroutine to start elections
	go rf.ticker()

	return rf
}
  • 然后是ticker()函数:
func (rf *Raft) ticker() {

	time.Sleep(1 * time.Millisecond) //让测试标题输出比这里面的输出早

	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 {
		case <-rf.timer.C:
			rf.mu.Lock()
			//(坑1)defer rf.mu.Unlock() 这样写,一直无法释放锁,应该写到select 语句最后

			switch rf.status {
			//1: follower先将自己的状态变为candidate,
			case "follower":
				rf.status = "candidate"
				fallthrough
			case "candidate": 
				//2 然后为为自己投票,总票数加1,
				rf.term++
				rf.overtime = time.Duration(150+rand.Intn(200)) * time.Millisecond
				rf.timer.Reset(rf.overtime)

				rf.voteFor = rf.me
				rf.voteCount = 1 //(坑2)rf.voteCount++错误,进入下一轮的candidate会在原来基础上加1
				fmt.Printf("(轮数:%d)candidate!!!		第%d号任务已经进入candidate状态\n", rf.term, rf.me)

				for i := 0; i < len(rf.peers); i++ {
					if i == rf.me {
						continue
					}
					reply := RequestVoteReply{}
					fmt.Printf("发起投票! 	第%d号任务(term:%d)向第%d号任务发起投票\n", rf.me, rf.term, i)
					//以线程的方式发起投票,前面参考文献里面关于锁那个部分提到了
					go rf.sendRequestVote(i, &RequestVoteArgs{Term: rf.term, CandidateId: rf.me}, &reply)
				}
				//3: leader 发起心跳
			case "leader":
				rf.timer.Reset(HeartBeatTimeout)
				for j, _ := range rf.peers {
					if j == rf.me {
						continue
					}

					reply := AppendEntriesReply{}
					go rf.sendAppendEntries(j, &AppendEntriesArgs{Term: rf.term, LeaderId: rf.me}, &reply)
				}

			}
			rf.mu.Unlock()
		}

	}

}

这里的锁的释放是个小细节,一定要放在select语句后面,而不是直接defer rf.mu.Unlock()

否则会让ticker()函数一直放不了锁,导致其他线程无法跑起来。在第三部分我会贴出我的bug截图

  • 请求投票函数sendRequestVote():
func (rf *Raft) sendRequestVote(server int, args *RequestVoteArgs, reply *RequestVoteReply) bool {

	//一直发送请求,直到成功
	ok := rf.peers[server].Call("Raft.RequestVote", args, reply)
	for ok == false {
		ok = rf.peers[server].Call("Raft.RequestVote", args, reply)
	}

	//一把大锁
	rf.mu.Lock()
	defer rf.mu.Unlock()

	//情况1: 收到过期的RPC回复,不处理
    //(这里很重要,因为可能发送了多次rpc,但是因为网络延迟导致每收到及时的rpc回复,等到下一轮选举投票的时候却收到了)
	if args.Term < rf.term {
		return false
	}

	//情况2: 允许投票,
	if reply.VoteGranted == true {
		fmt.Printf("	同意投票! 	%d号任务(term:%d) 同意给	%d号任务(term:%d) 投票\n", server, rf.term, rf.me, reply.Term)
		if rf.voteCount < len(rf.peers)/2 {
			rf.voteCount++
		}

		if rf.voteCount >= len(rf.peers)/2 {
			fmt.Printf("新leader!!     第%d号任务已经有选票%d,已经进入leader状态\n", rf.me, rf.voteCount)
			rf.status = "leader"
			rf.timer.Reset(HeartBeatTimeout)
		}

	} else {
		//情况3: 不允许投票,
		fmt.Printf("	拒绝投票! 	%d号任务(term:%d) 拒绝给	%d号任务(term:%d) 投票\n", server, rf.term, rf.me, reply.Term)
		if reply.Term > rf.term {
			rf.status = "follower"
			rf.term = reply.Term

			rf.voteFor = -1
			rf.voteCount = 0
			rf.timer.Reset(time.Duration(150+rand.Intn(200)) * time.Millisecond)
		}
	}

	return true
}

//情况1: 收到过期的RPC回复,不处理

if args.Term < rf.term { return false }

这一句代码要好好理解

因为测试2,3会将raft断开,所以请求RequestVote得不到回复,raft恢复后,旧的请求RequestVote可能会得到回复,这类回复直接抛弃-

  • 第一个rpc函数RequestVote():
// example RequestVote RPC handler.
func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
	// Your code here (2A, 2B).
	
	//一把大锁
	rf.mu.Lock()
	defer rf.mu.Unlock()
	//fmt.Printf("收到投票请求!	%d号任务(term:%d)收到来自%d号任务(term:%d)\n", rf.me, rf.term, args.CandidateId, args.Term)

	
	//默认不同意投票
	reply.VoteGranted = false 
	reply.Term = rf.term

	//情况1: 请求投票的raft的term太小,不给投票
	if args.Term < rf.term {
		return
	}
	
	//情况2: 请求投票的raft的term大 将自己的term更新,如果是自己是candidate状态,转为follower
	if args.Term > rf.term { 
		rf.term = args.Term

		//这里本来有投票的代码实现,放在了下面的情况3一起处理
		rf.voteFor = -1
		rf.voteCount = 0

		if rf.status == "candidate" {
			rf.status = "follower"
		}

	}

	//情况3: 请求投票的raft的term和自己相等情况下,如果rf.voteFor == -1,则投票
	if rf.voteFor == -1 || rf.voteFor == args.CandidateId {

		reply.VoteGranted = true
		rf.term = args.Term

		rf.voteFor = args.CandidateId

		rf.overtime = time.Duration(150+rand.Intn(200)) * time.Millisecond
		rf.timer.Reset(rf.overtime)//重新设置投票倒计时
	}
}

这里的 rf.voteFor == args.CandidateId 条件,是论文中给出,删除也可以过测试

放在这里,我猜测是为了处理,同一个raft请求了多次该raft投票,但是第一次的回复因为网络原因导致,请求raft没有收到,所以重发的请求。

这里临时想到一个问题:重复发送的投票请求,若请求raft先后都收到了回复,那么应该记一票,去重处理代码里没有做!(有机会再做吧,现在的思路:做一个标识的数组来标识已经收到的raft回复)

  • 发送心跳函数 sendAppendEntries():
func (rf *Raft) sendAppendEntries(server int, args *AppendEntriesArgs, reply *AppendEntriesReply) bool {
	//ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)
	//return ok

	
	//一直请求到成功
	ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)
	for ok == false {
		ok = rf.peers[server].Call("Raft.AppendEntries", args, reply)
	}

	//一把大锁
	rf.mu.Lock()
	defer rf.mu.Unlock()

	//情况1: 收到过期的rpc回复
	if args.Term < rf.term {
		return false
	}

	//情况2: 心跳不允许
	if reply.Success == false {
		if reply.Term > rf.term {
			rf.status = "follower"
			rf.term = reply.Term

			rf.voteFor = -1
			rf.voteCount = 0
			rf.overtime = time.Duration(150+rand.Intn(200)) * time.Millisecond
			rf.timer.Reset(rf.overtime)
		}
	}

	fmt.Printf("心跳回复!		%d号任务给%d号任务发送心跳,结果:%v\n", rf.me, server, reply.Success)

	return true
}

//情况1: 收到过期的rpc回复 if args.Term < rf.term { return false }

这里和sendRequestVote()函数一样,丢弃过期的rpc回复

  • 第二个rpc函数 AppendEntries():
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {

	rf.mu.Lock()
	defer rf.mu.Unlock()
	// Your code here (2A, 2B).
	//	fmt.Printf("收到心跳!	%d号任务(term:%d)收到来自%d号任务(term:%d)的心跳!,\n", rf.me, rf.term, args.LeaderId, args.Term)
	//情况1: 收到的rpc的term太旧
	if args.Term < rf.term {
		reply.Term = rf.term
		reply.Success = false

	} else {
		//情况2: 收到的rpc的term 比自己的term大或相等
		rf.status = "follower"
		rf.term = args.Term

		reply.Success = true
		rf.term = args.Term

		rf.overtime = time.Duration(150+rand.Intn(200)) * time.Millisecond
		rf.timer.Reset(rf.overtime)

		rf.voteCount = 0
		rf.voteFor = -1
	}

}
  • 最后一个函数GetState():测试中要用到
func (rf *Raft) GetState() (int, bool) {
	rf.mu.Lock()
	defer rf.mu.Unlock()

	var term int
	var isleader bool
	// Your code here (2A).
	term = rf.term
	isleader = rf.status == "leader"
	return term, isleader
}

3 bug解决

程序跑的时候,出现下列情况

在这里插入图片描述

  • 任务1没有经过第一轮直接进入了第二轮投票

原因:0号任务给1号发起投票后,这时候,0号任务的term是1,而1号任务term是0,直接把1号任务的term变成了1。1号任务再进入candidate就变成了2

解决:正常情况不是bug

  • 1号任务已经执行了requestVote() 但是,0号任务并没有记上票,对sendRequestVote() 打断点,发现进不到执行对reply操作的代码,

原因:可能是sendRequestVote发生了死锁,

解决:

select {
		case <-rf.timer.C:
			//2 follower先将自己的状态变为candidate,然后为为自己投票,总票数加1,
			rf.mu.Lock()
			defer rf.mu.Unlock()

			switch rf.status {

			case "follower":
				rf.status = "candidate"
				fallthrough
			case "candidate":
				rf.term++
				rf.overtime = time.Duration(150+rand.Intn(200)) * time.Millisecond
				rf.timer.Reset(rf.overtime)

				rf.voteFor = rf.me
				rf.voteCount = 1 //rf.voteCount++错误,率先进入第三轮选举就会被当选
				fmt.Printf("(轮数:%d)candidate!!!		第%d号任务已经进入candidate状态\n", rf.term, rf.me)

				for i := 0; i < len(rf.peers); i++ {
					if i == rf.me {
						continue
					}
					reply := RequestVoteReply{}
					fmt.Printf("发起投票! 	第%d号任务(term:%d)向第%d号任务发起投票\n", rf.me, rf.term, i)
					go rf.sendRequestVote(i, &RequestVoteArgs{Term: rf.term, CandidateId: rf.me}, &reply)
				}
			case "leader":
				rf.timer.Reset(HeartBeatTimeout)
				for j, _ := range rf.peers {
					if j == rf.me {
						continue
					}

					reply := AppendEntriesReply{}
					go rf.sendAppendEntries(j, &AppendEntriesArgs{Term: rf.term, LeaderId: rf.me}, &reply)
				}

			}
		}

这里的 defer rf.mu.Unlock() 写在了 for rf.killed() == false 里,导致主线程一直没有释放锁,

解决:rf.mu.Unlock() 把这个写在select 语句结束

			select {
		case <-rf.timer.C:
			//2 follower先将自己的状态变为candidate,然后为为自己投票,总票数加1,
			rf.mu.Lock()
			//defer rf.mu.Unlock() 这样写,一直无法释放锁,应该写到select 语句最后

			switch rf.status {

			case "follower":
				rf.status = "candidate"
				fallthrough
			case "candidate":
				rf.term++
				rf.overtime = time.Duration(150+rand.Intn(200)) * time.Millisecond
				rf.timer.Reset(rf.overtime)

				rf.voteFor = rf.me
				rf.voteCount = 1 //rf.voteCount++错误,率先进入第三轮选举就会被当选
				fmt.Printf("(轮数:%d)candidate!!!		第%d号任务已经进入candidate状态\n", rf.term, rf.me)

				for i := 0; i < len(rf.peers); i++ {
					if i == rf.me {
						continue
					}
					reply := RequestVoteReply{}
					fmt.Printf("发起投票! 	第%d号任务(term:%d)向第%d号任务发起投票\n", rf.me, rf.term, i)
					go rf.sendRequestVote(i, &RequestVoteArgs{Term: rf.term, CandidateId: rf.me}, &reply)
				}
			case "leader":
				rf.timer.Reset(HeartBeatTimeout)
				for j, _ := range rf.peers {
					if j == rf.me {
						continue
					}

					reply := AppendEntriesReply{}
					go rf.sendAppendEntries(j, &AppendEntriesArgs{Term: rf.term, LeaderId: rf.me}, &reply)
				}

			}
			rf.mu.Unlock()
		}

4 贴个测试结果

  • 测试1:
    在这里插入图片描述

  • 测试2:

在这里插入图片描述

  • 测试3:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v5OE1MEW-1667568700490)(C:\Users\cxc\AppData\Roaming\Typora\typora-user-images\image-20221104212542065.png)]

5 反思

lab2a对锁的使用,让我着实学到了不少。几乎踩完所有坑后,才看到官方文档里有关于锁的建议,然后觉得总结得是真的好,我全踩了一遍。

明天继续干lab2b说是比lab2a还酸爽。奥利给!!!

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值