问题在下一场景中讨论),数量达不到quorum的分区中的leader会退位,且该分区中的节点永远都无法选举出leader,因此该分区的节点的term会不断增大。当该分区与整个集群的网络恢复后,由于开启了Check Quorum / Leader Lease,即使该分区中的节点有更大的term,由于原分区的节点工作正常,它们的选举请求会被丢弃。同时,由于该节点的term比原分区的leader节点的term大,因此它会丢弃原分区的leader的请求。这样,该节点永远都无法重新加入集群,也无法当选新leader。(详见issue #5451、issue #5468)。
场景2: Pre-Vote机制也有类似的问题。如上图所示,假如发起预投票的节点,在预投票通过后正要发起正式投票的请求时出现网络分区。此时,该节点的term会高于原集群的term。而原集群因没有收到真正的投票请求,不会更新term,继续正常运行。在网络分区恢复后,原集群的term低于分区节点的term,但是日志比分区节点更新。此时,该节点发起的预投票请求因没有日志落后会被丢弃,而原集群leader发给该节点的请求会因term比该节点小而被丢弃。同样,该节点永远都无法重新加入集群,也无法当选新leader。(详见issue #8501、issue #8525)。
场景3: 在更复杂的情况中,比如,在变更配置时,开启了原本没有开启的Pre-Vote机制。此时可能会出现与上一条类似的情况,即可能因term更高但是log更旧的节点的存在导致整个集群的死锁,所有节点都无法预投票成功。这种情况比上一种情况更危险,上一种情况只有之前分区的节点无法加入集群,在这种情况下,整个集群都会不可用。(详见issue #8501、issue #8525)。
为了解决以上问题,节点在收到term比自己低的请求时,需要做特殊的处理。处理逻辑也很简单:
- 如果收到了term比当前节点term低的leader的消息,且集群开启了Check Quorum / Leader Lease或Pre-Vote,那么发送一条term为当前term的消息,令term低的节点成为follower。(针对场景1、场景2)
- 对于term比当前节点term低的预投票请求,无论是否开启了Check Quorum / Leader Lease或Pre-Vote,都要通过一条term为当前term的消息,迫使其转为follower并更新term。(针对场景3)
2. etcd中Raft选举的实现
2.1 发起vote或pre-vote流程
2.1.1 Election timeout
在集群刚启动时,所有节点的状态都为 follower
,等待超时触发 leader election
。超时时间由 Config
设置。etcd/raft
没有用真实时间而是使用逻辑时钟,当调用 tick
的次数超过指定次数时触发超时事件。 对于 follower
和 candidate
而言,tick
中会判断是否超时,若超时则会本地生成一个 MsgHup
类型的消息触发 leader election
:
// tickElection is run by followers and candidates after r.electionTimeout.
func (r *raft) tickElection() {
r.electionElapsed++
if r.promotable() && r.pastElectionTimeout() {
r.electionElapsed = 0
r.Step(pb.Message{From: r.id, Type: pb.MsgHup})
}
}
2.1.2 MsgHup消息处理与hup方法
etcd/raft通过raft
结构体的Step
方法实现Raft状态机的状态转移。Step
方法是消息处理的入口,不同 state
处理的消息不同且处理方式不同,所以有多个 step
方法:
raft.Step()
: 消息处理的入口,做一些共性的检查,如 term
,或处理所有状态都需要处理的消息。若需要更进一步处理,会根据状态 调用下面的方法:
raft.stepLeader()
: leader
状态的消息处理方法;
raft.stepFollower()
: follower
状态的消息处理方法;
raft.stepCandidate()
: candidate
状态的消息处理方法。
func (r *raft) Step(m pb.Message) error {
// ... ...
switch m.Type {
case pb.MsgHup:
if r.preVote {
r.hup(campaignPreElection)
} else {
r.hup(campaignElection)
}
// ... ...
}
// ... ...
}
Step
方法在处理MsgHup
消息时,会根据当前配置中是否开启了Pre-Vote
机制,以不同的CampaignType
调用hup
方法。CampaignType
是一种枚举类型(go语言的枚举实现方式),其可能值如下表所示。
值 |
描述 |
campaignPreElection |
表示Pre-Vote的预选举阶段。 |
campaignElection |
表示正常的选举阶段(仅超时选举,不包括Leader Transfer)。 |
campaignTransfer |
表示Leader Transfer阶段。 |
接下来对hup
的实现进行分析。
func (r *raft) hup(t CampaignType) {
if r.state == StateLeader {
r.logger.Debugf("%x ignoring MsgHup because already leader", r.id)
return
}
if !r.promotable() {
r.logger.Warningf("%x is unpromotable and can not campaign", r.id)
return
}
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
}
r.logger.Infof("%x is starting a new election at term %d", r.id, r.Ter