MIT 6.824 分布式系统课程lab实现 (2) lab2A Leader Election

前言

代码上传到个人github仓库6.824
多线程编程因为情况过于复杂,单单通过运行几次go test -run 2A命令得到PASS是无法证实代码的可靠性的.
可以通过该门课程助教提供的脚本test
个人最开始写过的一版代码能通过1000次测试,后来经过重新设计思考后才发现有明显的bug.
所以同学们可以多使用该脚本多跑几次,看看有无出错,查找log日志发现错误在哪里.
有需要的同学还可以修改测试代码.
该版本代码能通过上述脚本运行2000/2000次测试无错误,个人不敢保证bug free,如有发现错误望能指正.

运行模型

raft主要由三个部分组成:

  1. 主部(只能由此部分修改raft状态, 计时等工作都由此部分进行)
  2. 发送信息部分(进行rpc调用)
  3. 处理接收信息部分(响应其它raft的rpc调用,响应自己rpc调用收到的reply)

同步变量

type Raft struct {
    rpcMutex   sync.Mutex // 将收到,发出rpc调用完成之后的数据处理串行化 但并发调用rpc
    stateMutex sync.Mutex // 保护raft状态的读写安全

    //用于内部数据同步
    requestChan  chan InnerRequest
    responseChan chan InnerResponse

    timer *time.Timer
}

raft相当于状态机,要改变raft的状态只有两种方法 超时接收信息
运行方式如下

  1. raft主部串行执行,在加锁的状态下修改完状态之后,根据情况选择是否reset计时器,再释放锁,使用select监听超时信号或者是处理接收信息部分发送的信号
  2. 发送信息部分较为简单,只需要加锁状态下拷贝raft状态,然后并发进行rpc调用即可(注意:发送时加锁会导致不同的raft实体循环调用从而导致死锁)
  3. 接收的信息是并发到达的,在所有处理函数前加锁rpcMutex,函数退出后再释放该锁,使得该阶段串行化. 此外,在执行时如果发现需要通知raft修改状态,还要通过requestChan和responseChan与主部进行通信,由于都是0缓存,往这两个channel写数据未被读出时,写者处于阻塞状态.

代码主要部分

1. MainProcess

通过rf.state判断执行分支

func (rf *Raft) MainProcess() {
    for {
        switch rf.state {
        case FOLLOWERSTATE:
            rf.FollowerProcess()
        case CANDIDATESTATE:
            rf.currentTerm += 1
            rf.votedFor = rf.me
            rf.numOfVotedPeers = 1
            for i := range rf.peers {
                if i != rf.me {
                    rf.votedStateOfPeers[i] = false
                }
            }
            rf.CandidateProcess()
        case LEADERSTATE:
            rf.LeaderProcess()
        case DEADSTATE:
            return
        }
    }
}

2. CandidateProcess

