枯木逢春不在茂
年少且惜镜边人
写在前面
emmmmmmmmmmmmmmmmmmm,想了想没什么说的,说说lab2b,写lab2B之前我觉得应该先看一遍raft论文,当然两边更好,三遍更好好。
实现过程
也是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.role = ROLE_FOLLOWER
rf.leaderId = -1
rf.votedFor = -1
rf.lastActiveTime = time.Now()
// initialize from state persisted before a crash
rf.readPersist(persister.ReadRaftState())
// election逻辑
go rf.electionLoop()
// leader逻辑
go rf.appendEntriesLoop()
// apply逻辑
go rf.applyLogLoop(applyCh)
DPrintf("Raftnode[%d]启动", me)
return rf
}
新添加了apply的逻辑。其他的基本也没变,之后变得最大的就是心跳机制了
func (rf *Raft) Start(command interface{}) (int, int, bool) {
index := -1
term := -1
isLeader := true
// Your code here (2B).
rf.mu.Lock()
defer rf.mu.Unlock()
// 只有leader才能写入
if rf.role != ROLE_LEADER {
return -1, -1, false
}
logEntry := LogEntry{
Command: command,
Term: rf.currentTerm,
}
rf.log = append(rf.log, logEntry)
index = len(rf.log)
term = rf.currentTerm
rf.persist()
DPrintf("RaftNode[%d] Add Command, logIndex[%d] currentTerm[%d]", rf.me, index, term)
return index, term, isLeader
}
start函数会在3A的服务器端进行调用,进行日志的存储,然后进入一致性复制
func (rf *Raft) appendEntriesLoop() {
for !rf.killed() {
time.Sleep(10 * time.Millisecond)
func() {
rf.mu.Lock()
defer rf.mu.Unlock()
// 只有leader才向外广播心跳
if rf.role != ROLE_LEADER {
return
}
// 100ms广播1次
now := time.Now()
if now.Sub(rf.lastBroadcastTime) < 100*time.Millisecond {
return
}
rf.lastBroadcastTime = time.Now()
// 并发RPC心跳
type AppendResult struct {
peerId int
resp *AppendEntriesReply
}
for peerId := 0; peerId < len(rf.peers); peerId++ {
if peerId == rf.me {
continue
}
args := AppendEntriesArgs{}
args.Term = rf.currentTerm
args.LeaderId = rf.me
args.LeaderCommit = rf.commitIndex
args.Entries = make([]LogEntry, 0)
args.PrevLogIndex = rf.nextIndex[peerId] - 1
if args.PrevLogIndex > 0 {
args.PrevLogTerm = rf.log[args.PrevLogIndex - 1].Term
}
args.Entries = append(args.Entries, rf.log[rf.nextIndex[peerId]-1:]...)
DPrintf("RaftNode[%d] appendEntries starts, currentTerm[%d] peer[%d] logIndex=[%d] nextIndex[%d] matchIndex[%d] args.Entries[%d] commitIndex[%d]",
rf.me, rf.currentTerm, peerId, len(rf.log), rf.nextIndex[peerId], rf.matchIndex[peerId], len(args.Entries), rf.commitIndex)
// log相关字段在lab-2A不处理
go func(id int, args1 *AppendEntriesArgs) {
// DPrintf("RaftNode[%d] appendEntries starts, myTerm[%d] peerId[%d]", rf.me, args1.Term, id)
reply := AppendEntriesReply{}
if ok := rf.sendAppendEntries(id, args1, &reply); ok {
rf.mu.Lock()
defer rf.mu.Unlock()
defer func() {
DPrintf("RaftNode[%d] appendEntries ends, currentTerm[%d] peer[%d] logIndex=[%d] nextIndex[%d] matchIndex[%d] commitIndex[%d]",
rf.me, rf.currentTerm, id, len(rf.log), rf.nextIndex[id], rf.matchIndex[id], rf.commitIndex)
}()
// 如果不是rpc前的leader状态了,那么啥也别做了
if rf.currentTerm != args1.Term {
return
}
if reply.Term > rf.currentTerm { // 变成follower
rf.role = ROLE_FOLLOWER
rf.leaderId = -1
rf.currentTerm = reply.Term
rf.votedFor = -1
rf.persist()
return
}
if reply.Success { // 同步日志成功
rf.nextIndex[id] += len(args1.Entries)
rf.matchIndex[id] = rf.nextIndex[id] - 1
// 数字N, 让peer[i]的大多数>=N
// peer[0]' index=2
// peer[1]' index=2
// peer[2]' index=1
// 1,2,2
// 更新commitIndex, 就是找中位数
sortedMatchIndex := make([]int, 0)
sortedMatchIndex = append(sortedMatchIndex, len(rf.log))
for i := 0; i < len(rf.peers); i++ {
if i == rf.me {
continue
}
sortedMatchIndex = append(sortedMatchIndex, rf.matchIndex[i])
}
sort.Ints(sortedMatchIndex)
newCommitIndex := sortedMatchIndex[len(rf.peers) / 2]
if newCommitIndex > rf.commitIndex && rf.log[newCommitIndex - 1].Term == rf.currentTerm {
rf.commitIndex = newCommitIndex
}
// rf.commitIndex = minMatchIndex
} else {
rf.nextIndex[id] -= 1
if rf.nextIndex[id] < 1 {
rf.nextIndex[id] = 1
}
}
}
}(peerId, &args)
}
}()
}
}
我们来看看这个心跳处理机制比之前多了什么处理过程
心跳机制不再是发送一个空的请求,而是带有领导人前一条日志和前一条日志的周期去检查peer的一致性,并且带有领导人已经提交的最大日志索引,然后就是发送RPC请求,看看响应部分做了那些处理
周期检查都和之前的逻辑一样
- 如果同步成功,则修改nextindex,和matchindex,分别表示发送给该peer的下一条日志的索引,和peer的最大已经复制日志的索引,不出意外的话,matchindex=nextindex-1
- 然后就是提交日志了,看看这个日志是不是被集群中大于一半的peer所复制,这个检查机制,这个大佬做的挺机制的,就是把所有的peer的同步的最大索引拿出来,排序后,找出中间的那个 就是大多数peer复制的,然后进行周期检查,并且比较大小。周期检查有可能中位数不是这个周期发送的 ,则不能处理
- 同步失败的话,可能是由于follow进度过慢导致的,在lab2C中会优化这个过程使用矛盾周期和矛盾索引直接定位,2B中只要乖乖回退,重发就可
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
DPrintf("RaftNode[%d] Handle AppendEntries, LeaderId[%d] Term[%d] CurrentTerm[%d] role=[%s] logIndex[%d] prevLogIndex[%d] prevLogTerm[%d] commitIndex[%d] Entries[%v]",
rf.me, rf.leaderId, args.Term, rf.currentTerm, rf.role, len(rf.log), args.PrevLogIndex, args.PrevLogTerm, rf.commitIndex, args.Entries)
reply.Term = rf.currentTerm
reply.Success = false
defer func() {
DPrintf("RaftNode[%d] Return AppendEntries, LeaderId[%d] Term[%d] CurrentTerm[%d] role=[%s] logIndex[%d] prevLogIndex[%d] prevLogTerm[%d] Success[%v] commitIndex[%d] log[%v]",
rf.me, rf.leaderId, args.Term, rf.currentTerm, rf.role, len(rf.log), args.PrevLogIndex, args.PrevLogTerm, reply.Success, rf.commitIndex, rf.log)
}()
if args.Term < rf.currentTerm {
return
}
// 发现更大的任期,则转为该任期的follower
if args.Term > rf.currentTerm {
rf.currentTerm = args.Term
rf.role = ROLE_FOLLOWER
rf.votedFor = -1
rf.leaderId = -1
rf.persist()
// 继续向下走
}
// 认识新的leader
rf.leaderId = args.LeaderId
// 刷新活跃时间
rf.lastActiveTime = time.Now()
// appendEntries RPC , receiver 2)
// 如果本地没有前一个日志的话,那么false
if len(rf.log) < args.PrevLogIndex {
return
}
// 如果本地有前一个日志的话,那么term必须相同,否则false
if args.PrevLogIndex > 0 && rf.log[args.PrevLogIndex - 1].Term != args.PrevLogTerm {
return
}
for i, logEntry := range args.Entries {
index := args.PrevLogIndex + i + 1
if index > len(rf.log) {
rf.log = append(rf.log, logEntry)
} else { // 重叠部分
if rf.log[index - 1].Term != logEntry.Term {
rf.log = rf.log[:index - 1] // 删除当前以及后续所有log
rf.log = append(rf.log, logEntry) // 把新log加入进来
} // term一样啥也不用做,继续向后比对Log
}
}
rf.persist()
// 更新提交下标
if args.LeaderCommit > rf.commitIndex {
rf.commitIndex = args.LeaderCommit
if len(rf.log) < rf.commitIndex {
rf.commitIndex = len(rf.log)
}
}
reply.Success = true
}
接收心跳需要做的事情就细了起来
- 周期一致性检查
- 返回成功的前提就是 本地一定要有leader 的前一个日志,而且周期相同
- 如果本地日志和leader一样长,则直接添加到后边,如果本地日志大于leader日志,则删除掉周期不一致的条目。一致的条目不变
- 最后根据leadercommit进行更新自己的commitindex
func (rf *Raft) applyLogLoop(applyCh chan ApplyMsg) {
for !rf.killed(){
time.Sleep(10 * time.Millisecond)
var appliedMsgs = make([]ApplyMsg, 0)
func() {
rf.mu.Lock()
defer rf.mu.Unlock()
for rf.commitIndex > rf.lastApplied {
rf.lastApplied += 1
appliedMsgs = append(appliedMsgs, ApplyMsg{
CommandValid: true,
Command: rf.log[rf.lastApplied-1].Command,
CommandIndex: rf.lastApplied,
CommandTerm: rf.log[rf.lastApplied - 1].Term,
})
DPrintf("RaftNode[%d] applyLog, currentTerm[%d] lastApplied[%d] commitIndex[%d]", rf.me, rf.currentTerm, rf.lastApplied, rf.commitIndex)
}
}()
// 锁外提交给应用层
for _, msg := range appliedMsgs {
applyCh <- msg
}
}
}
这个是提交检测,一旦有日志复制且大于最后一次复制的就进行提交。这里不用担心一致性问题,因为leader会提交的日志会先于peer提交的
func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
//Your code here (2A, 2B).
rf.mu.Lock()
defer rf.mu.Unlock()
reply.Term = rf.currentTerm
reply.VoteGranted = false
DPrintf("RaftNode[%d] Handle RequestVote, CandidatesId[%d] Term[%d] CurrentTerm[%d] LastLogIndex[%d] LastLogTerm[%d] votedFor[%d]", rf.me, args.CandidateId, args.Term, rf.currentTerm, args.LastLogIndex, args.LastLogTerm, rf.votedFor)
defer func() {
DPrintf("RaftNode[%d] Return RequestVote, CandidatesId[%d] VoteGranted[%v] ", rf.me, args.CandidateId, reply.VoteGranted)
}()
// 任期不如我大,拒绝投票
if args.Term < rf.currentTerm {
return
}
// 发现更大的任期,则转为该任期的follower
if args.Term > rf.currentTerm {
rf.currentTerm = args.Term
rf.role = ROLE_FOLLOWER
rf.votedFor = -1
rf.leaderId = -1
}
if rf.votedFor == -1 || rf.votedFor == args.CandidateId {
// candidate的日志必须比我的新
// 1, 最后一条log,任期大的更新
// 2,更长的log则更新
lastLogTerm := 0
if len(rf.log) != 0 {
lastLogTerm = rf.log[len(rf.log)-1].Term
}
// if args.LastLogTerm >lastLogTerm || args.LastLogIndex < len(rf.log) {
// DPrintf("sbbbbb")
// return
// }
// rf.votedFor = args.CandidateId
// reply.VoteGranted = true
// rf.lastActiveTime = time.Now() // 为其他人投票,那么重置自己的下次投票时间
//lastLogTerm := rf.lastTerm()
// 这里坑了好久,一定要严格遵守论文的逻辑,另外log长度一样也是可以给对方投票的
if args.LastLogTerm > lastLogTerm || (args.LastLogTerm == lastLogTerm && args.LastLogIndex >= len(rf.log)) {
rf.votedFor = args.CandidateId
reply.VoteGranted = true
rf.lastActiveTime = time.Now() // 为其他人投票,那么重置自己的下次投票时间
}
}
rf.persist()
}
这就是请求投票的RPC处理,比之前多的就是
args.LastLogTerm > lastLogTerm || (args.LastLogTerm == lastLogTerm && args.LastLogIndex >= len(rf.log))
这个判断条件,这个判断条件说白了就是两个
- leader的日志要么比我的日志长或者相等且周期相等则投票
- leader的日志周期大于我的最后一条的日志周期
func (rf *Raft) electionLoop() {
for !rf.killed() {
time.Sleep(1 * time.Millisecond)
func() {
rf.mu.Lock()
defer rf.mu.Unlock()
now := time.Now()
timeout := time.Duration(200+rand.Int31n(150)) * time.Millisecond
// 超时随机化
elapses := now.Sub(rf.lastActiveTime)
if rf.role == ROLE_FOLLOWER {
if elapses >= timeout {
rf.role = ROLE_CANDIDATES
DPrintf("RaftNode[%d] Follower -> Candidate", rf.me)
}
}
if rf.role == ROLE_CANDIDATES && elapses >= timeout {
rf.lastActiveTime = now //重置下次选举时间
rf.currentTerm += 1
rf.votedFor = rf.me
rf.persist()
//reqvote
args := RequestVoteArgs{
Term: rf.currentTerm,
CandidateId: rf.me,
LastLogIndex: len(rf.log), //?
}
if len(rf.log) != 0 {
args.LastLogTerm = rf.log[len(rf.log)-1].Term
}
rf.mu.Unlock()
DPrintf("RaftNode[%d] RequestVote starts, Term[%d] LastLogIndex[%d] LastLogTerm[%d]", rf.me, args.Term,
args.LastLogIndex, args.LastLogTerm)
// 并发RPC请求vote
type VoteResult struct {
peerId int
resp *RequestVoteReply
}
/*
type RequestVoteReply struct {
// Your data here (2A).
Term int
VoteGranted bool
}
*/
voteCount := 1 // 收到投票个数(先给自己投1票)
finishCount := 1 // 收到应答个数
VoteResultChan := make(chan *VoteResult, len(rf.peers))
for peerId := 0; peerId < len(rf.peers); peerId++ {
go func(id int) {
if id == rf.me {
return
}
resp := RequestVoteReply{}
if ok := rf.sendRequestVote(id, &args, &resp); ok {
VoteResultChan <- &VoteResult{peerId: id, resp: &resp}
} else {
VoteResultChan <- &VoteResult{peerId: id, resp: nil}
}
}(peerId)
}
maxTerm := 0
for {
select {
case VoteResult := <-VoteResultChan:
finishCount += 1
if VoteResult.resp != nil {
if VoteResult.resp.VoteGranted {
voteCount += 1
}
if VoteResult.resp.Term > maxTerm {
maxTerm = VoteResult.resp.Term
}
}
if finishCount == len(rf.peers) || voteCount > len(rf.peers)/2 {
goto VOTE_END
}
}
}
VOTE_END:
rf.mu.Lock()
defer func() {
DPrintf("RaftNode[%d] RequestVote ends, finishCount[%d] voteCount[%d] Role[%s] maxTerm[%d] currentTerm[%d]", rf.me, finishCount, voteCount,
rf.role, maxTerm, rf.currentTerm)
}()
// 如果角色改变了,则忽略本轮投票结果
if rf.role != ROLE_CANDIDATES {
return
}
if maxTerm > rf.currentTerm {
rf.role = ROLE_FOLLOWER
rf.leaderId = -1
rf.currentTerm = maxTerm
rf.votedFor = -1
rf.persist()
return
}
if voteCount > len(rf.peers)/2 {
rf.role = ROLE_LEADER
rf.leaderId = rf.me
rf.nextIndex = make([]int, len(rf.peers))
for i := 0; i < len(rf.peers); i++ {
rf.nextIndex[i] = len(rf.log) + 1
}
rf.matchIndex = make([]int, len(rf.peers))
for i := 0; i < len(rf.peers); i++ {
rf.matchIndex[i] = 0
}
rf.lastBroadcastTime = time.Unix(0, 0) // 令appendEntries广播立即执行
return
}
}
}()
}
}
这个部分比上2A多了些这个
rf.nextIndex = make([]int, len(rf.peers))
for i := 0; i < len(rf.peers); i++ {
rf.nextIndex[i] = len(rf.log) + 1
}
rf.matchIndex = make([]int, len(rf.peers))
for i := 0; i < len(rf.peers); i++ {
rf.matchIndex[i] = 0
}
rf.lastBroadcastTime = time.Unix(0, 0) // 令appendEntries广播立即执行
就是选举成功后初始化每个peer的nextindex和matchindex matchindex为什么是0 每次心跳成功后会被leader更新,而nextindex 则需要在心跳机制中用到,确保发送最新的日志条目。
以上就是lAB2B的全部了
写在后边
这是某位大佬的总结,个人觉得很有用,仅供日后复习所用
- nextIndex是leader对follower日志同步进度的猜测,matchIndex则是实际获知到的同步进度,leader需要不断的appendEntries来和follower进行反复校对,直到PrevLogIndex、PrevLogTerm符合Raft论文约束。
- Leader更新commitIndex需要计算大多数节点拥有的日志范围,通过对matchIndex排序找中位数的index,就是大多数节点都拥有的日志范围,将其设置为commitIndex。
- Follower收到appendEntries时,一定要在处理完log写入后再更新commitIndex,因为论文中要求Follower的commitIndex是min(local log index,leaderCommitIndex)。
- Leader在不断调整某个follower的nextIndex过程中,注意不要让nextIndex减小到1以下,因为nextIndex的语义是follower的下一个日志写入下标。
- requestVote接收端,一定要严格根据论文判定发起方的lastLogIndex和lastLogTerm是否符合条件,这里很容易写错。
- appendEntries接收端,一定要严格遵从prevLogIndex和prevLogTerm的论文校验逻辑,首先看一下prevLogIndex处是否有本地日志(prevLogIndex==0除外,相当于从头同步日志),没有的话则还需要leader来继续回退nextIndex直到prevLogIndex位置有日志。在prevLogIndex有日志的前提下,还需要进一步判prevLogIndex位置的Term是否一样。
- appendEntries接收端返回success=true的前提必须是将leader传来的日志处理完了并且更新了commitIndex之后。
- appendEntries接收端,要做好日志冲突时的后续全部截断逻辑。
- 注意当log、voteFor、commitIndex三个状态变化后,返回RPC前一定要persist()持久化一下(Lab2B并不要求persist,但是Lab3C要求,所以在这里先做好,免得Lab2C回头加)
- 日志提交通过一个独立的goroutine实现就行,定期判断lastApplied和commitIndex之间的有日志就提交即可,注意提交效率(copy出来释放掉锁,然后往应用层推)