MIT 6.824 lab 2B 记录

77 阅读6分钟

MIT 6.824 lab 2B 记录

条件

  • 运行git pull获取最新的实验室软件。
  • 您的第一个目标应该是通过TestBasicAgree2B()。首先实现Start(),然后编写代码以通过AppendEntries RPC 发送和接收新日志条目,如下图 2 所示。在每个对等方的``applyCh上发送每个新提交的条目。
  • 您需要实施选举限制(本文第 5.4.1 节)。
  • 在早期 Lab 2B 测试中无法达成一致的一种方法是,即使领导者还活着,也进行重复选举。查找选举计时器管理中的错误,或者在赢得选举后没有立即发送心跳。
  • 您的代码可能具有重复检查某些事件的循环。不要让这些循环连续执行而不暂停,因为这会减慢您的实现速度,导致测试失败。使用 Go 的 条件变量,或者在每次循环迭代中插入一个 time.Sleep(10 * time.Millisecond)
  • 为未来的实验帮自己一个忙,编写(或重写)干净清晰的代码。如需想法,请重新访问我们的指南页面,其中包含有关如何开发和调试代码的提示。
  • 如果测试失败,请查看config.gotest_test.go中的测试代码,以更好地了解测试正在测试的内容。 config.go还说明了测试人员如何使用 Raft API。

Raft和一些初始化

Raft:

ype 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).
	log         map[int]LogEntry
	commitIndex int   // 已经提交的最高日志条目的索引
	lastApplied int   // 已经应用到状态机的最高日志条目的索引 (initialized to 0, increases monotonically)
	nextIndex   []int // 对于每个服务器(通常是集群中的其他节点),它表示下一个要发送到该服务器的日志条目的索引 (initialized to leader last log index + 1)
	matchIndex  []int // 对于每个服务器,表示已知已经在该服务器上复制的最高日志条目的索引 (initialized to 0, increases monotonically)
	applyCh     chan ApplyMsg
	// Look at the paper's Figure 2 for a description of what
	// state a Raft server must maintain.

}

type LogEntry struct {
	Term    int
	Command interface{}
	Index   int
}

Make:

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.lastApplied = 0
	rf.commitIndex = 0
	rf.nextIndex = make([]int, len(rf.peers))
	rf.matchIndex = make([]int, len(rf.peers))
	rf.log = make(map[int]LogEntry)
	rf.heartbeatTimer = time.NewTimer(100 * time.Millisecond)
	rf.electionTimer = time.NewTimer(getRandomTimeout())
	rf.applyCh = applyCh
	for i := range rf.peers {
		rf.matchIndex[i] = 0
		rf.nextIndex[i] = len(rf.log) + 1
	}
	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
}

Start:

func (rf *Raft) Start(command interface{}) (int, int, bool) {
	// Your code here (2B).
	index := -1
	rf.mu.Lock()
	defer rf.mu.Unlock()
	isLeader := rf.currentRole == ROLE_Leader
	term := rf.currentTerm
	if !isLeader {
		return index, term, isLeader
	}

	// record in local log
	index = len(rf.log) + 1
	rf.log[index] = LogEntry{Term: term, Command: command, Index: index}
	//rf.persist()
	//DPrintf("[Start] %s Add Log Index=%d Term=%d Command=%v\n", rf.role_info(), rf.getLogLogicSize(), rf.log[index].Term, rf.log[index].Command)
	return index, term, isLeader
}

leader心跳的增加

58b93e06bbe3dac0c2a39952df51a38d

type AppendEntriesArgs struct {
	Term         int
	LeaderId     int
	PrevLogIndex int
	PrevLogTerm  int
	Entries      []LogEntry
	LeaderCommit int
}

type AppendEntriesReply struct {
	Term    int
	Success bool
}

主要就是看这张图了,下面就是实行心跳的时候要求了:

  1. 如果term < currentTerm,则返回false

  2. 如果日志中不包含与prevLogTerm匹配的项,则返回false

  3. 如果现有entry与新的entry冲突了(相同索引但不同任期),则删除现有条目及其后面的所有entry

  4. 追加日志中没有的任何新entry

  5. 如果leaderCommit > commitIndex,则设置commitIndex = min(leaderCommit,最后一个新entry的索引)

AppendEntries

follwer 接收日志

// 发送心跳对应三个角色的执行
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
	rf.mu.Lock()
	defer rf.mu.Unlock()
	// 当前的任期比leader的都大
	if rf.currentTerm > args.Term {
		reply.Success = false
		rf.heartbeatFlag = 1
		return
	}

	// 0.优先处理curterm<args.term,直接转化为follow
	if rf.currentTerm < args.Term {
		rf.switchRole(ROLE_Follwer)
		rf.currentTerm = args.Term
		rf.heartbeatFlag = 1
		// TODO 差异一 没有补 -1

	}
	// 先做处理,便于直接return
	reply.Term = rf.currentTerm
	rf.electionTimer.Reset(getRandomTimeout())
	// 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
	}

	// 先获取 local log[args.PrevLogIndex] 的 term , 检查是否与 args.PrevLogTerm 相同,不同表示有冲突,直接返回失败
	prevLog, found := rf.log[args.PrevLogIndex]
	// 1.如果没找到prelogIndex或者args的任期不等于prelog的任期
	if args.PrevLogIndex != 0 && (!found || args.PrevLogTerm != prevLog.Term) {
		reply.Success = false
		return
	}
	// 2.同一任期,添加args的log
	for i := 0; i < len(args.Entries); i++ {
		// 拿log的idx
		idx := args.Entries[i].Index
		// 逐个将新的日志条目复制到Follower的日志中,以确保日志的一致性
		rf.log[idx] = args.Entries[i]
	}
	// 3.提交log
	if args.LeaderCommit > rf.commitIndex {
		for i := rf.commitIndex + 1; i <= args.LeaderCommit; i++ {
			rf.applyCh <- ApplyMsg{
				CommandValid:  true,
				Command:       rf.log[i].Command,
				CommandIndex:  i,
				SnapshotValid: false,
				Snapshot:      nil,
				SnapshotTerm:  0,
				SnapshotIndex: 0,
			}
		}
		rf.commitIndex = args.LeaderCommit
	}
	// leader不处理
	reply.Success = true
}

