[MIT6.824 lab2] Raft-Leader election 代码实现 & 踩坑记录

660 阅读3分钟

本文假设你已经对课程进行了部分了解,所以不对课程的要求进行过多描述,重点在代码的分析和一些踩坑记录和理解。

课程链接: nil.csail.mit.edu/6.824/2020/…

目标

实现raft算法中的领导人选举部分

需要实现的部分

  1. Make()

  2. Raft{}

  3. GetState()

  4. RequestVote()

  5. RequestVoteArgs{}

  6. RequestVoteReply{}

  7. AppendEntries()

  8. AppendEntriesArgs {}

  9. AppendEntriesReply {}

流程图

理解了流程图,要实现代码就比较简单了

code

这里直接展示代码,代码的逻辑配合流程图可以理解清楚。

Raft { }


const Leader int32 = 1

const Follower int32 = 2

const Candidate int32 = 3



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

    dead int32 // set by Kill()



    //任期号

    Term int32

    //获得选票的服务器

    VotedFor int

    //角色类型

    Role int32

    //下一个超时时间

    NextTimeout time.Time

}

Make()

最先要实现的部分是Make()方法,目的是要创建一个Raft对象、运行后台任务


func Make(peers []*labrpc.ClientEnd, me int, persister *Persister, applyCh chan ApplyMsg) *Raft {

    //log.Printf("== Make() ==")

    rf := &Raft{}

    rf.peers = peers

    rf.persister = persister

    rf.me = me

    // Your initialization Code here (2A, 2B, 2C).

    rf.Term = 0

    rf.Role = Follower

    rf.VotedFor = -1

    // 设置下次心跳检查时间

    rf.updateHeartbeatTime()

    // 维持状态的协程

    go rf.maintainStateLoop()

    // initialize from state persisted before a crash

    rf.readPersist(persister.ReadRaftState())



    return rf

}

goroutine 的方法 maintainStateLoop() 负责不停的执行各个角色对应的操作


func (rf *Raft) maintainStateLoop() {

    for !rf.killed() {

        rf.mu.Lock()

        if rf.isLeader() {

            rf.maintainsLeader()

        } else if rf.isFollower() {

            rf.maintainsFollower()

        } else if rf.isCandidate() {

            rf.maintainsCandidate()

        } else {

            log.Fatalf("[raft-%v %v] Role type error : %v \n", rf.me, rf.getRole(), rf.Role)

        }

    }

}

follower 的职责

判断心跳是否超时,如果超时就变成candidate


func (rf *Raft) maintainsFollower() {

    if rf.heartbeatTimeout() {

        rf.changeToCandidate()

        rf.mu.Unlock()

    } else {

        rf.mu.Unlock()

        time.Sleep(10 * time.Millisecond)

    }

}

candidate 的职责

负责发起竞选的rpc,根据请求的reply进行处理不同的处理


func (rf *Raft) maintainsCandidate() {

    // 检查超时

    if !rf.heartbeatTimeout() {

        rf.mu.Unlock()

        time.Sleep(10 * time.Millisecond)

        return

    }


    // 发送投票RPC

    log.Printf("[raft-%v %v %v] == 发起投票RPC == \n", rf.me, rf.getRole(), rf.Term)

    rf.VotedFor = rf.me

    rf.Term += 1

    maxTerm, sumVotes := rf.Term, len(rf.peers)

    voteReply := make(chan *RequestVoteReply)

    args := RequestVoteArgs{Peer: rf.me, Term: rf.Term}

    for idx := range rf.peers {

        if rf.me == idx {

            continue

        }

        go func(from int, to int, term int32) {

            reply := RequestVoteReply{Peer: to, Term: term, VoteGranted: false}

            _ = rf.sendRequestVote(to, &args, &reply)

            voteReply <- &reply

        }(rf.me, idx, rf.Term)

    }

    rf.mu.Unlock()


    // 处理投票回复

    validVotes, acceptVotes := 1, 1

    select {

    case reply := <-voteReply:

        validVotes++

        if reply.VoteGranted {

            acceptVotes++

        } else if reply.Term > maxTerm {

            maxTerm = reply.Term

        }

        if validVotes > sumVotes/2 {

            goto VotedDone

        }

    }


    //汇总投票结果

VotedDone:

    rf.mu.Lock()

    defer rf.mu.Unlock()

    if rf.killed() || !rf.isCandidate() {

    } else if maxTerm > rf.Term {

        log.Printf("[raft-%v-%v-%v] 投票结果: 有更大的Term,放弃竞选: %v.\n", rf.me, rf.getRole(), rf.Term, maxTerm)

        rf.changeToFollower(maxTerm)

    } else if rf.isQuorum(acceptVotes) {

        log.Printf("[raft-%v-%v-%v] == 投票通过: 总票数 = %v. 赞同票数 = %v == \n", rf.me, rf.getRole(), rf.Term, sumVotes, acceptVotes)

        rf.changeToLeader()

    } else {

        log.Printf("[raft-%v-%v-%v] == 投票未通过: 总票数 = %v. 赞同票数 = %v == \n", rf.me, rf.getRole(), rf.Term, sumVotes, acceptVotes)

        rf.updateHeartbeatTime()

    }

}


