Raft在etcd中的实现(三)选举流程

Raft中的选举流程

Raft中使用心跳机制来出发leader选举。当服务器启动的时候,服务器成为follower。只要follower从leader或者candidate收到有效的RPCs就会保持follower状态。如果follower在一段时间内(该段时间被称为election timeout)没有收到消息,则它会假设当前没有可用的leader,然后开启选举新leader的流程。流程如下:

  1. follower增加当前的term,转变为candidate。
  2. candidate投票给自己,并发送RequestVote RPC给集群中的其他服务器。
  3. 收到RequestVote的服务器,在同一term中只会按照先到先得投票给至多一个candidate。且只会投票给log至少和自身一样新的candidate。
  4. candidate节点保持2的状态,直到下面三种情况中的一种发生。
    4.1 该节点赢得选举。即收到大多数的节点的投票。则其转变为leader状态。
    4.2 另一个服务器成为了leader。即收到了leader的合法心跳包(term值等于或大于当前自身term值)。则其转变为follower状态。
    4.3 一段时间后依然没有胜者。该种情况下会开启新一轮的选举。
    Raft中使用随机选举超时时间来保证当票数相同无法确定leader的问题可以被快速的解决。

代码详解

首先看下节点为follower状态时的情况。节点在启动的时候,以及在收到合法的心跳包的时候会转化为follower状态。此时会调用becomeFollower方法。该方法主要是设置节点的状态,以及相应的方法。即follower状态的step方法为stepFollower,tick方法为tickElecation。其中reset方法将重置ElectionTimeout的计时器等。

// github.com/coreos/etcd/raft/node.go
func StartNode(c *Config, peers []Peer) Node {
    r := newRaft(c)
    // become the follower at term 1 and apply initial configuration
    // entries of term 1
      //启动的时候节点进入follower状态,会调用becomeFollower函数。
    r.becomeFollower(1, None)
      //...
}
// github.com/coreos/etcd/raft/raft.go
// becomeFollower方法,设置step函数为stepFollower,tick函数为tickElection。
// 使用reset重置状态,修改当前状态为StateFollower,设置leader的id。
func (r *raft) becomeFollower(term uint64, lead uint64) {
    r.step = stepFollower
    r.reset(term)
    r.tick = r.tickElection
    r.lead = lead
    r.state = StateFollower
    r.logger.Infof("%x became follower at term %d", r.id, r.Term)
}

follower正常运行的时候会收到leader节点的心跳。此时其会重设其electionElapsed。若一段时间收不到心跳包,electionElapsed达到ElectionTimeout值,则会调用Step方法处理MsgHup的消息。准备进入candidate状态。

// github.com/coreos/etcd/raft/raft.go
// follower状态收到leader的心跳包,会重设electionElapsed,可以理解为重设ElectionTimeout的计时器。
func stepFollower(r *raft, m pb.Message) error {
    switch m.Type {
    //......各种其他情况
    case pb.MsgHeartbeat:
        r.electionElapsed = 0
        r.lead = m.From
        r.handleHeartbeat(m)
     }
}
// github.com/coreos/etcd/raft/raft.go
// tickElection,每一个tick所运行的方法。在follower状态是增加electionElapsed的值,当其到达ElectionTimeout的值时,调用Step方法处理MsgHup消息。
func (r *raft) tickElection() {
    r.electionElapsed++

    if r.promotable() && r.pastElectionTimeout() {
        r.electionElapsed = 0
        r.Step(pb.Message{From: r.id, Type: pb.MsgHup})
    }
}

开启新term,进入选举的流程如下:

  1. Step收到MsgHup消息(tickElection发过来的),调用campaign方法(这里会先确认是否有可提交但未执行的关于集群配置变更的消息,若有则不会调用campaign)。另未简化以下均不讨论preVote的情况。
// github.com/coreos/etcd/raft/raft.go
//Step方法处理MsgHup消息,调用campaign方法
func (r *raft) Step(m pb.Message) error {
     switch m.Type {   
  //......
     case pb.MsgHup:
        if r.state != StateLeader {
           //检查是否有未执行的配置变更,大致就是先取出可提交还未执行的这一段,
          //然后检查里面是否有是变更集群配置的消息,如果有则直接return不进入candidate状态。
            ents, err := r.raftLog.slice(r.raftLog.applied+1, r.raftLog.committed+1, noLimit)
            if err != nil {
                r.logger.Panicf("unexpected error getting unapplied entries (%v)", err)
            }
            if n := numOfPendingConf(ents); n != 0 && r.raftLog.committed > r.raftLog.applied {
                r.logger.Warningf("%x cannot campaign at term %d since there are still %d pending configuration changes to apply", r.id, r.Term, n)
                return nil
            }

            r.logger.Infof("%x is starting a new election at term %d", r.id, r.Term)
            if r.preVote {
                r.campaign(campaignPreElection)
            } else {
                r.campaign(campaignElection)
            }
        } else {
            r.logger.Debugf("%x ignoring MsgHup because already leader", r.id)
        }
    }
}
  1. 调用campaign方法,进入candidate状态方法。并向其他节点发送RequestVote RPCs。
