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.go
和test_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心跳的增加
type AppendEntriesArgs struct {
Term int
LeaderId int
PrevLogIndex int
PrevLogTerm int
Entries []LogEntry
LeaderCommit int
}
type AppendEntriesReply struct {
Term int
Success bool
}
主要就是看这张图了,下面就是实行心跳的时候要求了:
-
如果term < currentTerm,则返回false
-
如果日志中不包含与prevLogTerm匹配的项,则返回false
-
如果现有entry与新的entry冲突了(相同索引但不同任期),则删除现有条目及其后面的所有entry
-
追加日志中没有的任何新entry
-
如果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 选举限制
先看论文里面提到的选举限制
// 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加一下