mit6.824-lab2-2A和2B

资料

实验官网:http://nil.csail.mit.edu/6.824/2018/labs/lab-raft.html

论文翻译:https://www.infoq.cn/article/raft-paper/

视频讲解:https://www.bilibili.com/video/BV1TW411M7Fx?from=search&seid=6824636064276951860

动画模拟:https://raft.github.io/

实验参考:

  • https://blog.csdn.net/Miracle_ma/article/details/80030610
  • https://segmentfault.com/a/1190000020893300
  • https://www.jianshu.com/p/01d4c1bd5c65

Part 2A

Implement leader election and heartbeats (AppendEntries RPCs with no log entries).

重点阅读论文的 5.1 以及 5.2,结合 Figure 2 食用。

字段定义,Figure2都有。

最重要的就是实现三个状态的转换,我使用状态机来实现。发现使用select实现有限状态机真的很方便。打印log的时候记得精确到ms。

在这里插入图片描述

// 运行状态机, Figure 4
// 暂时没有考虑锁,但是通过了2A
func (rf *Raft)runFSM() {
	for {
		state := rf.state
		switch state {
		case Follower:
			rf.electionTimer.Reset(randDuration())
			select {
			// 1 Follower -> Candidate:timeout, start election
			case <- rf.electionTimer.C:
				DPrintf("P%d time out", rf.me)
				rf.state = Candidate
			// leader的心跳
			case term := <- rf.highTerm:
				DPrintf("P%d get leader's heartbeat", rf.me)
				rf.currentTerm = term
			}
		case Candidate:
			DPrintf("P%d start election", rf.me)
			rf.startElection()
			select {
			// 1 Candidate -> Leader : 获得了大多数票
			case <- rf.becomeLeader:
				rf.state = Leader
				rf.electionTimer.Stop()
				DPrintf("P%d become leader\n", rf.me)

			// 2 Candidate -> Candidate: timeout, new election
			case <- rf.electionTimer.C:
				DPrintf("P%d election timeout\n", rf.me)
				// TODO

			// 3 Candidate -> Follower : 发现了新的leader或new term
			case term := <-rf.highTerm:
				DPrintf("P%d find a high term\n", rf.me)
				rf.currentTerm = term
				rf.votedFor = -1
				rf.state = Follower
			}
		case Leader:
			// 发送心跳
			rf.broadcast()
			DPrintf("P%d BH", rf.me)
			rf.heartBeatTimer.Reset(heartBeatInterval)
			// 1 Leader->Follower: 发现了更高的term
			select {
			case term := <- rf.highTerm:
				rf.currentTerm = term
				rf.votedFor = -1
				rf.state = Follower
			// 心跳到时
			case <- rf.heartBeatTimer.C:
			}
		}
		if state != rf.state {
			DPrintf("P%d change state: %s -> %s\n", rf.me, stateName[state], stateName[rf.state])
		}
	}

}

Part2B

实现Raft.Start函数,参考论文第 5.3 和 5.4.1 节。

参考:

  • https://blog.csdn.net/guidao13/article/details/88070273
  • https://www.cnblogs.com/mignet/p/6824_Lab_2_Raft_2B.html

matchIndex和nextIndex

  • matchIndex:已经同步的位置,作用是用来更新commitIndex
  • nextIndex:下一个需要复制的位置

matchIndex和nextIndex在什么时候发生变化?

答:当收到对方同步成功的reply后,需要更新matchIndex和nextIndex。两者的值相差1

什么时候两者的值不一样:

答:当新的Leader上任后,nextIndex是一个乐观估计(其他节点都有我的日志),初始化为rf.log.len;matchIndex是保守估计(其他节点的日志是空的),初始化为0。

TestFailAgree2B

这个测试案列,调了好久。我一开始认为Follower掉线重连后,会继续追随原来的Leader。但是发现Follower的任期是远远大于Leader的,用动画模拟后,知道了Leader的任期也会增加,转变成Follower->Candidate。然后开始新的一轮的选举。

TestFailNoAgree2B

Raft认为任期和index都一样的日志,那么其内容也是一样的。

假设是P2,P3,P4掉线,重连后会有两种结果:

  1. Leader是P0或者P1,那么状态机就会有cmd=20这条命令
  2. Leader是P2 or P3 or P4,那么P0,P1就需要删除cmd=20这条命令

就是完善AppendEntries的第3条。

