ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ZooKeeper观察者节点全解析:读写分离的读扩展利器

ZooKeeper观察者节点全解析:读写分离的读扩展利器 1. 先搞清楚一个反直觉的问题ZooKeeper 到底卡在读还是卡在写1.1 读写路径的真相读不走共识写必须走共识老有人问我“ZooKeeper 性能瓶颈不都在写吗搞读扩展有什么意义”这个想法对了一半但恰恰会让人错过 ZooKeeper 集群调优里最值钱的一块。先说写。ZooKeeper 的写请求无论客户端连到哪个节点最终都会被转发给 Leader然后 Leader 向所有参与投票的 Follower 广播提案等收到过半 ACK 之后才 Commit。这一整套过程依赖 Zab 协议走的是共识路径延迟对网络和磁盘同步都很敏感。再说读。读请求完全不走共识。客户端连上任意一个节点这个节点直接用本地内存中的数据副本把结果返回给客户端。没有网络交互、没有投票、没有 Leader 参与。也就是说读操作的极限吞吐量基本由“单个节点自身的内存读取能力 网络吞吐”决定。问题恰恰出在这里写虽然慢但它在一个没写压力的系统里根本不是瓶颈读虽然快但读流量一大单节点分分钟被打满。很多业务的 ZooKeeper 集群长年累月没有多少写入量但有几百上千个业务实例在启动时同时读配置、听 Watcher这种场景下你会眼睁睁看着某一台节点的 CPU 冲到 90% 以上而整个集群的写路径一点事都没有。这时候直觉会告诉你加节点把读请求分散开。1.2 投票节点越多写路径越容易“钝”那加 Follower 行不行行但有代价。Follower 是参与选举和投票的参与节点每加一个 Follower共识组的规模就变大一圈。Zab 协议的投票算法虽然只等多数派 ACK并不需要等所有人但集群节点越多Leader 需要维护的会话、事务日志同步、心跳检测就越多网络开销和选举耗时有明显增长。有一个很经典的教训早年我接触过一套集群运维为了扛住读流量从 3 个节点加到 7 个节点读是稳了但后来某次机房抖动触发重新选举7 个节点的选举耗时比 3 节点多了接近一倍。业务那边对写链路要求极其敏感直接炸了。这个矛盾的本质是读容量和写性能在同一个资源池里抢位置。你要么多花钱堆机器要么寻找一种“只出力、不站队”的节点形态。ZooKeeper 早就给了答案观察者节点Observer。它的定位就是被设计用来干这件事的——不参与投票、不参与选举只同步数据、提供本地读服务。下面我会把这个角色的底层机制、配置方法、性能边界和踩坑经验一次说透。2. 观察者节点的定位一个只干活不投票的成员2.1 它在 Zab 协议里扮演的角色观察者节点的是机制要追溯到 ZooKeeper 3.4.0 引入 Learner 模型。官方文档里对 Observer 的定义很直接不参与 Leader 选举投票不参与写请求的提交投票只作为学习节点从 Leader 接收数据变更并应用于本地状态。放到 Zab 协议里Observer 处于非常特殊的生态位它不是候选者永远不会被选举为 Leader它没有投票权任何集群变更的计算都不需要等它点头它照样维护完整的 znode 树、Session、Watcher客户端连上来一切功能正常它还是会转发写请求给 Leader只是自己不参与后续的提案确认环节。这种设计在运行机制上让我想起“读副本”的概念但它比读副本更轻因为 ZooKeeper 本身不需要像数据库那样做 binlog 回放或者 MVCCObserver 只需要把 Leader 的提交记录按顺序往本地状态机上套即可。2.2 数据同步路径Observer 是怎么拿到最新数据的Observer 与 Leader 之间的数据同步走的是和 Follower 相同的 LearnerHandler 通道。Observer 启动并完成 Leader 发现后会向 Leader 发起 FOLLOWERINFO 请求。Leader 测出它与最新提交位置之间的差距如果差距不算大就通过增量事务日志同步DIFF如果差距过大比如 Log 已经被清理或者 Observer 长时间离线Leader 会直接发快照SNAP之后继续追加增量。关键区别在提交确认环节。Follower 收到提案后需要落盘并向 Leader 返回 ACKObserver 不落盘提案、不返回 ACK只在收到 Leader 的 commit 消息后像“追剧”一样把事务应用到本地 DataTree。这意味着 Observer 的数据更新天然存在一个异步窗口某个写请求已经 Commit 成功并返回客户端了但 Observer 可能还差那么几个事务没追上。这么说吧Follower 相当于“大家一起拍板之后各自干活”Observer 相当于“你们拍板我只看结果”。所以它非常轻但代价是你得接受读到的版本可能不是最新。2.3 为什么加 Observer 不会拖慢写这个问题我在前司分享的时候用一句话解释过写路径的耗时取决于“Leader 发起提案后等多数派 ACK 的时间”。在 ZooKeeper 的设计里只有参与投票的 Follower 才会占用这个等待窗口。Observer 不在多数派里所以它的存在对写路径的时延和非功能性损耗都趋近于零。加 Follower 就像开会多拉了一个必须表态的参会人人数越多达成共识越慢。加 Observer 就像会议室后面多坐了一个旁听的他可以看资料、可以做笔记但不参与表决会议该多久还是多久。这也是观察者节点在处理读扩展时非常有价值的核心原因你可以横向加一堆 Observer把读流量摊出去而不用提升写路径的成本。前提是控制好 Observer 与 Leader 之间的同步网络质量。3. 什么场景下观察者节点真的值钱3.1 跨机房部署场景一到跨机房Follower 的问题立刻变得很碍眼。假设你有两个机房 A 和 B为了容灾在 A 部署两个节点、B 部署两个节点加上 Leader 在 A一共五节点。任何跨机房的写请求都要等四个 Follower 里的多数派 ACKB 机房两个节点只要网络抖动一下写延迟就能翻几倍。这时把 B 机房的节点改成 Observer 就合理了读流量留在 B 本地写流量还是要发往 A 机房的 Leader 处理但异步窗口对 B 机房读业务的瞬时可用性影响并不大。这本质上是在“读的本地性”和“写的跨机房一致性要求”之间取得平衡。但别忘了一件事Office 和部署在 B 机房的 Observer 之间的网络同样重要。如果 A 到 B 的专线丢包率偏高Observer 落后太多业务层面就要能容忍一定的数据延迟。3.2 低频写高频读业务ZooKeeper 最常见的两类使用方式是分布式锁和注册中心。分布式锁偏写注册中心偏读。很多公司把微服务注册信息全部放进 ZooKeeper每个服务实例启动时拉一遍全量服务列表运行期还要维持大量 Watcher 感知上下线。这种场景下集群写量低但读量和长连接数可以高得惊人。我遇到过一个真实业务配置中心只在发布的时候改几十个节点配置平时完全无写但每次发布都有一百多个应用实例同时去读导致一个四节点集群的读 CPU 飙到 70% 以上。后来加了两个 Observer把客户端读流量按权重分流过去CPU 峰值直接降到了安全水位而且整个改动只花了一个变更窗口。这类场景适合观察者节点的本质原因在于读流量对节点内存状态机的压力是可叠加的写压力却受共识机制约束。观察者节点就是为“把读资源从共识组里剥离出来”而生的。3.3 什么时候该用 Observer 而不是加 Follower我一直建议团队在做容量规划时先建立一个判断框架。如果你的 ZooKeeper 集群面临的核心矛盾是“读吞吐不够、连接数被打满”而写路径延迟健康有余量那么优先考虑 Observer。如果集群整体参与节点的数量已经偏多比如超过七个或者写请求经常出现网络分区导致的延迟抖动那你更应该慎重加 Follower甚至在考虑把一部分 Follower 降级为 Observer。相反如果是以下情况Observer 帮不了你瓶颈在写吞吐加 Observer 改不了 Leader 的单点写能力集群需要更强的容错性而不是读性能业务要求所有客户端读取到的数据强一致Observer 的异步窗口无法满足。这些情况该考虑的是拆分集群、引入其他存储方案或者调整业务模型不要把 Observer 当成万能膏药。4. 从配置到上线观察者节点的正确打开方式4.1 单机伪集群验证配置在动生产环境之前我强烈建议先在本地用单机伪集群把配置跑通。所谓伪集群就是在一台机器上起多个 ZooKeeper 进程用不同端口区分。假设要构建一个包含 1 个 Leader、1 个 Follower、1 个 Observer 的最小集群。三个目录分别生成 myid 文件内容分别为 1、2、3。每个节点的 zoo.cfg 长这样节点一tickTime2000 initLimit10 syncLimit5 dataDir/data/zk1 clientPort2181 server.1127.0.0.1:2888:3888 server.2127.0.0.1:2889:3889 server.3127.0.0.1:2890:3890:observer节点二clientPort2182 dataDir/data/zk2 server.1127.0.0.1:2888:3888 server.2127.0.0.1:2889:3889 server.3127.0.0.1:2890:3890:observer节点三除了端口和路径不同配置里必须多一行peerTypeobserver或者继续沿用 server.3 声明列表里的:observer后缀。这里得解释一下两套写法的区别server.Xhost:port1:port2:observer是“告诉所有节点X 号节点是观察者”需要出现在所有节点的配置里尤其是 Leader/Follower 需要知道它的存在peerTypeobserver是“告诉本节点自己是观察者”只放在观察者节点的配置里就够了。实际生产我习惯两种都写防止某个节点配置缺失导致整个集群的成员身份认知不一致。4.2 生产环境节点配置注意事项伪集群跑通后生产环境部署要额外注意三个细节。第一端口分配。ZooKeeper 每个节点需要两个集群通信端口一个用于 Leader 与 Learner 之间的数据同步默认 2888另一个用于投票选举默认 3888。Observer 虽然不投票但选举端口依然要保留因为 Learner 需要通过这个端口和 Leader 建立初始连接。如果机器上有防火墙记得把两个端口都放通不然会出现节点能互相 ping 通但就是无法建立集群的诡异问题。第二Observer 节点的数据目录建议单独做持久化磁盘不要放在系统盘。因为当 Observer 落后太多触发全量快照时会一口气从 Leader 拉取整个数据树瞬间写磁盘。如果磁盘 IO 能力太弱快照同步会拖很久造成“追不上又继续落后”的恶性循环。第三配置变更后需要滚动重启集群。修改 zoo.cfg 不会热生效。顺序上建议先重启 Observer再重启 Follower最后重启 Leader或者利用滚动重启窗口逐个重启避免同时宕掉多个参与节点导致法定人数不足。4.3 用四字命令验证节点身份配置完先别急着接流量用四字命令确认节点身份是对的。ZooKeeper 原生支持通过nc发送四字命令来获取运行时状态echo stat | nc localhost 2181输出里会明确写着Mode: observer或Mode: leader/Mode: follower。如果观察到 Mode 还是 follower多半是配置文件没有更新或者server.X声明里的:observer后缀漏写了。注意从 ZooKeeper 3.5 开始四字命令默认是白名单模式。如果执行stat没有输出需要在 zoo.cfg 里显式配置4lw.commands.whiteliststat,conf,ruok,srvr如果忘了加白名单你可能会在一台新装节点上排查半天发现命令根本发不进去。5. 读扩展收益实测性能数据的合理预期5.1 一组典型的压测方案数据永远是让人信服的最好方式。我拿了一个内部环境做参考三台 8C16G 云主机组成 Leader 两个 Follower再用一台同样规格的机器作为 Observer。客户端使用官方 Java SDK 的异步接口持续发起getData读请求记录 QPS 和延迟分布。压测模型很简单阶段一三个参与节点均摊读流量记录基准数据阶段二把一半读流量切换到 Observer剩余留在原节点阶段三全部读流量打到 Observer观察延迟和 QPS 变化。每组压测持续 15 分钟观察稳定后的中位数和 P99 延迟。为了避免 Watcher 通知和 session 建立对数据的干扰客户端在压测前就预先建立好连接压测期间不做 watch 注册。5.2 不要被绝对数值带偏看趋势说结论前先打个预防针ZooKeeper 的读性能受硬件、JVM 堆内存、客户端连接数影响太大直接比绝对数值没有意义。我这次压测只是验证两个趋势第一读 QPS 并不随节点数线性增长但分散流量后单节点 CPU 明显下降。原本三个节点每台 CPU 在 60% 到 75% 之间徘徊流量分流到 Observer 后每个节点 CPU 回落到 25% 左右相当于给系统预留了巨大的突发流量缓冲空间。第二延迟中位数基本不变P99 在流量切换的瞬间有轻微上扬。原因不难理解流量切换会导致部分客户端重新建立连接、缓存热度的变化随后又回归稳定。Observer 本地读的本质和高性能读节点没有区别只要不触发 GC 抖动延迟不会因为换了角色而明显恶化。还有一个比较容易被忽略的收益所有 Observer 读流量都从 Leader 和 Follower 的负载中剥离后写请求反而更稳了。因为 Leader 和 Follower 的网络和 CPU 资源被释放出来写提案的 ACK 交互延迟更稳定P99 写延迟在压测期间不升反降。这印证了前面说的读流量侵占资源最终会反噬写路径。6. 读一致性边界Observer 读到的数据新不新鲜6.1 为什么会有过期读这个点我必须单独拿出来讲因为它最容易引发事故。ZooKeeper 客户端连接任意节点读请求都由该节点的本地 DataTree 直接响应。Follower 和 Observer 都采用异步方式从 Leader 同步提交记录因此严格来说任何非 Leader 节点的本地读都有“可能读到旧值”的窗口。Observer 因为不参与提案确认这个窗口理论上比 Follower 更宽。我实测过正常网络环境下Observer 和 Leader 之间的数据差异往往只有几毫秒到几十毫秒。但如果网络抖动或者事务量大这个差异有可能被拉大。很多人只看到“读扩展”就默认读到的数据一定是准确的这是非常危险的误解。举个实际场景分布式锁被释放后业务方在很短的时间内重新读锁节点如果请求落到了一个还没来得及同步释放事务的 Observer 上就会误以为锁仍被占用。这种偶发问题排查起来极其隐蔽。6.2 客户端侧如何规避ZooKeeper 从 3.4.6 开始提供了sync()原语它能强制客户端连接的节点与 Leader 的数据同步到调用时间点。在需要强一致读的关键路径上可以先sync再执行getData。这会引入一次额外的往返但比每次写请求都从 Leader 走一遍共识要便宜得多。如果业务上有大量“写完立刻读”的场景比如创建临时节点后马上检查它是否对别的客户端可见建议把这类强一致读流量直接固定到 Leader。客户端 SDK 里可以设置server.xhost:port的优先级或者干脆在业务代码里通过专用的连接池连接 Leader 地址。如果业务绝大多数是读多写少且对秒级延迟不敏感比如服务注册列表的拉取那连 Observer 是没问题的但心里要有数注册列表可能不是最新状态服务的上下线感知会有几十毫秒到几百毫秒的滞后。这取决于你的服务发现机制能不能容忍。需要说明的是Watcher 事件同样存在这个窗口。ZooKeeper 保证 Watcher 事件的触发顺序与事务提交顺序一致但如果事件发生在 Leader 而客户端连接在 Observer客户端会晚一步收到通知。如果你的业务有“必须第一时间感知节点变化”的强需求读链路就不能全走 Observer需要做分流。7. 生产环境里那些坑以及我怎么填的7.1 坑一Observer 长时间离线后重启变成“追日志的煎熬”有一次我增加了一台 Observer 后发现它重启时日志一直停留在TRACKING状态事务差异始终追不上。后来查出来两个原因叠加一是 Leader 端autopurge.snapRetainCount设置得太小历史事务日志被清理得只剩最近几份二是这台 Observer 因为网络隔离离线了半天等到恢复时Leader 已经没有它需要的完整日志段了。这时候 ZooKeeper 只能切换为快照同步把整份 DataTree 从 Leader 拉过来。如果集群数据量很大这个过程慢不说还会拉高 Leader 的磁盘和网络 IO影响正常服务。解法有两层。第一层是配置层面调大autopurge.snapRetainCount和autopurge.purgeInterval避免日志被过早清理为 Observer 所在机器配置足够的磁盘空间。第二层是运维层面Observer 长期离线后不要直接接入生产流量先观察它是否完成了快照同步确认stat输出的 Zxid 与 Leader 差距收敛到合理范围再接流量。7.2 坑二动态 reconfig 导致 Observer 身份“漂移”ZooKeeper 3.5 之后支持动态成员变更通过reconfig命令可以替换节点、修改端口。这确实方便但也埋过雷。有一次我用 reconfig 在线替换一台 Observer 的地址写完新的成员列表后这台节点重启时日志里显示的模式变成了 follower。排查后发现根源是 reconfig 生成的成员配置里server.Xhost:2888:3888末尾没有保留:observer后缀。节点读到自己没有 observer 身份声明本地也没配peerTypeobserver自然就落回了参与投票模式。更危险的是如果这种身份漂移发生在集群运行中会导致法定人数计算模型改变极端情况下可能破坏 Leader 选举的安全属性。现在我的处理原则很简单只要节点是 Observer就同时在 zoo.cfg 的server.X声明里写:observer并且在节点自己的配置里加peerTypeobserver。双重保险任何一侧缺失都不会影响身份判断。涉及 reconfig 操作的模板脚本也会显式校验成员字符串末尾是否带:observer。7.3 坑三读流量无脑打满 Observer引入单点新瓶颈还有一次压测时我把绝大部分读流量都引到一台 Observer 上结果这台节点的网卡和线程池先扛不住了客户端大面积超时。回头看这其实不是 Observer 的问题是我把流量分配策略设计错了。Observer 的作用是“分担读压力”不是“承接所有读压力”。它本质上还是一个普通节点受 CPU、内存、文件描述符和网络带宽约束。如果你的集群只有一台 Observer把它当成读流量的唯一入口等于把多个节点的性能压力集中到单点上还不如原来的分散方案稳健。合理的做法是至少部署两个 Observer并使用客户端侧的负载均衡策略按权重分发给每个节点保留 30% 以上的容量冗余。同时要监控每个节点的文件描述符数量和连接数ZooKeeper 对单连接的处理模型虽然轻量但太多连接一样会让线程调度成为瓶颈。读扩展的是集群整体能力不是让某一个旁听节点变成新的靶子。7.4 坑四把 Observer 当“读高可用”节点忽略容灾角色最后提醒一点Observer 再轻巧它也不属于投票法定人数的一部分。如果集群中所有参与节点都宕机了只剩 Observer集群照样不可用因为 Observer 无法参与选举无法产生新的 Leader整个 ZooKeeper 服务对外表现为拒绝服务。所以在规划容灾时千万别把 Observer 数量算进可用性容量里。一个 3 Participant 2 Observer 的集群容灾能力依然是“允许挂掉 1 个 Participant”跟 3 节点的容灾级别一致。如果需要提升容灾能力正确的做法是增加参与节点而不是观察者。这也是为什么我一直建议团队用“观察者节点做读扩展用参与节点做容灾计算”来区分两类资源的不同价值。读扩展让你活得轻松容灾能力让你活得安稳两者各司其职。
返回列表