Raft选举机制解析
前言
Etcd是一个强一致性的分布式架构,即CP,所有请求必须经过leader节点,先由leader节点向follower节点发送日志同步消息,经过二阶段提交最终将数据应用到状态机。因此集群在初始化时必须有个选主的过程。
Etcd节点有以下三种角色:
Follower
集群初始化时,都是follower节点,follower节点负责以下几个功能:
- 接收leader节点的日志同步请求。
- 接收竞选节点的投票请求。
Candidate
每个follower节点都有一个超时时间,当过了这个超时时间一直没有收到leader节点的心跳,则会成为竞选节点,向其他节点发起投票请求。
Leader
所有的读写请求都需要经过leader节点,当竞选节点竞选成功后,会将角色置为leader,follower节点会从该节点同步最新的日志,以保证整个集群的一致性。
选举流程
-
-
follower节点收到投票请求后会根据投票规则决定是否投票,并将投票结果(赞成/反对)返回给竞选节点。
-
竞选节点发起投票请求时,会向所有follower节点发送自己日志的任期和索引号, follower节点收到后会比较自己的日志是否比竞选节点的新,先比较任期,任期大的的日志最新,如果任期一样,则比较索引号,索引号大的日志最新。
预选举
raft算法中,竞选节点在选举之前会先把自己的任期加1,然后发起投票请求,那如果此时出现了网络分区,如下图所示:
当Follower_2在达到electionTimeout后还没收到leader的心跳,会触发选举,并转为Candidate。每次发起选举时,会把Term加1。由于网络隔离,它既不会被选成Leader,也不会收到Leader的消息,而是会一直不断地发起选举。Term会不断增大,这会产生什么问题呢?
在网络恢复之后,因为Follower_2还是处于竞选中,它这会把它的Term传播到集群的其他节点,其他节点认为自己的日志比它旧,就肯定会选它为leader,,但事实上Follower_2节点的日志可能会落后其他节点很多了,显然是不应该成为leader节点的。那如何避免这种情况发生呢?
raft算法对竞选机制进行了改良,就是所谓的预选举。Candidate首先要确认自己要能赢得集群中大多数节点的投票,这样才会把自己的term增加,然后发起正式的投票,如果预选举不通过,则该节点的term不会增加。关键逻辑如下:
// becomePreCandidate()方法 func (r *raft) becomePreCandidate() { if r.state == StateLeader { panic("invalid transition [leader -> pre-candidate]") } r.step = stepCandidate r.prs.ResetVotes() r.tick = r.tickElection r.lead = None r.state = StatePreCandidate r.logger.Infof("%x became pre-candidate at term %d", r.id, r.Term) } //becomeCandidate()方法 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) //一旦变为竞选角色,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) }
可以看到预选举时Term并不会增加,而是等进入正式选举时Term才会增加。