尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

集群脑裂与多数派Quorum选举:分布式系统一致性的关键防线

集群脑裂与多数派Quorum选举:分布式系统一致性的关键防线 凌晨一点半监控大屏突然炸了。业务群里有人喊“分布式锁失效了”后台日志里出现了两个节点同时认为自己才是集群主节点的记录。等网络抖动恢复数据一比对才发现同一把锁被两边各写了一次关键业务字段互相覆盖损失已经无法挽回。这种事故圈里人叫它集群脑裂split brain而绝大多数脑裂事故的化解方案绕不开一个核心机制多数派 Quorum 选举。作为一个常年跟分布式系统打交道的工程师我可以直说脑裂不是一个“万一遇到”的小概率问题它是任何多节点系统在设计时都必须正面回答的问题。这篇内容围绕“集群脑裂多数派 Quorum 选举”展开重点讲清楚脑裂的本质、多数派为什么能救场、选举机制如何落地以及我踩过的坑和现场排障经验。适合正在做分布式存储、消息队列、配置中心、注册中心这类基础组件的开发同学也适合负责运维分布式集群的 SRE 和架构师阅读。1. 先搞懂脑裂一场令人头疼的分布式灾难1.1 什么是脑裂用生活场景理解它互联网公司里任何分布式系统都由多个节点组成。节点之间通过网络传递心跳、同步数据、协商主从关系。脑裂的本质就是“网络分区”导致集群内部出现两派节点各自都认为自己是合法的主控方彼此无法协商最终各干各的。举个例子。你家有两个管家平时一个管账、一个管物配合得很好。但有一天他们之间用来沟通的对讲机坏了。A 管家联系不上 B 管家B 管家也联系不上 A 管家。两边都担心对方“不在了”于是各自开始全权处理家里所有事务。等对讲机修好两个人一对账发现钱和东西都对不上了——这就是脑裂。在分布式系统里“对讲机”就是节点之间的网络连接“管家”就是集群中的节点角色。网络抖动、交换机故障、网卡异常、长停顿比如垃圾回收暂停、磁盘 IO 卡顿都可能导致节点之间的心跳丢失。一旦心跳丢失超过阈值节点就会认为同伴“死”了于是尝试重新选举试图让自己成为新的主节点。但与此同时旧主节点如果还活着它并不知道自己被“下线”了继续接受写请求。这时两个“主节点”同时存在脑裂发生了。1.2 脑裂为什么可怕不只是多了一个主节点很多人第一反应是脑裂无非就是多选出一个主节点把其中一个降级不就行了但实际上问题要严重得多。最典型的伤害是多个主节点同时写入同一个数据项。比如配置中心里有条配置项switchon旧主节点把它改成switchoff新主节点把它改成switchon。两边都成功写入本地存储。等到网络恢复、两边尝试同步数据时发现同一个 key 有两个不同的值到底以谁为准任何自动合并规则都可能产生错误。再比如分布式锁。基于 Redis 的分布式锁、基于 ZooKeeper 的临时节点锁在脑裂时都可能出现两个客户端同时拿到锁的情况。对资金操作、库存扣减这类场景双写一次可能就会造成严重的事故。我见到过不止一次因为脑裂导致的资损事故往往事后要花大量时间人工对账、修复脏数据痛苦至极。更麻烦的是脑裂发生时不一定会立刻暴露问题。如果两个分区各自正常运行没有发生对同一 key 的写入表面上看集群是健康的。直到网络恢复后开始做数据对齐才发现两个分区的数据出现了不可调和的矛盾。这种滞后性使得排障更加困难因为你很难确定事故是从哪一刻开始的。1.3 哪些场景最容易触发脑裂根据我的经验下面这些场景是最常见的高危区网络设备异常交换机单点故障、光纤不稳定、防火墙误拦心跳端口。虚拟化环境抖动宿主机负载过高导致虚拟机网络中断或者宿主机迁移期间网络不可用。长 GC 暂停JVM 应用 Full GC 长达十几秒节点间心跳超时。磁盘 IO 卡顿本地磁盘写入速度骤降节点“假死”。网络分区维护机房断电、链路割接、跨地域专线质量不稳定。只要心跳机制存在上述任何一种情况都可能触发集群的重新选举逻辑。如果没有 Quorum 机制兜底后果不堪设想。反过来有了 Quorum 机制即使某些节点失联也无法轻易形成两个“合法主节点”这是分布式系统设计的第一道防线。2. 多数派 Quorum解决脑裂的数学底线2.1 Quorum 是什么一句话版本Quorum中文常译为“法定人数”或“仲裁”。在一个 N 个节点的集群里任何一项需要达成共识的操作都必须获得超过半数节点即N/2 1个节点的认可才能被认为是合法的。这句话听着很简单但它背后的作用是革命性的在任何网络分区场景下永远不可能同时存在两个“超过半数”的派别。因为两个派别若要各自达到多数派节点总数加起来至少需要N 2个这在只有 N 个节点的集群里不可能发生。数学上保证了“多数派”的唯一性也就避免了脑裂后出现两个合法的决策源。我举个具体例子。集群有 5 个节点网络抖动导致两个节点和另外三个节点失联。此时前两个节点即使想选主最多只有 2 票不够法定人数后三个节点有 3 票可以顺利选主。于是只有后者能合法选举产生主节点。两个节点的分区因为达不到多数派会一直停留在 Candidate 状态或退化为只读。等到网络恢复两个节点再重新加入集群并同步数据。这就是 Quorum 的威力。2.2 为什么集群设计成奇数节点做分布式系统选型时很多人会问为什么大多数中间件推荐 3 个、5 个节点而不是 2 个、4 个答案就在多数派容忍度和成本权衡之间。用 2 个节点举例。两个节点的多数派是 2因为2/2 1 2只要一个节点挂了剩下的一个节点就无法达到多数派整个集群就停了毫无高可用性。3 个节点时多数派是 2允许 1 个节点宕机而不影响集群运行。5 个节点时多数派是 3允许 2 个节点宕机。可以总结为2N1 个节点最多容忍 N 个节点故障。相比之下4 个节点的集群多数派是 3也只允许 1 个节点故障和 3 个节点的容忍度一样却多买了一台机器。所以从成本和容错能力的角度看奇数节点总是更经济的方案。这也是 Raft、Zab、etcd、Consul 等系统普遍建议奇数成员的原因。2.3 不只是写操作读操作同样需要 Quorum很多人忽略了一个点Quorum 不只在选举主节点时生效在读写数据时也有对应的约束。分布式共识算法里写操作需要获得多数派节点确认才能提交而读操作如果不想读到过期数据也需要与写操作形成某种交集。具体来说假设集群有 5 个节点写 Quorum 设为 W比如 3读 Quorum 设为 R比如 3。只要R W 5就能保证一次读操作至少能读到一次写操作已经落盘的数据。极端情况下读操作读取的那几个副本中可能没有最新数据但由于与写 Quorum 有交集至少有一个节点返回了新值配合版本号比较就能拿到最新结果。这一条在选择存储系统副本策略时很关键。有些系统允许配置R1只读主副本此时读性能很高但读到过期的风险也高。有些系统配置RWN所有人都要参与安全性最高但可用性大打折扣。真实业务里需要根据一致性需求去权衡 R 和 W 的大小。2.4 Quorum 背后的假设先谈投票权再谈多数严格来说多数派并不总是按“节点个数”计算也可以按“权重”计算。比如一个节点磁盘更大、网络更好、承担更多数据分片可以给它更高的投票权重。此时 Quorum 的计算就要基于权重的总和过半而不是节点数量过半。但这种做法在实践中相对少见因为它增加了管理复杂度一旦权重配置不合理很容易出现某个节点权重过高但它宕机后整个集群满足不了多数派的情况。大多数开源系统选择“一节点一票”的简单模型。如果你在设计自研系统建议也遵循简单原则先把一节点一票跑通再考虑权重这类增强特性。3. 选举机制Quorum 如何从理论走向落地3.1 旧主节点的心跳租约如何失效选举的第一步是让旧主节点腾出位置。如果旧主节点一直没有感知到自己失联它可能继续以主节点身份对外服务。Quorum 机制中通常引入“租约”概念主节点只有在租约有效期内才能执行写操作而租约需要节点们定期续约。比如一个基于 Raft 的系统Leader 会周期性发送心跳给其他节点Follower 收到心跳就刷新本地计时器。假设选举超时时间设置为 5 秒如果 Follower 超过 5 秒没有收到 Leader 的心跳它就会认为 Leader 可能已经失效开始发起新一轮选举。这里的“5 秒”就是一种租约。哪怕旧 Leader 实际还活着但由于它无法在超时时间内联系到 Follower它也不能继续合法地写入。等网络恢复后旧 Leader 会发现自己已不是当前任期的 Leader必须转化为 Follower 角色接受新 Leader 的日志同步。这个过程中有一个非常关键的机制叫fencing token隔离令牌每次选举成功都会生成一个单调递增的令牌旧 Leader 持有的令牌已经过期。如果旧 Leader 在恢复后还想写入它必须用新令牌因此无法提交旧数据防止了脑裂后旧主节点继续写脏数据。3.2 任期与投票完整选举流程以 Raft 为例一次典型的选举过程可以拆成下面几步Follower 在选举超时时间内没有收到 Leader 心跳进入 Candidate 状态。Candidate 将当前任期号 Term 1并给其他节点发送 RequestVote RPC请求对方投票给自己。每个节点同一个任期只能投一票通常先来先得并且只投票给日志足够新的候选者。如果 Candidate 收到超过半数的投票它成为新 Leader。新 Leader 立即开始发送心跳巩固自己的地位同时重置其他节点的选举定时器。这里有个细节必须注意节点并不是盲目投票的。Raft 要求候选者的日志至少不能落后于自己否则它可能丢失已提交数据。日志新的判定规则是比较最后一个日志条目的任期号和索引号任期号大的更新任期号相同则索引号大的更新。如果不检查这一步一个日志落后的节点被选为 Leader就可能把已提交的数据覆盖掉造成严重的数据丢失。ZooKeeper 的 Zab 协议在选主时也有类似机制。ZooKeeper 里每个事务都有一个 ZXIDZooKeeper Transaction ID包含逻辑时钟和事务序号选主时会选择 ZXID 最大的节点优先成为主节点目的同样是避免数据丢失。3.3 投票统计为什么必须严格过半回到多数派的本质投票统计必须严格超过N/2而非等于N/2。比如 4 节点集群多数派是 3如果误以为 2 就是多数派那就可能出现两个 2 节点的分区各自选主脑裂依旧会发生。“严格过半”确保了任何两个多数派集合必有交集这是共识算法正确性的基石。那么如果恰好两个候选者各获得一半投票怎么办比如 4 节点集群中 A、B 各得 2 票都不足 3 票选举失败两个候选者在超时后重新发起新选举。为了避免两个节点长期僵持系统通常加入随机超时时间。每个候选者等待一个随机的重新选举超时时间比如 150ms 到 300ms 之间谁先超时就先发起新选举大概率能打破僵局。这也是 Raft 随机 election timeout 的设计初衷。3.4 不只选一次日志复制时也需要 Quorum选主只是 Quorum 的一种应用。一个集群在正常运行期间Leader 需要把客户端请求写入日志并复制到各个节点。每一条日志的提交同样需要多数派节点确认写入成功。这体现了一个更底层的规律Quorum 是一个贯穿共识算法、数据复制、状态机同步的统一原则。Leader 可以没有到多数派就选不出来日志不能到多数派就不能提交只有提交后的日志才能应用到状态机。理解了这一点你就明白为什么很多系统会发生“写延迟增加”这种情况——因为每笔写都要等多数派节点返回 ACK而不是只等主节点写本地磁盘。我见过很多人在调优时问能不能降低写 Quorum只要一个节点返回就提交答案是可以但会牺牲一致性。如果那个唯一的节点又宕机了数据可能就永久丢失。除非业务能接受这样的数据风险否则最好让 Quorum 保持多数派。4. 实际系统里的 Quorum 选主从 etcd 到 ZooKeeper4.1 etcd 选主参数与心跳调整etcd 是云原生世界里最常见的分布式键值存储底层基于 Raft 算法。和选主相关的两个核心参数是heartbeat-interval和election-timeout。heartbeat-intervalLeader 给 Follower 发送心跳的间隔默认 100ms。election-timeoutFollower 等待 Leader 心跳的超时时间默认 1000ms实际取值范围是 heartbeat-interval 到两倍之间。这两个参数需要配合调整。如果心跳间隔太短系统会频繁发送心跳消耗网络和 CPU如果太长故障感知变慢集群恢复时间变长。如果集群跨地域部署网络 RTT 较高可能需要把心跳间隔调到 200ms 甚至更高不然心跳频繁超时会导致 Leader 频繁切换。调整参数时要注意election-timeout一般要大于心跳间隔的 5 倍以上避免网络抖动引起选举风暴。以 etcd 的默认值来说100ms 的心跳配合 1000ms 的选举超时已经留出了较大余量。但如果你的网络质量不好建议用官方文档推荐的--heartbeat-interval500 --election-timeout5000这类组合。4.2 ZooKeeper 的 Quorum 机制和角色演进传统 ZooKeeper 集群使用 Zab 协议角色分为 Leader、Follower 和 Observer。选举时只有 Follower 和 Leader 有投票权Observer 只负责处理读请求不参与投票。这一点常常让新人困惑为什么加了 Observer 不能提升选主性能因为 Observer 的目的在于扩展读能力而非提升共识能力。ZooKeeper 里的核心超时参数是tickTime以及initLimit和syncLimit。其中initLimit是 Follower 初始连接 Leader 并完成同步的最大时间syncLimit是 Follower 与 Leader 之间心跳的最大延迟时间。如果集群节点分布在不同机房需要相应调大这些参数。ZooKeeper 3.8 之后还引入了QuorumVerifier可以灵活配置投票权重但默认还是等权。4.3 主流中间件的 Quorum 参数速查为了方便平时查阅我整理一个常用系统 Quorum 选主相关的速查表覆盖同类开源系统在选举与多数派方面的关键配置系统共识协议默认多数派计算关键参数特别说明etcdRaftN/2 1heartbeat-interval, election-timeout节点数建议奇数最少 3 节点ZooKeeperZabN/2 1tickTime, initLimit, syncLimitObserver 不参与投票可扩展读ConsulRaftN/2 1HeartbeatTimeout, ElectionTimeout多数据中心场景要小心跨地域选举KafkaKRaftRaftN/2 1controller.quorum.election.timeout.ms新版本不再依赖 ZooKeeper控制器选主关乎分区元数据Redis Sentinel自研选主逻辑N/2 1按 Sentinel 数量down-after-milliseconds, failover-timeoutSentinel 数量建议奇数防止两个数据中心独立选主ClickHouse内置多主 选主如 keeperN/2 1依赖 Keeper 的 election_timeout老版本 zoo 专家调度新版本推荐 ClickHouse Keeper这里要补充一点上面这些系统的多数派计算都是基于“节点数量”如果你部署成偶数节点同样可能发生两个候选者各拿一半票导致选举僵持。因此部署时不要把节点数搞成偶数特别是在跨机房双活场景下更要注意避免两个机房的节点数对半开。5. 现场排障脑裂问题识别与恢复实录5.1 判断脑裂的信号有哪些脑裂发生时系统通常会出现以下异常表现同一时刻有两个节点声称自己是 Leader。在 etcd 中可能看到etcdserver: leader changed日志反复出现或者raft.node提示不同节点的 Leader ID 不一致。分布式锁偶尔加锁成功但业务侧同时出现两个执行者持有锁。如果锁基于 ZooKeeper可能出现两个客户端都创建了临时节点且都认为自己获取锁成功。监控图中出现 Write/Read 失败率突增Leader 频繁切换或者某些节点一直在进行选举循环。数据不一致不同节点查询同一个 key 返回不同值或客户端连接的节点各不相同导致看到的数据视图不一致。值得注意的是脑裂期间集群不一定会完全不可用可能只是部分请求失败。如果某个分区不足多数派但包含客户端连接的节点客户端写请求就会失败因为系统在等待多数派确认时永远等不到响应。这类“无响应”也是脑裂的常见信号。5.2 排障步骤日志、网络、状态三连当你怀疑集群发生脑裂我建议按以下步骤做检查顺序很重要第一步看日志在 etcd 中搜索election、leader、request vote关键字在 ZooKeeper 中查看LEADER、FOLLOWER、LOOKING状态切换记录。日志往往能直接告诉你节点当时在做什么。第二步看网络检查节点之间的 TCP 连接是否正常用ping、telnet或nc测试目标端口。重点观察是否有丢包、延迟突增。跨机房部署时还要检查专线质量。很多“脑裂”其实就是网络抖动触发的误判。第三步看状态用系统自带的健康接口查看集群成员状态。etcd 可以用etcdctl endpoint status --cluster查看每个节点的 Leader IDZooKeeper 用mntr命令或 4 字命令stat查看当前角色的归属。如果发现两个节点分属不同 Leader基本可以确认脑裂已经发生。这里我特别强调一个排查技巧不要只看单个节点的日志要把集群中所有节点同一时间段的日志拉出来对齐。脑裂本身就是“多个节点视角不一致”只盯一台机器容易误判。5.3 恢复与修复合并数据的正确姿势脑裂恢复后首要任务是停止脏写入然后进行数据合并或回滚。具体做法取决于你的业务模型如果是配置数据通常希望保留携带最新版本号的数据。如果系统支持 MVCC 或多版本控制你可以对比两个分区的数据版本以最新者为准回滚掉旧数据。如果系统不支持版本号那就需要人工根据业务语义决定保留哪个分区的值。如果是分布式锁数据重点是检查是否有业务动作在此期间执行。锁数据本身可以删除但执行业务产生的外部影响需要单独排查。比如库存扣减记录、资金变动流水这些需要结合业务日志和数据库审计进行对账。如果是有状态的数据存储恢复过程会更痛苦。一个可接受的方案是选择包含最多已提交日志的分区作为最终数据源另一个分区的增量数据按业务规则进行重放或丢弃。Raft 类和 Zab 类系统本身会通过日志复制自动合并如果旧 Leader 的数据落后会以新 Leader 为准回放。手动恢复时务必备份所有分区的数据再操作且恢复过程要保证新 Leader 只接受完整多数派仲裁后的请求。5.4 预防脑裂的三道防线第一道防线是 Quorum 选主按多数派原则确保不会出现两个合法主节点。这是标配所有生产集群都必须有。第二道防线是隔离机制也就是 fence 能力。选主成功后新 Leader 必须能够阻止旧 Leader 继续写入。很多系统通过令牌机制实现比如生成单调递增的 epoch 或 term写入请求中携带该标识若标识低于当前合法值则拒绝。如果你的自研系统没有实现这一层网络恢复后的“旧主回写”是极大的隐患。第三道防线是架构设计。尽量避免把偶数节点集群部署成两个机房对半分布因为这在大分区场景下很容易形成“两边一边一半”的局面。更合理的方式是奇数节点跨机房分布或者容忍单机房整体不可用。此外自动化监控也必不可少。盯住 Leader 任期变化频率、心跳超时次数、节点状态切换记录一旦指标偏离基线立刻告警把脑裂扼杀在早期。6. 避坑指南与实用建议6.1 三个最容易踩的坑第一个坑是随意调整选举超时参数。很多人为了降低故障切换时间把心跳间隔调得非常短结果正常网络波动也会引起选举集群频繁抖动可用性反而下降。记住一个原则超时参数要能容忍正常情况下的网络抖动而且要经过压测验证再上生产。第二个坑是使用偶数节点部署。有些团队总觉得 4 节点比 3 节点更“安全”实际上 4 节点只允许 1 个节点故障跟 3 节点一样却增加了一台机器的成本而且 4 节点在节点两两分组时容易出现两个 2 票阵营机器越多反而越容易出现选主僵局。第三个坑是脑裂恢复后直接人工干预没有先做数据备份。有些同学一看脑裂恢复了就马上把旧 Leader 的库删掉或者直接回滚日志结果新 Leader 的数据本身也有缺失导致二次事故。任何恢复动作前先备份这是保命操作。6.2 推荐落地的配置检查清单结合我的经验下面一份清单可以帮你快速核对集群配置是否健康集群节点数是否为奇数3、5、7 等最少不要低于 3。部署节点是否分散在至少两个故障域机架/机房避免单点故障拖垮整个集群。心跳和选举超时时间是否对当前网络状况留有余量。是否验证过“某个节点宕机集群仍然可写”的场景。是否测试过“网络分区后少数派节点不会对外提供写服务”。是否配置了 Leader 任期变化的监控告警。是否设计了 fence 机制保证旧 Leader 在租约过期后无法继续写入。是否定期演练脑裂恢复流程确保团队实际操作熟练。我建议把上面这些项目作为上线前的硬性检查而不是等出事了再回头查。6.3 一个可以立刻上手的验证方法如果你有测试环境想验证一个系统是否具备脑裂防护能力有个简单粗暴的方法用 Linux 的iptables或tc命令人为制造网络分区。模拟步骤如下在某个 Follower 节点上执行iptables -A INPUT -s leader_ip -j DROP模拟它与 Leader 的单向失联。观察该节点是否会主动开始选主它的选主是否会成功。如果选主失败因为无法获得多数派说明 Quorum 机制正常。恢复网络后观察该节点是否自动重新加入集群并同步数据。这个实验能快速暴露配置错误也能帮助团队理解 Quorum 的运作边界。更重要的是通过这样的演练整个团队对脑裂的应对会更有底气。真正的稳定性不是祈祷不故障而是故障来了也能安全兜底。
返回列表