func (rf *Raft) CandidateProcess() {
    //candidate一个Term内可以发送多轮请求选票
    //因此多设置了一个tmpTimer这一个计时器
    tmpTimer := time.NewTimer(HEARTBEATS_INTERVAL)
    //Term计时器
    rf.timer.Reset(GetTimeoutInterval())
    //并发发送选票请求
    go rf.sendAllRequestVote()
    for {
        //在进入监听状态之前要对stateMutex进行解锁
        rf.stateMutex.Unlock()
        select {
        //该轮Term超时
        case <-rf.timer.C:
            rf.stateMutex.Lock()
            //这里的冗余代码是为了方便提醒,该版本有多处冗余代码
            rf.state = CANDIDATESTATE
            return
        //超时,发起该Term内的又一轮选票请求
        case <-tmpTimer.C:
            rf.stateMutex.Lock()
            tmpTimer.Reset(GetTimeoutInterval())
            go rf.sendAllRequestVote()
            continue
        //处理接收信息函数发来的信号
        case tmp := <-rf.requestChan:
            rf.stateMutex.Lock()
            //操作类型
            operation := tmp.operation
            //该信号对应的Term,
            term := tmp.term
            extraInf := tmp.extraInf
            //Term过期,抛弃
            if term < rf.currentTerm {
                rf.responseChan <- InnerResponse{false, UNDIFINE, rf.currentTerm, []int{}}
                continue
            }
            switch operation {
            case NEWTERM:
                //这里判断条件可以是==
                if term <= rf.currentTerm {
                    //由于处理部分阻塞在该channel上,即使不操作,也要释放信号
                    rf.responseChan <- InnerResponse{false, UNDIFINE, rf.currentTerm, []int{}}
                    //continue用于不需要重启计时的操作的分支的退出
                    continue
                } else {
                    if !rf.timer.Stop() {
                        <-rf.timer.C
                    }
                    rf.state = FOLLOWERSTATE
                    rf.currentTerm = extraInf[0]
                    rf.votedFor = -1
                    rf.responseChan <- InnerResponse{true, UNDIFINE, rf.currentTerm, []int{}}
                    //return用于需要重启计时的操作的分支的退出
                    //返回至MainProcess()
                    //因此这种分支之前需要排空rf.timer.C
                    return
                }
            case LEGALLEADER:
                if !rf.timer.Stop() {
                    <-rf.timer.C
                }
                rf.state = FOLLOWERSTATE
                rf.currentTerm = extraInf[0]
                rf.responseChan <- InnerResponse{true, UNDIFINE, rf.currentTerm, []int{}}
                return
            case LATERCANDIDATE:
                if !rf.timer.Stop() {
                    <-rf.timer.C
                }
                rf.state = FOLLOWERSTATE
                rf.currentTerm = extraInf[0]
                rf.votedFor = extraInf[1]
                rf.responseChan <- InnerResponse{true, UNDIFINE, rf.currentTerm, []int{}}
                return
            case VOTEFOR:
                rf.responseChan <- InnerResponse{false, UNDIFINE, rf.currentTerm, []int{}}
                continue
            case GETVOTE:
                if !rf.votedStateOfPeers[extraInf[1]] {
                    rf.numOfVotedPeers += 1
                    rf.votedStateOfPeers[extraInf[1]] = true
                    if rf.numOfVotedPeers > rf.numOfAllPeers/2 {
                        if !rf.timer.Stop() {
                            <-rf.timer.C
                        }
                        rf.state = LEADERSTATE
                        rf.responseChan <- InnerResponse{true, UNDIFINE, rf.currentTerm, []int{}}
                        return
                    } else {
                        continue
                    }
                } else {
                    continue
                }

            case BEDEAD:
                rf.state = DEADSTATE
                rf.responseChan <- InnerResponse{true, UNDIFINE, rf.currentTerm, []int{}}
                if !rf.timer.Stop() {
                    <-rf.timer.C
                }
                return
            }
        }
    }
}

需要注意的细节

1. 计时器timer的使用

在assignment的页面里提到了可以使用time.Sleep()来代替计时功能,因为Timer和Ticker难以使用正确,但是使用time.Sleep()方法终归是不优雅.
timer通过time.NewTimer(Duration)创建,在经过指定的Duration时间之后,会往timer.C这个channel里发送信号,使用timer.Stop()可以停止计时,使用timer.Reset(Duration)可以重设时间,这些是通过简单地阅读文档就能得到的信息.
但是需要注意的一点是调用timer.Stop()的返回值
在调用timer.Stop()后正确将计时器停止后,timer.Stop()返回值为true.
但是当timer.Stop()在计时器停止后再调用则会返回false,为了不影响后序的信号传递,需要将timer.C排空

if !timer.Stop(){
    <-timer.C
}
//错误示范:
//通过select语句判断timer.C中是否有信号,若无信号直接经过default分支
//但是问题在于一种情况timer.Stop()未正确停止,但是信号量还未发送到timer.C上,此时程序直接从default语句经过,而未正确处理信号量
if !timer.Stop(){
    select{
    case <- timer.C:
    case default:
    }
}

2. golang闭包

和大小写首字母一样对go不熟悉的人常踩的坑,for循环中使用for定义的参数会因为协程引用同一块地址而导致运行错误

func (rf *Raft) sendAllAppendEntries() {
    tmp := rf.getCopy()
    args := AppendEntriesArgs{
        Term:     tmp.currentTerm,
        LeaderId: tmp.me,
    }
    for i := range tmp.peers {
        if i != rf.me {
            //使用tmp_i
            tmp_i := i
            go rf.sendAppendEntries(tmp_i, &args, &AppendEntriesReply{})
        }
    }
}

3. goroutine id

调试代码时可能会遇到不可能出现的输出,例如goroutine杀不干净导致输出错误等,这种情况下可以尝试打印goroutine id进行调试.

func GoID() int {

    var buf [64]byte

    n := runtime.Stack(buf[:], false)

    // 得到id字符串

    idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]

    id, err := strconv.Atoi(idField)

    if err != nil {

        panic(fmt.Sprintf("cannot get goroutine id: %v", err))

    }

    return id

}

你可能感兴趣的:(分布式系统golang)