当前位置: 首页 > news >正文

当 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
http://www.jsqmd.com/news/1092384/

相关文章:

  • 从一个 “笨办法“ 说起
  • # Rocky Linux 9.5 搭建 Kafka + ELK 完整日志平台技术文档
  • 番外篇 F05:电机控制与PID调节实战《电机控制中的PID调节:位置式/增量式算法解析与使用场景全攻略》
  • 拼多多运营整体框架(2026 最新精细化玩法)
  • 【无标题】实训平台基础软件基于自研Docker容器编排管理引擎,运用云原生和容 器技术构建训练环境
  • 【实战解析】从噪声到特征:ECG信号预处理与智能筛选全流程拆解
  • 5大架构设计原则:深入剖析React Icons开源项目架构
  • 第86题 2026年国家级科研痛点——碳化硅(SiC)单晶衬底缺陷控制与扩径技术
  • MSPM0微控制器GPAMP与VREF模块:构建高精度模拟信号链的实战指南
  • Grad-CAM实战:从理论到热力图生成
  • WPF现代化界面开发架构解析:HandyControls控件库核心技术实现与性能优化指南
  • 正则表达式详解(C++20 )
  • 这些宝藏级在线工具,让你的效率原地起飞
  • HarmonyOS技术精讲-应用间跳转:一键调用系统能力(系统应用跳转)
  • 鹤壁企业采购白酒,怎么选得知道
  • Unity Mod Manager:轻松管理Unity游戏模组的终极指南
  • 专业级暗黑3战斗自动化工具深度解析:5大核心功能实战指南
  • 大麦网Python自动化抢票系统:技术架构与实战应用解析
  • MSP432硬件调试实战:适配器与插座板配置详解
  • 戴森球计划3000+工厂蓝图终极指南:从新手到专家的完整解决方案
  • TrollInstallerX突破性指南:iOS 14-16.6.1设备快速部署TrollStore的实战手册
  • HarmonyOS技术精讲-应用间跳转:跨应用传递数据与返回结果
  • Java高并发编程核心原理:程序员进阶必会!
  • Docker--Docker引擎与镜像相关命令
  • 完整学习LLM(五):Embedding是什么,为什么文本能变成向量
  • 【infra之路】10-PagedAttention 与 KV Cache 管理
  • 配置中心:为什么需要它?如何选型?
  • 开源社区新动态,Github 上值得关注的 ROCm 项目推荐
  • 有限域原根求解:Python实现与数学原理
  • 3分钟掌握AI智能分层:Layerdivider让单图变多层的终极指南