本文主要讨论网络分区等场景下各个节点尤其是 leader 节点在做什么以加深对etcd-raft模块的了解。网络分区如上图所示假设在 t1 时刻 s1 是集群的 leader 节点t2 时刻发生网络分区脑裂导致 s1/s2 在分区 As3/s4/s5 在分区 B。此时由于分区 B 中无 leaderB 中的 follower 节点在到达electionTimeout将转换为candidate发起preVote预投票。假设 s3 是 candidate 节点s4/s5 预投票给 s3接着 s3 发起Vote消息并获得 s4/s5 加上自己的投票获得集群超过半数以上5/21投票当选为 leader。这里有几个问题需要思考下旧 leader s1 在做什么需要退位吗为什么需要 preVote 预投票旧 leader 会做什么现在集群中有两个 leader一个分区 A 的旧 leader一个分区 B 的新 leader。由于新 leader 获得多数节点的投票只要正常做 leader 的工作就行。接下来我们把重点放在旧 leader 上看看分区后旧 leader 在做什么。首先旧 leader 不会主动退位它会正常做 leader 的事情。给 follower 发心跳消息。由于网络隔离只有 s2 收到 leader 心跳消息并回复。旧 leader 收到 s2 的回复将 s2 标记为 RecentActive: true。该标记会在一个 electionTimeout 周期性重置leader 通过这个标记判断自己是不是 leader。旧 leader 超过 electionTimeout 会发pb.MsgCheckQuorum进入 raft 状态机判断自己是不是 leader。由于只有 s2 的 标记是 electionTimeout 周期内活跃的其它节点都是不活跃的。raft 判断节点未得到多数节点的响应降级为 follower。这里的关键是 RecentActive 标志位raft 没有根据回复的消息来统计票数确定是否是 leader而是根据统计一个 electionTimeout 周期内 RecentActive 节点数来统计这种滑动窗统计的方式很好的避免了网络延迟拥塞抖动等导致频繁切换 leader 的情况。接下来从源码角度分析这一流程阅读的 etcd 源码版本为release-3.6。旧 leader 发心跳消息给 follower// go.etcd.io/raft/v3/raft.go // tickHeartbeat is run by leaders to send a MsgBeat after r.heartbeatTimeout. func (r *raft) tickHeartbeat() { // 每次发送心跳自增 heartbeatElapsed 和 electionElapsed r.heartbeatElapsed r.electionElapsed // 如果 electionElapsed 超过 electionTimeout则发起 pb.MsgCheckQuorum // 确认自己是否是 leader if r.electionElapsed r.electionTimeout { // 一旦进入确认逻辑重置 electionElapsed下一次继续确认 r.electionElapsed 0 // checkQuorum 默认打开 if r.checkQuorum { // 进入节点状态机处理 pb.MsgCheckQuorum 消息 if err : r.Step(pb.Message{From: r.id, Type: pb.MsgCheckQuorum}); err ! nil { r.logger.Debugf(error occurred during checking sending heartbeat: %v, err) } } // If current leader cannot transfer leadership in electionTimeout, it becomes leader again. if r.state StateLeader r.leadTransferee ! None { r.abortLeaderTransfer() } } // 如果 节点降级成 follower 则返回只有 leader 可以执行 tickHeartbeat 方法 if r.state ! StateLeader { return } // 如果还是 leader 并且 heartbeatElapsed 超过 heartbeatTimeout // 重置 heartbeatElapsed继续 follower 发心跳消息 if r.heartbeatElapsed r.heartbeatTimeout { r.heartbeatElapsed 0 if err : r.Step(pb.Message{From: r.id, Type: pb.MsgBeat}); err ! nil { r.logger.Debugf(error occurred during checking sending heartbeat: %v, err) } } }这段函数流程如注释所示有两点需要注意的是tickHeartbeat 是哪里触发的electionElapsed 变量有什么作用第一个问题tickHeartbeat 是上层应用层触发应用层维护一个定时器定时器周期性的往 tick 通道内写数据算法层node消费tick通道然后将请求发送给raft.tickHeartbeat。具体的流程可参考 raft 工程化案例之 etcd 源码实现 写的很好很详细就不赘述了。第二个问题electionElapsed 对于 follower 来说是比较好理解的变量如果 follower 收到心跳等消息它会重置 electionElapsed。表示现在 leader 还在安心做好 follower 就行。对于 leader 来说leader 用这个变量是为了表明如果 leader 超过 electionTimeout它会发pb.MsgCheckQuorum消息给自己的raft来判断自己是不是合法 leader。实际是复用了这个变量做了不同的事情。follower 接收到心跳消息并回复// go.etcd.io/raft/v3/raft.go func stepFollower(r *raft, m pb.Message) error { switch m.Type { case pb.MsgProp: ... case pb.MsgHeartbeat: // follower 接收到 leader 的心跳消息 // 重置 electionElapsed r.electionElapsed 0 // 只有 leader 可以发送 MsgHeartbeat 消息 // 将本机的 raft.lead From r.lead m.From r.handleHeartbeat(m) } return nil } func (r *raft) handleHeartbeat(m pb.Message) { r.raftLog.commitTo(m.Commit) // 发送心跳回复消息给 leader r.send(pb.Message{To: m.From, Type: pb.MsgHeartbeatResp, Context: m.Context}) }leader 接受心跳回复消息// go.etcd.io/raft/v3/raft.go func stepLeader(r *raft, m pb.Message) error { ... switch m.Type { case pb.MsgHeartbeatResp: // 将 follower 节点的 RecentActive 设为 true // 表示该节点在 electionTimeout 周期内是活跃的 pr.RecentActive true pr.MsgAppFlowPaused false ... } ... }这里 leader 并没有统计回复心跳消息的票数而是将返回心跳消息的 follower 节点的 RecentActive 标记为 true。leader 根据这个标记判断 follower 节点的活跃状态。那么 leader 是在哪里退位的呢leader 退位答案还是在tickHeartbeat函数。当 electionElapsed 累积到超过 electionTimeout 时进入r.Step(pb.Message{From: r.id, Type: pb.MsgCheckQuorum})。// go.etcd.io/raft/v3/raft.go func (r *raft) Step(m pb.Message) error { ... switch m.Type { ... default: err : r.step(r, m) if err ! nil { return err } } return nil }进入 leader 自己的状态机处理pb.MsgCheckQuorum消息// go.etcd.io/raft/v3/raft.go func stepLeader(r *raft, m pb.Message) error { switch m.Type { case pb.MsgCheckQuorum: // 进入 raft.trk.QuorumActive 判断 leader 的 follower 是不是活跃的 if !r.trk.QuorumActive() { // 如果不活跃则降级成 follower r.logger.Warningf(%x stepped down to follower since quorum is not active, r.id) r.becomeFollower(r.Term, None) } // Mark everyone (but ourselves) as inactive in preparation for the next // CheckQuorum. r.trk.Visit(func(id uint64, pr *tracker.Progress) { // 不管活跃不活跃都要重置节点的 RecentActive 标记 // 这一步非常重要每个统计周期就是根据它来判断 leader 是否合法 // 所以每个 electionTimeout 统计周期都要重置该标记 if id ! r.id { pr.RecentActive false } }) return nil } // 根据节点的 RecentActive 标记判断 leader 是否合法 func (p *ProgressTracker) QuorumActive() bool { votes : map[uint64]bool{} p.Visit(func(id uint64, pr *Progress) { if pr.IsLearner { return } votes[id] pr.RecentActive }) return p.Voters.VoteResult(votes) quorum.VoteWon }可以看出leader发pb.MsgCheckQuorum消息给自己如果 leader 的 follower 节点活跃数未超过半数以上则 leader 将降级成 follower。为什么需要 preVote还是回到分区的示例中在分区 B 中 s3 发起预投票投票最终当选为 leader。从这个流程并没有看出 preVote 的优势有多大我们把目光集中在分区 A 中。假设分区 A 中 s1 降级成 follower分区 A 中有两个 follower s1 和 s2。其中某一个 follower假设是 s2 到达 electionTimeout。如果没有 preVotes2 会转成 candidate 状态自增 term假设当前 term10成 11// go.etcd.io/raft/v3/raft.go 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 // 这里很重要candidate 节点会自增自己的 term
当 leader 被隔离: etcd 网络分区深度分析
发布时间:2026/6/29 23:24:16
本文主要讨论网络分区等场景下各个节点尤其是 leader 节点在做什么以加深对etcd-raft模块的了解。网络分区如上图所示假设在 t1 时刻 s1 是集群的 leader 节点t2 时刻发生网络分区脑裂导致 s1/s2 在分区 As3/s4/s5 在分区 B。此时由于分区 B 中无 leaderB 中的 follower 节点在到达electionTimeout将转换为candidate发起preVote预投票。假设 s3 是 candidate 节点s4/s5 预投票给 s3接着 s3 发起Vote消息并获得 s4/s5 加上自己的投票获得集群超过半数以上5/21投票当选为 leader。这里有几个问题需要思考下旧 leader s1 在做什么需要退位吗为什么需要 preVote 预投票旧 leader 会做什么现在集群中有两个 leader一个分区 A 的旧 leader一个分区 B 的新 leader。由于新 leader 获得多数节点的投票只要正常做 leader 的工作就行。接下来我们把重点放在旧 leader 上看看分区后旧 leader 在做什么。首先旧 leader 不会主动退位它会正常做 leader 的事情。给 follower 发心跳消息。由于网络隔离只有 s2 收到 leader 心跳消息并回复。旧 leader 收到 s2 的回复将 s2 标记为 RecentActive: true。该标记会在一个 electionTimeout 周期性重置leader 通过这个标记判断自己是不是 leader。旧 leader 超过 electionTimeout 会发pb.MsgCheckQuorum进入 raft 状态机判断自己是不是 leader。由于只有 s2 的 标记是 electionTimeout 周期内活跃的其它节点都是不活跃的。raft 判断节点未得到多数节点的响应降级为 follower。这里的关键是 RecentActive 标志位raft 没有根据回复的消息来统计票数确定是否是 leader而是根据统计一个 electionTimeout 周期内 RecentActive 节点数来统计这种滑动窗统计的方式很好的避免了网络延迟拥塞抖动等导致频繁切换 leader 的情况。接下来从源码角度分析这一流程阅读的 etcd 源码版本为release-3.6。旧 leader 发心跳消息给 follower// go.etcd.io/raft/v3/raft.go // tickHeartbeat is run by leaders to send a MsgBeat after r.heartbeatTimeout. func (r *raft) tickHeartbeat() { // 每次发送心跳自增 heartbeatElapsed 和 electionElapsed r.heartbeatElapsed r.electionElapsed // 如果 electionElapsed 超过 electionTimeout则发起 pb.MsgCheckQuorum // 确认自己是否是 leader if r.electionElapsed r.electionTimeout { // 一旦进入确认逻辑重置 electionElapsed下一次继续确认 r.electionElapsed 0 // checkQuorum 默认打开 if r.checkQuorum { // 进入节点状态机处理 pb.MsgCheckQuorum 消息 if err : r.Step(pb.Message{From: r.id, Type: pb.MsgCheckQuorum}); err ! nil { r.logger.Debugf(error occurred during checking sending heartbeat: %v, err) } } // If current leader cannot transfer leadership in electionTimeout, it becomes leader again. if r.state StateLeader r.leadTransferee ! None { r.abortLeaderTransfer() } } // 如果 节点降级成 follower 则返回只有 leader 可以执行 tickHeartbeat 方法 if r.state ! StateLeader { return } // 如果还是 leader 并且 heartbeatElapsed 超过 heartbeatTimeout // 重置 heartbeatElapsed继续 follower 发心跳消息 if r.heartbeatElapsed r.heartbeatTimeout { r.heartbeatElapsed 0 if err : r.Step(pb.Message{From: r.id, Type: pb.MsgBeat}); err ! nil { r.logger.Debugf(error occurred during checking sending heartbeat: %v, err) } } }这段函数流程如注释所示有两点需要注意的是tickHeartbeat 是哪里触发的electionElapsed 变量有什么作用第一个问题tickHeartbeat 是上层应用层触发应用层维护一个定时器定时器周期性的往 tick 通道内写数据算法层node消费tick通道然后将请求发送给raft.tickHeartbeat。具体的流程可参考 raft 工程化案例之 etcd 源码实现 写的很好很详细就不赘述了。第二个问题electionElapsed 对于 follower 来说是比较好理解的变量如果 follower 收到心跳等消息它会重置 electionElapsed。表示现在 leader 还在安心做好 follower 就行。对于 leader 来说leader 用这个变量是为了表明如果 leader 超过 electionTimeout它会发pb.MsgCheckQuorum消息给自己的raft来判断自己是不是合法 leader。实际是复用了这个变量做了不同的事情。follower 接收到心跳消息并回复// go.etcd.io/raft/v3/raft.go func stepFollower(r *raft, m pb.Message) error { switch m.Type { case pb.MsgProp: ... case pb.MsgHeartbeat: // follower 接收到 leader 的心跳消息 // 重置 electionElapsed r.electionElapsed 0 // 只有 leader 可以发送 MsgHeartbeat 消息 // 将本机的 raft.lead From r.lead m.From r.handleHeartbeat(m) } return nil } func (r *raft) handleHeartbeat(m pb.Message) { r.raftLog.commitTo(m.Commit) // 发送心跳回复消息给 leader r.send(pb.Message{To: m.From, Type: pb.MsgHeartbeatResp, Context: m.Context}) }leader 接受心跳回复消息// go.etcd.io/raft/v3/raft.go func stepLeader(r *raft, m pb.Message) error { ... switch m.Type { case pb.MsgHeartbeatResp: // 将 follower 节点的 RecentActive 设为 true // 表示该节点在 electionTimeout 周期内是活跃的 pr.RecentActive true pr.MsgAppFlowPaused false ... } ... }这里 leader 并没有统计回复心跳消息的票数而是将返回心跳消息的 follower 节点的 RecentActive 标记为 true。leader 根据这个标记判断 follower 节点的活跃状态。那么 leader 是在哪里退位的呢leader 退位答案还是在tickHeartbeat函数。当 electionElapsed 累积到超过 electionTimeout 时进入r.Step(pb.Message{From: r.id, Type: pb.MsgCheckQuorum})。// go.etcd.io/raft/v3/raft.go func (r *raft) Step(m pb.Message) error { ... switch m.Type { ... default: err : r.step(r, m) if err ! nil { return err } } return nil }进入 leader 自己的状态机处理pb.MsgCheckQuorum消息// go.etcd.io/raft/v3/raft.go func stepLeader(r *raft, m pb.Message) error { switch m.Type { case pb.MsgCheckQuorum: // 进入 raft.trk.QuorumActive 判断 leader 的 follower 是不是活跃的 if !r.trk.QuorumActive() { // 如果不活跃则降级成 follower r.logger.Warningf(%x stepped down to follower since quorum is not active, r.id) r.becomeFollower(r.Term, None) } // Mark everyone (but ourselves) as inactive in preparation for the next // CheckQuorum. r.trk.Visit(func(id uint64, pr *tracker.Progress) { // 不管活跃不活跃都要重置节点的 RecentActive 标记 // 这一步非常重要每个统计周期就是根据它来判断 leader 是否合法 // 所以每个 electionTimeout 统计周期都要重置该标记 if id ! r.id { pr.RecentActive false } }) return nil } // 根据节点的 RecentActive 标记判断 leader 是否合法 func (p *ProgressTracker) QuorumActive() bool { votes : map[uint64]bool{} p.Visit(func(id uint64, pr *Progress) { if pr.IsLearner { return } votes[id] pr.RecentActive }) return p.Voters.VoteResult(votes) quorum.VoteWon }可以看出leader发pb.MsgCheckQuorum消息给自己如果 leader 的 follower 节点活跃数未超过半数以上则 leader 将降级成 follower。为什么需要 preVote还是回到分区的示例中在分区 B 中 s3 发起预投票投票最终当选为 leader。从这个流程并没有看出 preVote 的优势有多大我们把目光集中在分区 A 中。假设分区 A 中 s1 降级成 follower分区 A 中有两个 follower s1 和 s2。其中某一个 follower假设是 s2 到达 electionTimeout。如果没有 preVotes2 会转成 candidate 状态自增 term假设当前 term10成 11// go.etcd.io/raft/v3/raft.go 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 // 这里很重要candidate 节点会自增自己的 term