news 2026/6/29 23:23:59

当 leader 被隔离: etcd 网络分区深度分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
当 leader 被隔离: etcd 网络分区深度分析

本文主要讨论网络分区等场景下各个节点,尤其是 leader 节点在做什么,以加深对etcd-raft模块的了解。

网络分区

如上图所示,假设在 t1 时刻 s1 是集群的 leader 节点,t2 时刻发生网络分区(脑裂)导致 s1/s2 在分区 A,s3/s4/s5 在分区 B。

此时,由于分区 B 中无 leader,B 中的 follower 节点在到达electionTimeout将转换为candidate发起preVote预投票。假设 s3 是 candidate 节点,s4/s5 预投票给 s3,接着 s3 发起Vote消息,并获得 s4/s5 加上自己的投票,获得集群超过半数以上(5/2+1)投票,当选为 leader。

这里有几个问题需要思考下:

  1. 旧 leader s1 在做什么?需要退位吗?
  2. 为什么需要 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) } } }

这段函数流程如注释所示,有两点需要注意的是:

  1. tickHeartbeat 是哪里触发的?
  2. 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 }

可以看出leaderpb.MsgCheckQuorum消息给自己,如果 leader 的 follower 节点活跃数未超过半数以上则 leader 将降级成 follower。

为什么需要 preVote?

还是回到分区的示例中,在分区 B 中 s3 发起预投票,投票最终当选为 leader。从这个流程并没有看出 preVote 的优势有多大,我们把目光集中在分区 A 中。

假设分区 A 中 s1 降级成 follower,分区 A 中有两个 follower s1 和 s2。其中某一个 follower(假设是 s2) 到达 electionTimeout。

如果没有 preVote,s2 会转成 candidate 状态,自增 term(假设当前 term=10)成 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
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/29 23:22:17

3分钟上手开源医院信息系统:让医疗管理变得简单又高效

3分钟上手开源医院信息系统:让医疗管理变得简单又高效 【免费下载链接】HIS HIS英文全称 hospital information system(医院信息系统),系统主要功能按照数据流量、流向及处理过程分为临床诊疗、药品管理、财务管理、患者管理。诊疗…

作者头像 李华
网站建设 2026/6/29 23:16:33

基于MSPA与ArcGIS的生态源地识别与核心区提取实战

1. 生态源地识别与MSPA方法基础 生态源地识别是景观生态学中的关键环节,它直接影响到生态网络构建的准确性。MSPA(形态学空间格局分析)作为一种基于数学形态学的景观格局分析方法,能够将复杂的景观结构分解为7种基本空间模式&…

作者头像 李华
网站建设 2026/6/29 23:15:11

Unity Mod Manager:轻松管理Unity游戏模组的终极指南

Unity Mod Manager:轻松管理Unity游戏模组的终极指南 【免费下载链接】unity-mod-manager UnityModManager 项目地址: https://gitcode.com/gh_mirrors/un/unity-mod-manager 还在为Unity游戏模组安装的复杂性而烦恼吗?Unity Mod Manager&#xf…

作者头像 李华
网站建设 2026/6/29 23:11:40

如何编写一个SpringBoot项目告警推送的Starter

最近有一点时间了,于是便开始做以前自己想做但是没有完成的事情。之前我其实就一直想写一个通用一点的告警推送组件,把项目里的异常信息、慢请求、状态码异常、JVM 指标,甚至数据库慢 SQL 这些内容统一收集起来,然后直接推送到飞书…

作者头像 李华
网站建设 2026/6/29 23:09:25

索尼相机逆向工程终极指南:PMCA-RE深度解析与实战应用

索尼相机逆向工程终极指南:PMCA-RE深度解析与实战应用 【免费下载链接】Sony-PMCA-RE Reverse Engineering Sony Digital Cameras 项目地址: https://gitcode.com/gh_mirrors/so/Sony-PMCA-RE 索尼数码相机逆向工程工具PMCA-RE是一款功能强大的开源工具&…

作者头像 李华