ZooKeeper集群角色详解:Leader、Follower、Observer与Quorum机制
ZooKeeper集群里最常见的三个角色是Leader、Follower和Observer但绝大多数运维人员对它们的理解都停留在“主节点管写、从节点管读、观察节点只读”这种粗略层面。我曾经接手过一个线上ZooKeeper集群5个节点业务方反馈写操作偶尔卡顿我第一反应是查负载、查网络、查磁盘IO折腾了半天才发现问题根子在角色配置上某个Observer节点因为配置错误悄悄参与了投票把原本3节点的Quorum硬生生拖成了5节点写确认路径被拉长性能自然不对劲。那次排查之后我意识到一个道理在没彻底弄清这三种角色的真实职责、投票规则和故障切换路径之前任何调优手段都像蒙着眼睛改参数。这篇文章不打算堆概念而是从一个维护者的角度把ZooKeeper集群里Leader、Follower、Observer各自负责什么、通信怎么走、选举时谁说了算、Observer为什么不能提高容错这些事讲透顺带把部署和故障排查中最容易踩的坑一并梳理出来。无论你是在搭Hadoop HA、Kafka、还是被公司内部ZK集群的告警缠住这份整理应该都能帮你少走弯路。1. 角色划分的底层逻辑三种身份对应三种不同的系统诉求1.1 ZooKeeper要解决的核心问题是什么ZooKeeper对外提供的本质能力是一份所有节点都能看到、且保持一致性的分布式状态视图。分布式锁、配置中心、服务发现、集群元数据管理这些场景全都依赖同一个前提无论客户端连上集群中的哪台机器拿到的状态都必须是有序的、可信的。举个常见例子Kafka的Topic分区状态、Broker上下线信息依赖ZooKeeper保存一份全局可见的元数据HBase用ZooKeeper维护RegionServer的在线状态和Master选举。如果后台不同节点之间的状态各说各话整个系统就会乱套。正因为这样ZooKeeper必须有一种机制确保所有状态变更按照同一个顺序发生并且已经被足够多的节点确认。“顺序一致”这件事天然需要一个单点来拍板这就是Leader存在的根本原因。没有Leader就没有明确的事务编号来源A节点认为事务1先发生B节点认为事务2先发生两边永远对不上。所以ZooKeeper不是简单的主从复制而是通过一个被多数节点认可的Leader把写入变成唯一的、串行化的事件流再广播给所有节点。1.2 三个角色的本质区别在读写路径和投票资格Leader、Follower、Observer的差别可以归纳成两条线读请求谁来响应、写请求谁能处理以及谁有投票资格。Leader是整个集群写入的唯一入口所有写请求无论从哪个节点进来最终都必须到Leader这里生成事务并广播。Follower可以承载读请求也可以参与投票但写请求它自己处理不了必须转发给Leader。Observer是纯只读节点可以接受客户端读请求不参与任何投票也不会被选为Leader。打个比方Leader像公司里唯一有审批权的部门经理所有合同必须经过他签字生效Follower像是各分公司的营业员客户可以来查资料但要办业务还是得把资料转给总部审批Observer则更像是客服中心的分机可以解答咨询但如果需要变更合同条款它也只能帮你转接。理解这个分工比死记角色名称有用得多。1.3 一台机器的角色是由配置文件决定的不是跑起来才判断的ZooKeeper集群节点的角色定义在zoo.cfg里通过server.X配置项声明。典型的配置长这样server.1zk1.example.com:2888:3888 server.2zk2.example.com:2888:3888 server.3zk3.example.com:2888:3888 server.4zk4.example.com:2888:3888:observer前三行都是默认的投票节点角色为Follower或Leader第四行末尾多了:observer标识这表示4号节点是Observer。每台机器还有自己的myid文件里面写入的是一个数字比如4它和server.4对应。启动时ZooKeeper根据myid找到自己对应的配置才能确定自己是普通节点还是Observer。很多人第一次搭集群会忽略这里的一个关键点2888:3888这两个端口不是随便写的。2888是Follower和Observer连接Leader用的同步端口事务日志、快照同步都走它3888是集群成员之间进行Leader选举投票用的端口。配置错误、防火墙只放行了其中一个端口都会造成集群无法正常同步或者无法选举。2. Leader一切状态变更的起点也是写性能瓶颈所在2.1 Leader是如何被选出来的ZooKeeper刚启动时或者运行的Leader意外失联时集群内的投票节点会进入选举状态。每个节点都会投出自己的一票推举某个节点当Leader选票规则不是看谁先启动而是比较两个关键维度ZXID事务ID和myid。ZXID是ZooKeeper内部的事务编号64位高32位是epoch任期编号低32位是事务计数器。节点会优先投票给ZXID更大的节点因为ZXID越大意味着它保存了越新的数据状态如果ZXID相同再比较myidmyid大的节点胜出。这种设计能最大限度保证新选出的Leader不丢数据。选举需要拿到多数票注意是“投票节点中的多数”Observer没有投票权所以即使集群里混着10个Observer实际选举结果也只由那3个或5个投票节点决定。这一点非常重要忘掉它后面排查问题会走很多弯路。2.2 Leader处理写请求的完整链路客户端把写请求发给任意节点如果发给Follower或Observer它们会把这个请求原样转发给Leader而不是自己直接改数据。Leader收到请求后做三件事生成一个全局唯一的事务ZXID把事务广播给所有投票节点收集多数确认后提交。具体流程大致是Leader给事务标记ZXID同步给FollowerFollower收到后写入本地事务日志返回ACKLeader收到超过半数的ACK后广播Commit消息各节点把事务应用到内存数据库然后Leader才向客户端返回成功。这个“先过半确认、再提交”的过程本质是为了保证宕机的节点恢复后可以从日志里补回状态不会出现“一部分节点执行了、另一部分节点没执行”的分叉。代价是每一次写操作都要经过两轮网络通信所以集群里投票节点越多写延迟通常会越高。这也是为什么我不建议为了扩展读能力而一味增加Follower节点数量。2.3 Leader失效时会发生什么Leader挂掉或失去与多数节点的连接时集群进入新一轮选举。此时所有写请求都会立刻报错或超时因为没有任何节点能生成新事务读请求在Follower上还能继续但读到的是旧状态。新Leader选出之后会加载本地快照并重放事务日志中尚未落快照的事务把状态追到最新然后开始接收新的写请求。整个中断时间在正常网络和硬件条件下通常在秒级以内但如果选举中节点之间网络不通、磁盘IO卡顿时间可能被拉长到十几秒甚至更久。我在实际运维中最常看到的Leader失效原因不是真正的宕机而是节点之间的网络抖动或者某一台机器因为GC停顿、磁盘打满导致心跳超时。别的节点觉得它“死了”发起选举等它恢复过来发现Leader已经换了。所以监控里重点关注的不只是进程存活还有节点的心跳延迟和选举次数。3. Follower承载读流量和参与投票的“骨干”3.1 Follower的读请求处理能读但不保证最新Follower收到读请求后直接基于本地内存数据库返回结果不再去问Leader要数据。这个设计让读请求可以水平分散到多个Follower上吞吐量比单点强很多。代价是它返回的数据可能不是最新的。假设一个写请求刚提交给Leader其他Follower还没来得及应用事务此时读某个节点的数据拿到的是旧值。ZooKeeper通过Watch机制通知客户端数据变化但通知不代表你在变化前读到的值一定是最新。如果业务对“读到最新数据”有硬性要求有两条路可选一是把读请求强制打到Leader节点上让Leader直接返回二是客户端先发送sync命令强制Follower追上Leader的最新状态然后再读。前者会牺牲部分负载均衡能力后者会影响性能具体怎么选要看业务对强一致的容忍度。3.2 Follower转写请求不是性能优化是协议强制很多刚接触ZooKeeper的人会疑惑为什么Follower不能直接把写请求写到本地再异步同步给其他节点答案很简单如果每个Follower都能本地写入两个写请求在不同节点上执行事务顺序就不可控了最终数据收敛不到同一个状态。所以Follower的角色是“承上启下”收到写请求后立刻转发给当前Leader由Leader生成唯一ZXID并广播提交。Follower自己在这个过程中做的事情更像是“执行者和确认者”它要负责把Leader广播的事务写入本地日志、返回ACK、应用Commit。这一点和MySQL常见的读写分离不太一样。MySQL从库可以本地落盘再异步复制到主库位置但ZooKeeper因为要求强一致性必须所有事务都经过Leader统一编号。理解这个差异再看“为什么写请求必须在Leader处串行处理”就不会觉得多余了。3.3 Follower投票参与的具体行为Follower在两类场景里会行使投票权事务提交确认和Leader选举。事务提交确认要求“过半ACK”选举也要求“过半票数”。所谓过半指的是投票节点总数的一半以上比如5个投票节点就要拿到至少3票3个投票节点就要拿到至少2票。Follower投票的意义在于给“多数派决策”提供基数。删掉一个Follower容错能力下降加一个Follower写确认延迟上升。所以集群节点数不是拍脑袋定的它直接决定系统能承受的故障数和写性能。实际部署时常见的投票节点数量是3、5、7。3节点容1个故障5节点容2个7节点容3个。继续往上加容错收益递减写性能损失却肉眼可见。因此读需求量大时更合理的做法是保持投票节点数不变增加Observer节点来扛读流量。4. Observer只扩展读能力不掺和投票的“旁观者”4.1 为什么需要Observer这种角色如果单纯为了扩展读能力最直观的办法是加Follower但前面说过加Follower会增大Quorum反而拖慢写请求。Observer就是为了解决这个矛盾而设计的它可以像Follower一样处理读请求、同步Leader的增量事务但完全不具备投票资格。这意味着集群可以无限加Observer来分摊读压力同时投票节点数量保持不变写性能几乎不受影响。对“读多写少”的场景比如配置中心、服务发现、一些元数据查询Observer几乎是标配。Observer的另一个典型应用场景是跨机房部署。比如总部机房放3个投票节点外地机房放几个Observer本地客户端可以就近读Observer拿到大部分配置信息不需要每次都跨地域走网络减少读等待时间。它不参与选举所以不会因为跨地域的网络延迟扰动整个集群的投票过程。4.2 Observer的同步机制与特殊之处Observer的配置方式是在zoo.cfg的server.X行末尾增加:observer后缀如前面所写。它启动后同样会连接Leader处理事务日志同步和应用这个流程和Follower基本一致。差别集中在投票上Observer不参与事务ACK不参与Leader选举也不上报自己是否存活给选区。在Leader看来Observer更像一个“带读能力的日志消费者”它需要知道最新事务但它不回确认所以它的存在与否不影响集群提交事务。如果Observer和Leader之间断连它会一直保存着断开前的数据并继续服务读请求但读到的数据会越来越旧。所以对于强调数据实时性的业务不能只把Observer当廉价读库用需要配合监控观察Observer和Leader之间的同步延迟。4.3 Observer扩容时容易踩的坑我给团队做ZK集群扩容指导时发现最常见的一个误区是把Observer当成“增加容错”的手段。有人在3节点集群基础上加了2个Observer以为现在能容忍2台机器故障了其实容错能力一点都没变因为Observer的票数等于零。另一个常见问题是客户端连接串到Observer上但未开启只读模式或者在Observer上执行写请求时得到异常。写请求发到Observer后Observer依然会把请求转发给Leader所以正常情况下写请求不会失败但要关注Observer上的连接数、请求积压避免它成为单点瓶颈。还有一点Observer节点同样需要落盘快照和事务日志。如果只有它自己保存数据哪天需要它顶上时数据就断档了。虽然它不投票但它作为一个数据副本日志和快照策略应当与其他Follower保持一致。5. Quorum和Zab协议集群行为的底层规则5.1 Quorum到底怎么算Quorum即“法定数量”在ZooKeeper中指的是至少多少票才能完成提交或选举。计算公式是投票节点总数除以2再加1向下取整。3节点Quorum是25节点是36节点是47节点是4。这里有个很典型的坑节点总数用偶数到底行不行。理论上4节点能容忍1个故障但一旦发生网络分区可能被分成两个各2节点的分区两个分区都凑不齐Quorum3集群直接失去写入能力。奇数节点则能尽量保证任意分区场景下最多只有一个分区满足Quorum。所以生产环境建议用奇数这句话不是玄学是网络分区下的数学结论。还要注意Quorum的基数永远是“投票节点”Observer不算数。如果你配置了3个投票节点加若干Observer上面所有对容错和写延迟的分析都以3为基数展开Observer多到天上也不影响Quorum。5.2 为什么写请求必须等“过半确认”假设Leader把事务广播给A、B、C三个Follower只有A确认了B和C还没确认此时Leader就返回成功。接着Leader宕机剩下的节点中A有这条事务B和C没有。选举时若B、C联合选出新Leader这条事务就丢失了客户端以为自己写入成功实际上数据没了。如果所有节点都确认了再返回任何节点被选为Leader时数据都在不会丢。但这样写延迟太大而且只要一台慢机器就能拖住全局。于是ZooKeeper采用“过半确认”只要超过半数的节点确认了事务剩下的少数节点即使丢失数据也无法在多数派主导的选举中胜出因为多数派里总有包含该事务的节点。Zab协议就是在这个前提下实现状态机复制的它保障所有正常节点按同一顺序应用事务即使某些节点崩溃、延迟、慢速最终也能追上最新状态。理解“过半”这个数字等于理解ZooKeeper一致性模型的一半。5.3 和“脑裂”的对抗逻辑脑裂指一个集群被网络分成两个独立运作的部分每部分都认为自己是唯一合法的集群都对外提供服务最终数据不一致。ZooKeeper防止脑裂靠的正是Quorum任何一个节点要想继续对外提供服务必须能与过半投票节点保持连通不满足这个条件的部分只能进入只读或不可用状态。比如5个节点被分成23两个分区3节点那边满足Quorum能选Leader、能写2节点那边凑不齐3票无法选举写请求直接失败。系统宁可短暂不可用也不能让两边都活着写数据。这也是ZooKeeper与Redis哨兵、Redis Cluster在脑裂处理上思路不同的地方。Redis的故障转移策略相对轻量追求快速切换ZooKeeper则把一致性和防脑裂放在更优先的位置。运维时如果看到ZK集群部分节点报出连接不到多数节点判断它是否还能写原则上只要看它是否满足Quorum即可。6. 故障场景和排查建议从现象到根因的完整链路6.1 单节点故障看似没事其实在透支容错一台Follower宕机集群还能正常工作因为剩余投票节点仍满足Quorum。但容错额度已经用掉了1/N后续再有一个节点故障集群可能直接瘫痪。这类故障最难排查的点在于“表面上没有报错”。写请求还能成功读请求也正常但潜在风险已经积累。所以监控上不能只看进程和端口还要持续跟踪节点心跳状态、同步延迟、选举次数。四字命令是我用得最多的排查工具echo mntr | nc zk1 2181mntr可以返回当前节点的角色、zk_sync_peers、zk_pending_syncs等关键指标。看到角色不对或者同步对端数量异常基本能定位角色配置或者连接问题。6.2 Leader重选时业务中断的排查Leader重选期间写请求全部不可用所以一旦监控发现写超时第一件事就该确认是不是集群正在选举。查看方式很多最直观的是四字命令输出里大量节点处于looking状态或者日志里出现LEADER ELECTION TOOK ... ms。选举耗时是不是正常取决于日志同步、磁盘、连接数。我遇到过几次新Leader反复选不出来的情况根因多集中在磁盘IO被日志写满拖垮、节点内存不足触发频繁GC、网卡丢包导致投票消息大量超时。解决思路不是去调选举算法参数而是先修硬件和网络底座再保证每个节点有独立的磁盘和足够的JVM堆。6.3 网络分区导致的“假死”问题网络分区下最容易出现“假Leader”现象一台节点与多数派断开但客户端还能连上它它也不知道自己已经不是合法集群成员仍尝试对外提供写服务结果客户端拿到的响应和真正集群不一致。ZooKeeper通过fencing机制防止这种失控当节点发现无法连接多数派时即使进程还活着也必须退出正常服务状态不再响应写请求。所以看到节点进程存活但写失败不要先怀疑代码看看它是否处于分区状态。排查手段包括检查节点之间2888和3888端口的连通性看是否有丢包检查节点上的防火墙策略是否误伤了内网通信还要确认是否有某些节点被单独隔离在另一个网段。网络分区是分布式系统最难处理的故障之一靠逻辑判断比靠重启靠谱重启只会丢掉自己的投票身份和状态视图。6.4 配置文件引发的角色错乱ZooKeeper角色配置错误非常隐蔽通常不会直接报错而是体现成“明明加了Observer选举却变慢了”这类间接问题。检查时优先看zoo.cfg的server.X行确认Observer行末尾有:observer并且所有投票节点都没有这个后缀。还要检查myid文件是否与配置对应比如server.4对应myid内容为4否则节点可能无法正确识别自己的角色。如果节点主机名变了zoo.cfg里同步更新hostname或IP避免连接走错地址同时确保事务日志目录和快照目录分离写日志和写快照竞争同一块磁盘时对性能的影响非常大。7. 从维护角度我的几点实操体会真正把这些角色吃透之后再看ZooKeeper集群的部署和运维会清晰很多3个投票节点保底读量上来优先加Observer写性能紧张先排查硬件和GC而不是盲目动集群拓扑。每次扩容都是重新审视Quorum配置的好时机不要让改造动作自己变成故障源。如果只能从这篇文章带走一句话我想说ZooKeeper集群里的每个角色都有明确边界Observer不投票、Follower不拍板、Leader不丢序所有边界共同构成一个能对抗故障、防止脑裂的一致性系统。盲目加节点不会自动提升可靠性只会改变你为这份可靠性付出的代价。我在维护ZooKeeper的过程中越来越发现分布式系统的很多故障其实不是算法不够高级而是操作人员对基础角色的职责理解有偏差把不该写的节点配成了可写把不该投票的节点拉进了选举。搞清楚本文梳理的这层底层逻辑后续排障、调优和扩容会轻松得多。