func (rf *Raft) isQuorum(accept int) bool {

    return accept > len(rf.peers)/2

}

Leader 的职责

负责向其他所有节点发送心跳


func (rf *Raft) maintainsLeader() {

    //log.Printf("[raft-%v %v %v] 发送心跳RPC. now = %v", rf.me, rf.getRole(), rf.Term, time.Now().Local())

    args := AppendEntriesArgs{Peer: rf.me, Term: rf.Term}

    for idx := range rf.peers {

        if idx == rf.me {

            continue

        }

        go func(from int, to int, term int32) {

            reply := AppendEntriesReply{}

            flag := rf.sendAppendEntries(to, &args, &reply)

            rf.mu.Lock()

            defer rf.mu.Unlock()

            if !rf.killed() && rf.isLeader() && flag && reply.Term > rf.Term && args.Term == rf.Term {

                rf.changeToFollower(reply.Term)

            }

        }(rf.me, idx, rf.Term)

    }

    rf.mu.Unlock()

    time.Sleep(100 * time.Millisecond)

}

竞选投票相关


type RequestVoteArgs struct {

    // 竞选人id

    Peer int

    // 竞选人term

    Term int32

}


type RequestVoteReply struct {

// 发送回复的节点id

    Peer int

// 发送回复的节点term

    Term int32

// 如果为true表示赞同

    VoteGranted bool

}


func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {

    rf.mu.Lock()

    defer rf.mu.Unlock()

    if rf.killed() {

        return

    }



    log.Printf("[raft-%v %v %v] 处理%v的投票RPC. T = %v \n", rf.me, rf.getRole(), rf.Term, args.Peer, args.Term)

    reply.Term, reply.VoteGranted = rf.Term, false



    // term太小,不理

    if args.Term < rf.Term {

        return

    }


    // 更大的term

    if args.Term > rf.Term {

        rf.changeToFollower(args.Term)

    }


    // 每个任期,只能投票一次

    if rf.VotedFor == -1 || rf.VotedFor == args.Peer {

        reply.VoteGranted = true

        rf.VotedFor = args.Peer

        rf.updateHeartbeatTime()

    }

}

心跳相关


type AppendEntriesArgs struct {

    Peer int

    Term int32

}


type AppendEntriesReply struct {

    Term int32

    Result bool

}

\



func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {

    rf.mu.Lock()

    defer rf.mu.Unlock()

    if rf.killed() {

        return

    }

    //log.Printf("[raft-%v %v %v] 接收来自%v的心跳请求. T = %v.\n", rf.me, rf.getRole(), rf.Term, args.Peer, args.Term)

    reply.Term, reply.Result = rf.Term, args.Term >= rf.Term


    if args.Term < rf.Term {

        return

    } else if rf.isCandidate() {

        rf.changeToFollower(args.Term)

    } else {

        rf.updateHeartbeatTime()

    }


    // 日志操作lab-2A不实现

    rf.persist()

}

注意事项


--- FAIL: TestReElection2A (8.51s)

config.go:330: expected one leader, got none

在这个test case中,先disconnect已经选出来的leader,然后给2s的时间选举出新的leader,这里的错误原因就是2s时间没有选举出新的leader。

要点在于如何提高选举的效率,保证更快的时间内(2s内)选举出leader,

出现这个情况的原因大概率和活锁有关,两个或多个candidate争夺竞选,这里注意一些要点:

  1. 关于选举间隔时间:建议 150 ~ 300 之间
  • 设置太高会浪费空闲时间

  • 设置太低又容易发生活锁,也存在一种情况就是leader选举出来了,但是又收到term更高的vote rp请求。

  1. 当选leader,应该尽快发送心跳rpc,防止follow过期

  2. leader 发送心跳的频率 , 建议在100ms一次,官方也是建议1s不超过10次

关于更新心跳时间的条件,首先本质上就是在收到有效的rpc请求时(知道有别人存活)或者自己发起选举时(知道自己存活),就要更新心跳时间,如下三个点:

  • 收到leader的心跳rpc时(leader.term >= self.term 即算有效)

  • 收到candidate的投票请求,并投了赞同票时

  • 在进行新一轮的选举时

脚本测试

测试脚本: gist.github.com/jonhoo/

250 次测试结果通过

Reference