
1. 从一次机房断电说起容灾到底在保什么很多团队第一次认真对待容灾都是被一次真实的故障逼出来的。我印象很深的一次是某业务集群所在机房的一路供电出现异常几台机器瞬间掉线。当时集群里跑着 ZooKeeper 三节点挂掉一台之后剩下两台还能正常对外服务业务侧几乎无感知。但如果当时挂的是两台整个集群就会失去多数派写入直接停摆。这件事让我彻底明白一个道理容灾不是保证服务永远不挂而是保证在部分节点挂掉时系统依然能对外提供一致的服务并且在故障恢复后能自动回到健康状态。ZooKeeper 的容灾能力本质上建立在多数派这个核心概念上。它把集群里的节点分成两种角色Leader 和 Follower还有 Observer后面会讲。所有的写请求都必须经过 LeaderLeader 把写操作转换成事务提案Proposal广播给所有 Follower只有当超过半数的节点都确认收到并持久化之后这个写操作才算真正提交。这个超过半数就是容灾的数学基础。为什么是超过半数而不是全部因为如果要求全部节点确认那只要有一台机器网络抖动或者磁盘慢整个集群的写就会被拖死可用性极差。而多数派机制允许少数节点掉队只要剩下的节点还能凑够半数以上集群就能继续工作。这其实是一种用一致性换可用性的经典权衡只不过 ZooKeeper 选择的是强一致优先。具体到容灾的量化指标我们通常关心两个维度能容忍几个节点故障以及故障后多久能恢复服务。对于一个 N 节点的集群ZooKeeper 能容忍的故障节点数是 floor((N-1)/2)。也就是说集群规模可容忍故障节点数说明10单点无容灾能力20挂任意一台就失去多数派实际不可用31生产环境最小推荐规模52推荐规模容灾与成本平衡73规模较大选举开销上升这里有个特别容易被忽略的点2 节点的集群容灾能力其实和 1 节点一样都是 0。因为 2 节点里挂掉任意一台剩下的 1 台凑不够 2 票的多数派多数派是 2整个集群就无法写入了。所以生产环境要么用 3 节点起步要么干脆别用偶数节点。我见过有团队为了高可用特意部署了 4 节点结果发现容灾能力和 3 节点一样都只能挂 1 台白白多花了一台机器的钱还增加了选举时的网络开销。容灾还涉及一个常被忽视的层面数据容灾。ZooKeeper 的每个节点都会把事务日志transaction log和数据快照snapshot持久化到磁盘。Leader 在返回写成功之前必须确保多数派节点已经把这条事务写入了本地磁盘的日志文件。这意味着即使整个集群断电重启只要多数派节点的磁盘没坏数据就能恢复。但这里有个坑ZooKeeper 默认的日志刷盘策略是批量刷盘也就是说它不会每来一条事务就 fsync 一次而是攒一批再刷。这样做吞吐量高但在极端情况下比如机器突然断电最后几条还没刷盘的事务可能会丢。如果你的业务对数据丢失零容忍需要关注forceSync这个参数把它设为 yes 可以强制每次事务都刷盘代价是写入延迟会明显上升。提示容灾规划时除了节点数量一定要把节点的物理分布考虑进去。如果三台机器都在同一个机柜、同一路电源下那所谓的容灾只是心理安慰。理想情况下节点应该分布在不同的机架、不同的供电单元甚至不同的可用区。2. CAP 定理在 ZooKeeper 里到底怎么落地CAP 定理几乎是分布式领域的常识但真正能把它和具体系统对上号的人并不多。CAP 说的是一个分布式系统在一致性Consistency、可用性Availability、**分区容错性Partition tolerance**这三者中最多只能同时满足两个。很多人背得滚瓜烂熟但一到实际选型就懵了ZooKeeper 到底选了哪两个先说结论ZooKeeper 选择的是 CP即一致性和分区容错性牺牲的是强可用性。但这个结论需要拆开来看因为 CAP 里的可用性和我们日常说的高可用不是一回事。CAP 里的可用性定义非常严格每一个非故障节点都必须在有限时间内返回一个非错误的响应。注意是每一个非故障节点而且响应必须是非错误的。ZooKeeper 在发生网络分区时如果少数派那一侧的节点收到了写请求它会直接拒绝或者一直等待不会返回一个可能过期的数据。这就违反了 CAP 的可用性定义。但从工程角度看这种拒绝服务恰恰是保证数据一致性的必要代价。分区容错性P在分布式系统里几乎是必选项。因为网络分区是客观存在的现实你没法假设网络永远可靠。所以真正的选择其实是在 C 和 A 之间。ZooKeeper 选了 C这意味着在网络分区发生时它宁可让少数派一侧不可用也要保证多数派一侧的数据是一致的。这里要澄清一个常见的误解ZooKeeper 的读操作并不保证强一致而是保证顺序一致性sequential consistency。什么意思呢ZooKeeper 的写操作是线性的linearizable所有写都经过 Leader全局有序。但读操作可以由任意一个 Follower 直接处理不需要经过 Leader。这就导致一个客户端可能从 Follower A 读到了最新数据而另一个客户端从 Follower B 读到的还是旧数据。不过 ZooKeeper 保证同一个客户端发起的读请求不会读到比它之前读过的数据更旧的数据这就是顺序一致性。如果你需要强一致的读ZooKeeper 提供了sync()方法。调用sync()之后当前节点会强制和 Leader 同步一次之后的读就能保证读到最新数据。这个操作在跨机房场景下特别有用但代价是增加了一次网络往返。特性ZooKeeper 的表现说明写一致性线性一致所有写经 Leader全局有序读一致性顺序一致Follower 可本地读可能读到旧数据分区时少数派不可用拒绝写请求保证不返回脏数据分区时多数派可用正常提供读写服务强一致读需 sync()强制与 Leader 同步理解 CAP 在 ZooKeeper 里的落地对实际使用有很强的指导意义。比如你在做分布式锁锁的获取和释放都是写操作走的是强一致路径没问题。但如果你用 ZooKeeper 做配置中心客户端从 Follower 读配置可能会短暂读到旧配置。这时候要么接受这个延迟要么在关键配置读取前加一次sync()。我个人的经验是对于变更频率低、容忍秒级延迟的配置直接读 Follower 就够了对于变更后必须立即生效的开关类配置老老实实加 sync()。还有一个容易踩的坑很多人以为 ZooKeeper 的 CAP 选择是固定的其实不是。通过调整readOnly模式ZooKeeper 3.4 之后支持可以让少数派节点在分区时仍然提供只读服务。这相当于在可用性上做了一点妥协性的增强但读到的数据可能不是最新的。这个模式适合那些读多写少、能容忍数据稍旧的场景比如服务发现里的节点列表查询。3. 脑裂分布式系统里最危险的精神分裂脑裂这个词听起来很玄乎但它的本质很简单一个集群因为网络分区分裂成了两个或多个互相无法通信的子集群每个子集群都以为自己是正统各自选出了 Leader各自对外提供服务。结果就是同一个集群里出现了多个大脑数据开始分叉等网络恢复后冲突的数据很难合并甚至会造成数据丢失。ZooKeeper 的设计里脑裂是被重点防范的问题。它的防脑裂机制核心就是前面说的多数派。假设一个 5 节点的集群网络分区把节点分成了 3 个和 2 个两组。3 个的那组能凑够多数派3 5/2可以正常选举 Leader 并对外服务2 个的那组凑不够多数派无法选举出 Leader也就无法对外提供服务。这样一来整个集群里始终只有一个正统的 Leader不会出现两个 Leader 同时写的情况。但这里有个关键前提多数派机制要生效必须保证每个节点只能投一票而且选举是全局唯一的。ZooKeeper 的选举算法FastLeaderElection通过比较事务 IDzxid和服务器 IDmyid来决定谁当 Leader。zxid 越大说明数据越新优先当 Leaderzxid 相同时myid 大的当 Leader。这个规则保证了选举结果的确定性不会出现两个节点同时宣称自己是 Leader 的情况。不过脑裂的防范并不是设了多数派就万事大吉。我踩过的一个坑是集群规模配置不当导致的假脑裂。有一次我们部署了 6 个节点的集群某次网络抖动恰好分成了 3 和 3 两组。两组都凑不够多数派多数派是 4结果整个集群彻底不可用业务全线报警。这就是偶数节点在分区时的尴尬没有哪一组能形成多数派集群直接瘫痪。后来我们把节点数改成 5同样分成 3 和 2 时3 的那组能正常工作2 的那组静默等待至少保证了一半的可用性。另一个和脑裂相关的概念是Observer。Observer 是一种特殊的 Follower它不参与投票只同步数据。引入 Observer 的目的是为了在不影响写性能的前提下扩展读能力。因为 Observer 不参与选举和写确认所以增加 Observer 不会降低集群的容灾能力多数派计算时不算 Observer。但要注意Observer 不参与投票所以它不能用来凑多数派。如果你把 Observer 当成普通 Follower 来规划容灾那就大错特错了。节点类型参与选举参与写确认计入多数派典型用途Leader是是是处理写请求Follower是是是参与投票处理读Observer否否否扩展读能力跨机房脑裂的排查在实际运维中也有讲究。如果你怀疑集群发生了脑裂第一步是看日志里有没有多个节点同时进入 LEADING 状态。正常情况下同一时刻只有一个节点是 LEADING其他都是 FOLLOWING 或 LOOKING。如果发现两个节点都宣称自己是 Leader那基本可以确定发生了脑裂。第二步是检查网络看是不是有防火墙规则或者交换机配置导致节点之间无法通信。第三步是检查zoo.cfg里的server.x配置确认所有节点的配置一致没有漏配或者 IP 写错的情况。注意脑裂一旦发生最危险的不是服务中断而是数据分叉。两个 Leader 各自接受写请求等网络恢复后其中一个 Leader 的写数据会被丢弃。如果你的业务在这期间写了关键数据那就真的丢了。所以生产环境一定要监控集群的 Leader 数量一旦发现异常立即告警。4. 选举过程拆解Leader 是怎么上位的理解了容灾和脑裂再来看 ZooKeeper 的选举过程就会清晰很多。选举是 ZooKeeper 保证一致性和容灾能力的核心机制也是很多面试题的常客。但光背zxid 大的当 Leader是不够的得知道整个流程是怎么跑起来的。ZooKeeper 的选举发生在两种场景集群启动时和Leader 挂掉时。集群启动时所有节点都处于 LOOKING 状态它们会互相发送投票信息内容包括自己推荐的服务器的 myid 和 zxid。每个节点收到其他节点的投票后会按照规则比较先比 zxidzxid 大的胜出zxid 相同则比 myidmyid 大的胜出。如果一个节点发现别人的票比自己投的更好就会改投别人。这个过程持续到某个节点获得了超过半数的票它就当选 Leader。Leader 挂掉时的选举稍微复杂一点。首先Follower 会检测到和 Leader 的心跳断了通过tickTime和syncLimit参数控制超时然后进入 LOOKING 状态发起新一轮选举。这里有个细节新选举出的 Leader 必须拥有集群中最新的数据。这就是为什么 zxid 是选举的第一比较条件。如果让一个数据旧的节点当了 Leader那它就会把旧数据同步给其他节点导致已经提交的数据丢失。ZooKeeper 通过 zxid 比较避免了这个问题。选举过程中的状态转换可以用下面这个简化的流程来理解所有节点初始状态为 LOOKING各自投自己一票。节点把自己的投票广播给其他节点。收到别人的投票后按 (zxid, myid) 规则比较保留更优的票。如果某个节点发现自己推荐的服务器获得了多数票它就尝试成为 Leader。成为 Leader 后状态变为 LEADING其他节点变为 FOLLOWING。Leader 开始接收写请求并同步数据给 Follower。这个过程中tickTime是个很关键的参数。它定义了 ZooKeeper 的基本时间单位默认是 2000 毫秒。心跳超时是tickTime * syncLimit选举超时是tickTime * initLimit。如果tickTime设得太小网络稍微抖动就会触发选举集群频繁进入选举状态性能急剧下降设得太大故障发现又太慢。我一般建议在局域网环境用默认的 2000跨机房场景可以适当调大到 3000 或 4000。还有一个实战经验选举期间集群是不可写的。因为写请求必须经过 Leader而选举期间没有 Leader所以所有写请求都会被阻塞。选举通常很快几百毫秒到几秒但如果集群规模大、网络差选举可能持续十几秒甚至更久。对于写敏感的业务这个停顿是致命的。所以生产环境要尽量避免不必要的选举比如不要频繁重启节点不要在网络不稳定的时候做扩容。参数默认值作用调优建议tickTime2000ms基本时间单位局域网默认跨机房可调大initLimit10启动时同步超时tickTime 倍数数据量大时调大syncLimit5心跳超时tickTime 倍数网络差时调大electionAlg3选举算法默认 FastLeaderElection选举算法本身也在演进。早期的 ZooKeeper 用的是基于 UDP 的选举算法后来改成了 FastLeaderElection基于 TCP更可靠也更快。现在生产环境基本都用 FastLeaderElection不需要手动改。但如果你看到zoo.cfg里有electionAlg0/1/2的配置那说明是历史遗留配置建议清理掉。5. 跨机房容灾理想很丰满现实有取舍单机房内的容灾相对简单多数派机制就能搞定。但一旦涉及跨机房问题就复杂了。跨机房容灾的核心矛盾是多数派机制要求节点之间低延迟通信而跨机房的网络延迟天然就高。如果强行把节点分散在多个机房写请求的延迟会被最慢的那个机房拖累性能急剧下降。常见的跨机房部署方案有三种各有取舍方案一单机房部署异地备份。集群只在一个机房内另一个机房只做数据备份比如通过快照文件定期同步。这种方案性能最好但容灾能力有限——机房整体故障时需要人工介入恢复RTO恢复时间目标较长。方案二两地三中心。在两个城市三个机房部署节点比如 221 的分布。这种方案能容忍单机房故障但写延迟受跨城网络影响通常在几十毫秒级别。而且多数派计算变得复杂如果两个机房各挂一个节点剩下的 21 能凑够多数派吗要看具体配置。方案三Observer 跨机房。主集群在一个机房其他机房部署 Observer。Observer 不参与投票所以跨机房的延迟不影响写性能。其他机房的客户端可以就近读 Observer降低读延迟。这种方案在读写分离场景下很实用但 Observer 的数据可能有延迟不适合强一致读。方案容灾能力写延迟复杂度适用场景单机房备份低低低非核心业务两地三中心高高高核心业务Observer 跨机房中低中读多写少我个人的经验是大部分业务其实不需要跨机房强一致用 Observer 方案就够了。真正需要两地三中心的是那些机房挂了也不能停的核心业务。而且跨机房部署一定要做充分的网络延迟测试别想当然地认为专线延迟很低。我曾经见过一个团队跨机房专线标称延迟 5ms实际高峰期抖动到 50ms导致 ZooKeeper 频繁触发选举业务雪崩。跨机房还有一个隐藏的坑时钟同步。ZooKeeper 的 zxid 是逻辑时钟不依赖物理时钟所以时钟不同步不会影响选举的正确性。但如果你用 ZooKeeper 做分布式锁锁的超时判断可能依赖本地时间时钟不同步会导致锁提前释放或者迟迟不释放。所以跨机房部署时NTP 同步一定要做好。6. 生产环境容灾配置的几条硬核经验聊了这么多理论最后落到实操上。下面这几条是我在实际运维中总结出来的每一条都对应着一次真实的教训。第一条集群规模宁奇勿偶3 或 5 是甜点区。前面说过偶数节点的尴尬这里再强调一次。3 节点能容忍 1 台故障5 节点能容忍 2 台7 节点能容忍 3 台但选举开销明显上升。大部分业务用 3 节点就够了核心业务用 5 节点。超过 7 节点的集群写性能会因为广播开销而下降得不偿失。第二条磁盘一定要用 SSD而且日志和数据分开盘。ZooKeeper 的写性能瓶颈几乎全在磁盘 fsync 上。用机械盘的话单节点写入可能只有几百 TPS换成 SSD 能到几千甚至上万。另外事务日志和数据快照最好放在不同的物理盘上避免 IO 互相干扰。如果条件允许给日志盘单独挂一块高性能 SSD效果立竿见影。第三条JVM 堆内存不要超过 4G而且要用 G1 或者 CMS。ZooKeeper 的数据都在内存里堆太小会频繁 GC堆太大 GC 停顿时间长同样影响心跳和选举。4G 是个比较平衡的值。GC 算法方面JDK 8 用 CMSJDK 11 以上用 G1避免用默认的 Parallel GC因为它的停顿时间不可控。第四条监控要覆盖四个核心指标。分别是Leader 是否存在且唯一、Follower 与 Leader 的同步延迟、事务日志的写入延迟、集群的选举次数。前两个指标反映集群健康度第三个反映磁盘性能第四个反映网络稳定性。任何一个异常都要立即告警。第五条定期做故障演练。别等到真出故障了才发现容灾方案有漏洞。定期手动 kill 掉一个节点观察集群是否能自动恢复模拟网络分区验证多数派机制是否生效。演练时要注意观察业务侧的表现而不仅仅是 ZooKeeper 自身的状态。提示故障演练最好在业务低峰期做而且要提前通知相关方。我见过有团队在双十一前做演练结果演练过程中真出了故障和业务高峰叠加场面一度非常混乱。第六条配置文件要版本化管理。zoo.cfg里的每个参数都影响集群行为改错了可能导致选举失败或者数据不一致。建议把配置文件纳入 Git 管理每次变更都走 review 流程。特别是server.x的配置IP 和端口写错一个字符整个集群就可能起不来。第七条数据目录不要放在系统盘。系统盘满了会导致 ZooKeeper 无法写入事务日志进而整个节点不可用。数据目录要单独挂载并且设置磁盘使用率告警。另外快照文件会随着时间累积虽然 ZooKeeper 会自动清理旧的快照但如果写入量特别大还是要手动检查一下磁盘空间。这些经验看起来琐碎但每一条都是真金白银换来的。ZooKeeper 本身是个很成熟的系统大部分问题不是出在它身上而是出在配置和运维上。把容灾、CAP、脑裂这些理论搞清楚再结合这些实操经验基本就能把 ZooKeeper 集群管得明明白白了。