// github.com/coreos/etcd/raft/raft.go
// campaign方法首先调用becomeCandidate方法进入candidate状态(暂时不考虑preVote的情况)
func (r *raft) campaign(t CampaignType) {
    var term uint64
    var voteMsg pb.MessageType
    if t == campaignPreElection {
        r.becomePreCandidate()
        voteMsg = pb.MsgPreVote
        // PreVote RPCs are sent for the next term before we've incremented r.Term.
        term = r.Term + 1
    } else {
        //调用becomeCandidate进入candidate状态
        r.becomeCandidate()
        voteMsg = pb.MsgVote
        term = r.Term
    }
 //在发VoteRequest前,节点把票投个自己。如果只有一个节点,则这个时候会胜出
    if r.quorum() == r.poll(r.id, voteRespMsgType(voteMsg), true) {
        // We won the election after voting for ourselves (which must mean that
        // this is a single-node cluster). Advance to the next state.
        if t == campaignPreElection {
            r.campaign(campaignElection)
        } else {
            r.becomeLeader()
        }
        return
    }
  //将自己的lastTerm,lastIndex信息,以及voteMsg即RequestVote RPC发送给其他节点。
    for id := range r.prs {
        if id == r.id {
            continue
        }
        r.logger.Infof("%x [logterm: %d, index: %d] sent %s request to %x at term %d",
            r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), voteMsg, id, r.Term)

        var ctx []byte
        if t == campaignTransfer {
            ctx = []byte(t)
        }
        r.send(pb.Message{Term: term, To: id, Type: voteMsg, Index: r.raftLog.lastIndex(), LogTerm: r.raftLog.lastTerm(), Context: ctx})
    }
}
// github.com/coreos/etcd/raft/raft.go
// candidate状态,设置自己的状态,step方法设为stepCandidate,tick方法设置为tickElection
func (r *raft) becomeCandidate() {
    // TODO(xiangli) remove the panic when the raft implementation is stable
    if r.state == StateLeader {
        panic("invalid transition [leader -> candidate]")
    }
    r.step = stepCandidate
    r.reset(r.Term + 1)
    r.tick = r.tickElection
    r.Vote = r.id
    r.state = StateCandidate
    r.logger.Infof("%x became candidate at term %d", r.id, r.Term)
}
  1. 其他节点收到RequestVote RPC。判断是否可以投给它(是否是重复消息之前也投的该节点;是否自身尚未投票,且目前没有leader。)并判断该节点的日志是否至少和自己一样新。若可以投且至少一样新则回复投票给该节点并记录,否则回复拒绝。
// github.com/coreos/etcd/raft/raft.go
//Step方法处理MsgVote消息,判断投票还是拒绝
func (r *raft) Step(m pb.Message) error {
    //......
    switch m.Type {
   //......
    case pb.MsgVote, pb.MsgPreVote:
        // 初步判断是否可以投票
        //1. 如果自身记录的Vote值和消息的来源者相同,说明是条重复消息
       //2. 如果自身尚未投票,且当前没有leader,则可以投。 
        canVote := r.Vote == m.From ||
            (r.Vote == None && r.lead == None) ||
            // PreVote的情况暂时忽略
            (m.Type == pb.MsgPreVote && m.Term > r.Term)
       //如果初步判断可以投,则看日志是否至少和自己一样新。这里主要是比较lastTerm和lastIndex。
        if canVote && r.raftLog.isUpToDate(m.Index, m.LogTerm) {
            r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] cast %s for %x [logterm: %d, index: %d] at term %d",
                r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), r.Vote, m.Type, m.From, m.LogTerm, m.Index, r.Term)
            
           //判断成功,则把票回复该节点,把票投给它。自身记录Vote,并重设election的计数器。          
            r.send(pb.Message{To: m.From, Term: m.Term, Type: voteRespMsgType(m.Type)})
            if m.Type == pb.MsgVote {
                // Only record real votes.
                r.electionElapsed = 0
                r.Vote = m.From
            }
        } else {
            r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] rejected %s from %x [logterm: %d, index: %d] at term %d",
                r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), r.Vote, m.Type, m.From, m.LogTerm, m.Index, r.Term)
            //否则回复拒绝投票给该节点
            r.send(pb.Message{To: m.From, Term: r.Term, Type: voteRespMsgType(m.Type), Reject: true})
        }
      }
}
  1. candidate节点调用stepCandidate处理各种消息。若收到了其他节点的合法心跳包MsgHeartbeat,AppendEntries消息MsgApp,以及installSnapshot消息MsgSnap,说明当前已有其他节点成为了leader,则自身转为follower状态。若自身赢得选举。即收到大多数的节点的投票。则其转变为leader状态。并发送AppendEntries消息给其他节点。若收到了多数的拒绝消息,则也转化为follower状态。因为在candidate状态时,tick方法也是tickElection方法,依然在记录着electionTimout的超时,所以一段时间后依然没有转化为leader或者follower则会开启新一轮的选举。