1. 如果 leader 的任期小于自己的任期返回 false(5.1)
2. 如果自己不存在索引、任期和 prevLogIndex、 prevLogItem
匹配的日志返回 false(5.3)
3. 如果存在一条日志索引和 prevLogIndex 相等,
但是任期和 prevLogItem 不相同的日志,
需要删除这条日志及所有后继日志。(5.34. 如果 leader 复制的日志本地没有,则直接追加存储。
5. 如果 leaderCommit>commitIndex,
设置本地 commitIndex 为 leaderCommit 和最新日志索引中
较小的一个。
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {}

需要测试多次,可以编写简单的测试脚本

export "GOPATH=/home/book/Develop/GOPATH/src/6.824"
for ((i=0;i<10;i++))
do
    go test -run $1
done

分析下数据成员的安全性

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[]

	// Your data here (2A, 2B, 2C).
	// Look at the paper's Figure 2 for a description of what
	// state a Raft server must maintain.
	// 2A
	currentTerm   int        // server 存储的最新任期
	votedFor      int        // 当前任期接受到的选票的候选者 ID
	state         State      // 节点的状态
	electionTimer *time.Timer // 选举定时器
	heartBeatTimer *time.Timer // use when it become Leader
	highTerm	chan int // 发现了更高的term
	becomeLeader chan bool
	// 2B
	log []LogEntry
	commitIndex int // 已知的提交的日志记录的索引(初值为 0 且单调递增)
	// Leader的可变状态
	nextIndex  []int // 每台机器在数组占据一个元素,元素的值为下条发送到该机器的日志索引 (初始值为 leader 最新一条日志的索引 +1)
	matchIndex []int // 每台机器在数组中占据一个元素,元素记录已经复制给该机器日志的索引的。
	// for test
	lastApplied int // 已经被提交到状态机的最后一个日志的索引(初值为0 且单调递增)
	applyCh chan ApplyMsg
}
fieldreadwrite结论
currentTermGetState;RequestVote ;AppendEntriesFSM+
votedForRequestVoteFSM+
stateGetState …FSM+
logAppendEntries ; RequestVoteLeader:Start
Follower:AppendEntries
+
commitIndexAppendEntriesLeader:每次请求后;
Follower:AppendEntries
-
nextIndexheartBeat,Start;
访问的数据不一样
-
matchIndexheartBeat,Start-
lastAppliedLeader在每次心跳请求后,
都会更新。
+

TestRejoin2B

假设三任Leader分别为A,B,C,在第二任Leader断线、第一任Leader重连时,他们的日志为

// 我初始化有个空日志
A	[{<nil> 0} {101 1} {102 1} {103 1} {104 1} {104 1}]
B	[{<nil> 0} {101 1} {103 2}] // disconnet
c	[{<nil> 0} {101 1} {103 2} {104 3}]

应该使A删除日志,与C同步,变成

// 我初始化有个空日志
A	[{<nil> 0} {101 1} {103 2} {104 3}]
B	[{<nil> 0} {101 1} {103 2}] // disconnet
c	[{<nil> 0} {101 1} {103 2} {104 3}]

如果B重连后,因为C的Term > B,因此B会变成C的Follower。所有人的日志变成:

[{<nil> 0} {101 1} {103 2} {104 3} {105 3}]

TestBackup2B

这个测试最难通过的,为了方便理解测试场景,修改程序使得每次只加一条log而不是50条。

当执行完:

cfg.disconnect((leader1 + 0) % servers)
cfg.disconnect((leader1 + 1) % servers)

// allow other partition to recover
cfg.connect((leader1 + 2) % servers)
cfg.connect((leader1 + 3) % servers)
cfg.connect((leader1 + 4) % servers)

日志的变化情况如下。此时只有P0,P1,P3在线,因为P3的日志比其他两个更up-to-date,所以P3应该成为Leader。P3必须P0和P1的赞成,也就是要保证P3每次是第一个选举超时的。不然,P0可能投给P1,P1也有可能投给P0。选择在选Leader的时候需要花很多时间,有时候很快通过,有时候要花很久时间。

在这里插入图片描述

重写AppendEntries Handler

起初通不过测试是因为该函数有问题,主要是那个翻译和原文好像有歧义。

在这里插入图片描述

第2条和第3条应该翻译错了吧。我认为第2条:接受者的日志中没有args.prevLog,第3条:是为了提高性能,你当然选择删除args.PrevLogIndex后面的所有内容,然后再加入一样的东西。

我的部分代码:

	// 2. Reply false if log doesn’t contain an entry at prevLogIndex
	// whose term matches prevLogTerm (§5.3)
	// 我的日志不包含args.prevLog, Leader will rf.nextIndex[]--
	lastLogIndex := len(rf.log) - 1
	if args.PrevLogIndex > lastLogIndex{
		reply.Success = false
		return
	}
	if rf.log[lastLogIndex].Term != args.PrevLogTerm {
		reply.Success = false
		return
	}
	// 3. If an existing entry conflicts with a new one (same index
	// but different terms), delete the existing entry and all that
	// follow it(5.3)
	// 3.1 说明[args.PrevLogIndex+1, conflict)是重复的,可以保留不删除
	// 3.2 当然也可以直接删除args.PrevLogIndex后面的所有内容
	start := args.PrevLogIndex + 1
	for i, newOne := range args.Entries {
		idx := start + i
		if idx < len(rf.log) {
			if rf.log[idx].Term != newOne.Term {
				rf.log = rf.log[:idx]
				break
			}
		}
	}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值