当前位置: 首页 > 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.gsyq.cn/news/1604349.html

相关文章:

  • 从像素到光点:基于SSD1306 OLED的动态光源控制与传感应用
  • HarmonyOS技术精讲-应用间跳转:精确控制跳转目标(显式跳转)
  • 【Vid-Agent】长视频理解VideoTemp-o3框架
  • TI TAS2559智能功放评估板硬件解析与上手指南
  • 打破进口垄断!云克隆推出肠道七因子高通量检测全新方案
  • Grad-CAM实战:从理论到热力图生成
  • 【实战解析】从噪声到特征:ECG信号预处理与智能筛选全流程拆解
  • 拼多多运营整体框架(2026 最新精细化玩法)
  • 【无标题】实训平台基础软件基于自研Docker容器编排管理引擎,运用云原生和容 器技术构建训练环境
  • 正则表达式详解(C++20 )
  • 戴森球计划3000+工厂蓝图终极指南:从新手到专家的完整解决方案
  • Unity Mod Manager:轻松管理Unity游戏模组的终极指南
  • Docker--Docker引擎与镜像相关命令
  • 【infra之路】10-PagedAttention 与 KV Cache 管理
  • 3分钟掌握AI智能分层:Layerdivider让单图变多层的终极指南
  • 5分钟快速上手:diff-pdf - 免费开源的PDF差异检测神器
  • 基于SQL实现分组的文字排序聚合
  • 8-EnBoT-SORT:面向高密度热红外无人机的层次化融合关联追踪与伪样本生成方法
  • 高速接口静电防护:ESD器件选型与电容考量实战
  • 泛化管理化技术模板与泛型编程
  • GEO代理总部提供售后支持吗
  • Splunk Enterprise高危漏洞CVE-2024-36991深度剖析与复现指南
  • 如何在Kodi上免费搭建115网盘云端影院:终极观影解决方案
  • AXI DMA实战:从ZYNQ PS到PL的高效数据通路构建【Vivado设计】
  • 研究背景:解决视频世界模型的“长时漂移”问题
  • 软件设计的模块划分与接口定义
  • 最新量化初学四步走,概念代码回测模拟别混在一起
  • 2.1 java面试题:说一说springcloud 的组件作用和各个组件之间是如何写作的。
  • 工业以太网PHY芯片TLK10xL外围电路设计与PCB布局实战指南
  • 如何彻底告别网盘限速:8大平台免费直链下载加速终极指南