// github.com/coreos/etcd/raft/raft.go
// stepCandidate处理收到的消息
func stepCandidate(r *raft, m pb.Message) error {
    // Only handle vote responses corresponding to our candidacy (while in
    // StateCandidate, we may get stale MsgPreVoteResp messages in this term from
    // our pre-candidate state).
    var myVoteRespType pb.MessageType
    if r.state == StatePreCandidate {
        myVoteRespType = pb.MsgPreVoteResp
    } else {
        myVoteRespType = pb.MsgVoteResp
    }
    switch m.Type {
    case pb.MsgProp:
        r.logger.Infof("%x no leader at term %d; dropping proposal", r.id, r.Term)
        return ErrProposalDropped
     // 收到AppendEntries RPC,或者心跳,或者installSnapshot RPC,都说明新leader已经选出来,则自身转变为follower状态
    case pb.MsgApp:
        r.becomeFollower(m.Term, m.From) // always m.Term == r.Term
        r.handleAppendEntries(m)
    case pb.MsgHeartbeat:
        r.becomeFollower(m.Term, m.From) // always m.Term == r.Term
        r.handleHeartbeat(m)
    case pb.MsgSnap:
        r.becomeFollower(m.Term, m.From) // always m.Term == r.Term
        r.handleSnapshot(m)
     //收到的是RequestVote的回复消息。收到一条回复,判断一次是否是多数赞成还是多数反对
    case myVoteRespType:
        // 统计投给自己票的个数
        gr := r.poll(m.From, m.Type, !m.Reject)
        r.logger.Infof("%x [quorum:%d] has received %d %s votes and %d vote rejections", r.id, r.quorum(), gr, m.Type, len(r.votes)-gr)
        switch r.quorum() {
        case gr:
           //多数赞成,则转化为leader,并发送AppendEntries消息给其他节点(preVote情况暂不考虑)
            if r.state == StatePreCandidate {
                r.campaign(campaignElection)
            } else {
                r.becomeLeader()
                r.bcastAppend()
            }
          //多数反对,则转化为follower状态
        case len(r.votes) - gr:
            // pb.MsgPreVoteResp contains future term of pre-candidate
            // m.Term > r.Term; reuse r.Term
            r.becomeFollower(r.Term, None)
        }
    case pb.MsgTimeoutNow:
        r.logger.Debugf("%x [term %d state %v] ignored MsgTimeoutNow from %x", r.id, r.Term, r.state, m.From)
    }
    return nil
}
  1. 某个candidate收到了多数的投票,转化为leader节点。发送空的日志条目的AppendEntries RPC给其他节点。此时选举完成,新的leader产生。leader会通过tickHeartbeat方法向其他节点发送心跳包。
// github.com/coreos/etcd/raft/raft.go
// becomeLeader 设置自身状态为leader,step方法设置为stepLeader,tick方法设置为tickHeartbeat,设置空的日志条目。
func (r *raft) becomeLeader() {
    // TODO(xiangli) remove the panic when the raft implementation is stable
    if r.state == StateFollower {
        panic("invalid transition [follower -> leader]")
    }
    r.step = stepLeader
    r.reset(r.Term)
    r.tick = r.tickHeartbeat
    r.lead = r.id
    r.state = StateLeader

    ents, err := r.raftLog.entries(r.raftLog.committed+1, noLimit)
    if err != nil {
        r.logger.Panicf("unexpected error getting uncommitted entries (%v)", err)
    }
    if len(ents) > 0 {
        r.pendingConfIndex = ents[len(ents)-1].Index
    }

    r.appendEntry(pb.Entry{Data: nil})
    r.logger.Infof("%x became leader at term %d", r.id, r.Term)
}
// github.com/coreos/etcd/raft/raft.go
// leader节点向其他节点发送心跳包。
func (r *raft) tickHeartbeat() {
    r.heartbeatElapsed++
    r.electionElapsed++

    if r.electionElapsed >= r.electionTimeout {
        r.electionElapsed = 0
        if r.checkQuorum {
            r.Step(pb.Message{From: r.id, Type: pb.MsgCheckQuorum})
        }
        // If current leader cannot transfer leadership in electionTimeout, it becomes leader again.
        if r.state == StateLeader && r.leadTransferee != None {
            r.abortLeaderTransfer()
        }
    }

    if r.state != StateLeader {
        return
    }

    if r.heartbeatElapsed >= r.heartbeatTimeout {
        r.heartbeatElapsed = 0
        r.Step(pb.Message{From: r.id, Type: pb.MsgBeat})
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容