leaderHeart

leader 同步日志到 follwer

// 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
			args.LeaderCommit = rf.commitIndex
			args.LeaderId = rf.me
			// args的log index应该是这个server的nextlog index-1
			args.PrevLogIndex = rf.nextIndex[s] - 1
			// 找出这个log的对应任期
			args.PrevLogTerm = rf.log[args.PrevLogIndex].Term
			// 如果发现节点还有没commit 的 log
			if len(rf.log) != rf.matchIndex[s] {
				// 就把log放进args里面
				for i := rf.nextIndex[s]; i <= len(rf.log); i++ {
					args.Entries = append(args.Entries, rf.log[i])
				}
			}
			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
			}
			// 如果同步失败,Leader会将 nextIndex 减1,然后再次尝试将上一个日志条目发送给Follower。
			// 这样,Leader就有机会重新同步Follower的日志,确保日志的一致性。
			if !reply.Success {
				rf.nextIndex[s]--
			} else {
				// 同步成功,增加发送日志的索引数
				rf.nextIndex[s] += len(args.Entries)
				rf.matchIndex[s] = rf.nextIndex[s] - 1
				// 检查是否可以开始提交,因为可能一心跳的时间提交了多个log,从已经commit+1开始
				for commitIdx := rf.commitIndex + 1; commitIdx <= rf.matchIndex[s]; commitIdx++ {
					// 初始化为1,Leader节点已经成功将 commitIdx 位置的日志复制到自己身上
					matchCnt := 1
					// 每个节点开始投票
					for i := 0; i < len(rf.matchIndex); i++ {
						if commitIdx <= rf.matchIndex[i] {
							matchCnt++
						}
					}
					// 投票过半,commit成功
					if matchCnt*2 > len(rf.matchIndex) {
						// 向applyCh发送表示确实提交
						rf.commitIndex = commitIdx
						rf.applyCh <- ApplyMsg{
							CommandValid:  true,
							Command:       rf.log[commitIdx].Command,
							CommandIndex:  commitIdx,
							SnapshotValid: false,
							Snapshot:      nil,
							SnapshotTerm:  0,
							SnapshotIndex: 0,
						}
					} else {
						// 投票失败break
						break
					}
				}
			}
			rf.mu.Unlock()

		}(server)
	}
}

candidate 选举限制

先看论文里面提到的选举限制

image-20231027092741121

image-20231027093331561

// 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
	LastLogIndex int
	LastLogTerm  int
}

// 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 表示拿到票了
}

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()
	if rf.currentTerm > args.Term ||
		(args.Term == rf.currentTerm && rf.votedFor != -1 && rf.votedFor != args.CandidateId) {
		reply.VoteGranted = false
		return
	}
	rf.electionTimer.Reset(getRandomTimeout())
	// 		任期不对,先转化成follow
	reply.Term = rf.currentTerm
	if rf.currentTerm < args.Term {
		rf.switchRole(ROLE_Follwer)
		rf.currentTerm = args.Term
		rf.votedFor = -1
	}

	// 2B Leader restriction,拒绝比较旧的投票(优先看任期)
	// 1. 任期号不同,则任期号大的比较新
	// 2. 任期号相同,索引值大的(日志较长的)比较新
	// 拿出最大的log
	lastLog := rf.log[len(rf.log)]
	if args.LastLogTerm < lastLog.Term || (args.LastLogTerm == lastLog.Term && args.LastLogIndex < lastLog.Index) {
		reply.VoteGranted = false
		return
	}
	// 先看这个follow有没有投票过
	if rf.votedFor == -1 {
		rf.votedFor = args.CandidateId
		reply.VoteGranted = true
	} else {
		reply.VoteGranted = false
	}

}
// candidta发送给其他的follow去拉票
func (rf *Raft) StartElection() {
	// 重置票数和超时时间

	rf.currentTerm += 1
	rf.votedCnt = 1
	rf.electionTimer.Reset(getRandomTimeout())
	rf.votedFor = rf.me

	// 遍历每个节点
	for server := range rf.peers {
		// 先跳过自己
		if server == rf.me {
			continue
		}
		// 接下来使用goroutine发送rpc
		go func(s int) {
			rf.mu.Lock()
			lastLog := rf.log[len(rf.log)]
      // 2B RequestVoteArgs添加字段
			args := RequestVoteArgs{
				Term:         rf.currentTerm,
				CandidateId:  rf.me,
				LastLogTerm:  lastLog.Term,
				LastLogIndex: lastLog.Index,
			}
			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)
	}
}

这里就args加一下

参考

www.cnblogs.com/lawliet12/p…

pdos.csail.mit.edu/6.824/labs/…