
1. 先从一次线上故障说起ZooKeeper的定位和它逃不掉的核心概念我第一次在生产环境部署ZooKeeper集群的时候做的就是最基础的三件事三台机器各自写好zoo.cfg启动服务客户端连上去创建几个节点。看起来一切正常直到某天凌晨业务方反馈大量请求超时我登上去一看集群里Leader那台机器宕了另外两个Follower成了无头苍蝇整个系统停了将近半分钟。那次故障之后我才意识到ZooKeeper不是装上就能跑那么简单。服务端架构的设计、集群选举的规则、数据同步的细节任何一个环节理解不到位出了问题就是两眼一抹黑。这篇文章就是把我在那次故障和之后的反复排查中积累的东西做一个系统的复盘适合正在使用ZooKeeper但还没彻底搞懂内部机制的开发者也适合准备深入分布式协调场景的运维和架构师。1.1 分布式系统里谁做主的问题分布式系统最核心的难题不是怎么把请求分发到多台机器而是多台机器之间如何达成一致。最简单的例子三个服务节点都保存着同一个配置项其中一个节点改了配置另外两个怎么办如果它们各自按自己的想法执行系统的状态就分叉了。业内解决这个问题的思路有很多ZooKeeper选择的是选出一个Leader由它统一决策。这个思路看似简单实际实现起来牵涉到一个关键前提选Leador的过程本身也得是一致的。于是就有了服务端架构里最关键的角色划分——Leader、Follower、Observer以及围绕Leader展开的选举和同步机制。1.2 节点、会话和WatcherZooKeeper给出的答案ZooKeeper对外提供的模型是一个树状的命名空间每个节点叫作znode。你可以把它想象成一个共享的文件系统但和普通文件系统最大的区别是每个znode都可以附带一段数据并且支持顺序节点和临时节点。持久节点客户端断开后依然存在。临时节点创建它的会话结束后自动删除。顺序节点在节点名后自动追加一个单调递增的序号。会话Session是ZooKeeper里的另一个核心概念。客户端与服务器建立连接后就创建了一个会话服务器会维护这个会话的心跳和超时时间。一旦会话超时临时节点就会被清除。理解这一点对排查问题非常重要——很多同学遇到节点数据突然消失第一反应是代码bug最后发现其实是会话超时导致临时节点被回收。Watcher机制则是ZooKeeper实现通知的方式。客户端可以注册一个Watcher监听某个节点节点发生变化时服务器会向客户端推送事件。不过要注意ZooKeeper的Watcher是一次性的触发一次之后需要重新注册。这个特点在实现分布式锁和配置监听时经常被忽略结果就是第一遍能收到通知第二遍就静悄悄了。1.3 配置、锁、注册发现为什么它成了基础设施有了上述三个基础能力ZooKeeper就能支撑很多常见的分布式场景配置管理节点存储配置客户端监听变更、分布式锁临时顺序节点加Watcher、服务注册与发现临时节点记录服务地址、集群选主竞争创建同一个节点。这些场景看起来各不相关但底层都依赖同一个东西一个强一致、可恢复的分布式状态存储。强一致意味着客户端无论连接到哪台服务器读到的数据都应该是最新的可恢复意味着即使多台机器宕机只要集群仍有半数以上节点存活服务就能继续工作。要实现这两点就得从服务端架构讲起。2. 服务端架构解剖一条写请求在进程里的完整旅程很多文章介绍ZooKeeper架构时都会给出一堆组件名词PrepRequestProcessor、SyncRequestProcessor、FinalRequestProcessor、LearnerHandler……然后就没有然后了。我刚开始学的时候也是懵的直到后来把一条写请求的完整旅程走了一遍才把这些组件真正串起来。2.1 三大RequestProcessorPreprocess、Sync、FinalZooKeeper服务端处理请求的核心是一个责任链模式链上有三个关键处理器PrepRequestProcessor是第一个关卡。它负责校验请求的合法性比如会话是否存在、节点路径是否合理、节点类型是否符合约束。对于写请求它还会计算出一个事务Transaction并分配一个全局递增的事务ID也就是zxid。SyncRequestProcessor是第二个关卡负责把事务写入磁盘上的事务日志。这一步之所以叫Sync是因为它会强制把数据刷到磁盘后才算完成确保哪怕进程崩溃也不会丢事务。我见过一些团队为了性能把刷盘改成异步结果就是在异常断电时丢了已提交的事务——这个优化真不建议做。FinalRequestProcessor是最后一个关卡负责把事务应用到底层的内存数据库DataTree然后向客户端返回响应。如果是Leader节点还会在应用事务之前把事务提案广播给所有Follower经过半数以上确认后才提交。这三个处理器串起来就是ZooKeeper服务端处理写请求的框架。架构本身不复杂但每个环节都有它的设计理由Prep做校验和编号Sync保证不丢数据Final保证可见性和一致性。2.2 Leader、Follower、Observer三种角色的分工差异角色分工是ZooKeeper服务端架构里最容易混淆的地方。很多人的理解停留在Leader处理写、Follower处理读实际上远远不止这些。Leader是整个集群的核心。它接收所有写请求生成事务提案广播给Follower收集投票最终提交事务。同时它也要处理Follower转发过来的写请求并维护与所有Follower之间的心跳。Follower有三个职责第一是参与投票包括选举Leader时的投票和事务提案的投票第二是处理客户端的读请求第三是如果在Follower上接收到写请求会将其转发给Leader处理。Observer是ZooKeeper 3.3.0开始引入的角色。它不参与投票只同步Leader的数据对外可以像Follower一样处理读请求。引入Observer的目的是解决增加服务器提高读性能与服务器太多导致投票成本上升之间的矛盾。因为Observer不投票所以集群里可以挂很多Observer而不用担心选举和提案变慢。这里有一个非常经典的误区以为ZooKeeper所有节点都能处理读请求就意味着读也能线性一致。实际上Follower和Observer上的读请求都只是读取当前自己内存里的数据这个数据可能是滞后的。如果需要强一致读取需要加sync参数或者走写请求ZooKeeper本身的读写模型设计更偏向最终一致线性一致的写。2.3 事务日志和快照数据持久化的两条腿ZooKeeper的数据持久化依赖两个文件事务日志transaction log和数据快照snapshot。事务日志记录的是每一次写操作产生的事务记录。设计上有个细节每台服务器会为事务日志预先分配一块固定大小的文件空间并在磁盘上通过先写日志再更新内存的顺序保证崩溃恢复的安全。在ZooKeeper的默认配置中事务日志文件是写入在dataLogDir指定的目录下的如果能单独挂载高速磁盘或者至少和系统盘分离写性能会提升很多。数据快照则是内存数据DataTree在某一个时刻的完整序列化。快照不是每次写都生成的而是到了一定数量的事务后自动触发或者由运维手动触发。为什么需要快照因为事务日志可以无限增长如果每次恢复都从头回放所有事务随着系统运行时间变长恢复时间会越来越不可控。有了快照只需要加载最近一次快照再从快照之后的事务日志做增量回放即可。2.4 为什么写请求必须过Leader从两阶段提交讲起ZooKeeper的写流程本质上是一个简化版的两阶段提交。Leader生成事务提案后把提案连同zxid一起发送给所有Follower。Follower收到提案后先将事务写入自己的事务日志再返回ACK给Leader。当Leader收到半数以上Follower的ACK后就会广播提交指令。Follower收到提交指令后才将事务应用到内存数据。这里有三个关键点值得展开。第一为什么是半数以上而不是全部因为ZooKeeper的设计目标是在少数节点宕机的情况下依然能正常工作如果要求全部确认任何一台机器掉线都会阻塞整个集群。第二为什么Follower在ACK之前要先写日志因为Leader可能在自己提交之后就立刻宕机新任Leader需要从其他节点恢复数据。如果某个Follower还没写日志就回复了ACK它可能丢失一笔已经提交的事务造成数据不一致。第三zxid的分配方式是理解整个机制的关键。zxid是一个64位的长整数高32位是当前的epoch选举纪元低32位是事务序号。每次重新选举Leader新的Leader都会把epoch加1保证新Leader的事务序号不会低于旧Leader。因为没有带epoch的事务ID比较是不准确的。3. 集群选举机制Leader不是随便指定的ZooKeeper的集群选举大概是所有基础组件里被问得最多、理解成本最高的部分之一。很多人死记硬背比较zxid和myid却完全不知道背后的场景和边界条件。这一节我结合自己的故障经历把选举机制完整拆一遍。3.1 三种触发选举的典型场景第一种是集群启动时。多个节点同时启动各自处于LOOKING状态需要通过选举确定谁当Leader。第二种是Leader运行期间宕机。这是我在线上故障中遇到的场景Leader进程因为内存溢出退出后剩下的Follower检测到Leader心跳超时各自发起新一轮选举。第三种是网络分区。如果某个Follower与Leader之间的网络断开了但与其他节点仍可通信它会认为当前Leader不可用于是发起选举。分区两侧可能会各自选出自己那侧的节点作为Leader但由于任何合法决策需要多数派节点参与两个Leader不可能同时都获得多数派支持完美的脑裂实际上很难发生这一点在后面讲多数派规则时会说明。3.2 投票到底比的是什么epoch、zxid、myidZooKeeper使用的选举算法叫Fast Leader ElectionFLE。每一张选票里包含三个核心字段myid、zxid、epoch。myid每台服务器的唯一编号配置在myid文件里。zxid本节点当前处理事务的最新zxid用于表明本节点的数据最新程度。epoch选举轮次每发起一轮新的选举epoch加1。FLE的比较步骤如下先比较epoch。epoch大者胜。如果epoch相同再比较zxid。zxid大者胜。如果zxid也相同最后比较myid。myid大者胜。我第一次看到这个规则时觉得为什么要搞三个字段直接比myid不就行了谁编号大谁当Leader。后来才明白只比myid的代价是很可能选出一个数据残缺的节点当Leader之后要花大量时间从其他节点拉数据甚至可能因为数据存在冲突而无法恢复。先比zxid能保证选出来的Leader拥有最新的数据这就为后续的数据同步省了很多事。3.3 Fast Leader Election的完整流程大致流程可以用几个阶段来概括发起投票当节点进入LOOKING状态时它首先生成一张选票推荐自己并把选票广播给集群中其他节点。接收与比较每个节点收到其他节点的选票后按照上面说的比较规则判断谁更有资格当Leader。如果对方的选票更好就更新自己的推选结果并再次广播直到所有节点的推选结果一致。统计投票当前投票结果一旦达到半数以上节点支持就认为选举完成。支持某个节点达到多数派的节点就会修改自身状态为LEADING若是自己或者FOLLOWING若是别人。通知与确认Leader向所有Follower发送Leader就绪通知Follower确认后整个集群进入正常服务状态。需要强调的是选举过程本身也是一个分布式共识问题。每个节点都在独立地发出和解收选票没有中心节点。整个算法能收敛的原因是比较规则是全序的不存在两个节点都认为自己比对方更好却又无法分出高下而多数派规则确保即使在网络分区的情况下也只有一个结果能获得足够的票数。3.4 多数派规则和脑裂防护多数派规则是整个ZooKeeper可用性模型的基石。在我的3节点集群里任何一个决策选Leader或提交事务至少需要2个节点的确认。这意味着允许1个节点宕机4节点集群则允许1个节点宕机因为多数是35节点允许2个宕机。所以ZooKeeper官方一直建议奇数台因为这能让容错数量最大化——4台能容忍1台宕机3台也是容忍1台多一台硬件成本却没有任何可靠性收益。看到这里你可能会想网络分区时两边都凑不够多数派岂不是集群完全不可用实际上这正是ZooKeeper的安全优先于可用的设计选择宁可不可用也不能出现两个Leader同时对外提供服务。脑裂预防是选举机制里最重要的一环。4. 数据同步新老节点如何对齐状态选举完成只是开始。新的Leader不管从哪台节点选出它都必须确保集群中所有节点回到一致的数据状态这就是数据同步要做的事。这一块在生产环境的故障排查中踩过不少坑我觉得有必要把细节完全写清楚。4.1 为什么选举完成后的第一步是同步想象一下三节点集群节点A、B、C。假设Leader A在处理到zxid0x100时宕机Follower B刚同步到zxid0x0ffFollower C同步到0x0fe。根据FLE规则B的数据比C新所以B当选新Leader。但B也只比C领先一个事务它必须把自己多出来的那笔事务同步给C才能对外提供一致的服务。更极端的情况是一个全新的Follower刚加入集群它没有任何数据。它需要先从Leader拉取全量快照然后基于快照继续同步后续的增量事务。这两种情况对应着不同的同步策略。4.2 握手阶段Learner如何找到并连上Leader这里的Learner泛指Follower和Observer。选举结束后Learner第一步是和自己选出的Leader建立TCP连接然后发送自己的身份信息和当前最大zxid。Leader侧收到连接后会为每个Learner创建一个LearnerHandler这个对象里维护着与Learner的连接、队列、同步进度等等。LearnerHandler也是整个同步过程的项目经理它要知道Learner现在差了多少数据选择同步策略然后协调数据包的发送。这里有一个容易忽略的细节Learner与Leader建立连接后需要先发起一个FOLLOWERINFO或OBSERVERINFO包里面包含了Learner的当前zxid和epoch。Leader拿到这个zxid后判断Learner数据落后到什么程度决定采用哪种同步方式。4.3 SNAP、DIFF、TRUNCDIFF三种同步策略详谈ZooKeeper的同步策略按数据差距大小和方向分为三种代码里对应封装在不同的处理逻辑中。SNAP全量同步当Learner与Leader的zxid差距过大或者Learner还没有任何历史数据时Leader直接发送当前内存数据生成的一份快照给Learner。由于快照文件可能很大同步时间也会比较长。新节点加入集群时必然走SNAP。DIFF增量同步当Learner的zxid落后不多且Leader的事务日志中还保留着Learner缺失的那部分事务时Leader会把缺失的事务逐条发送给Learner。这是性能最好的方式网络中只需要传几条事务记录。TRUNCDIFF先回滚再增量这是很多文章一笔带过但实际上很重要的场景。当Learner的zxid比Leader还大也就是Learner多出了一些Leader没有的事务时说明Learner之前接触过一个旧Leader而那个旧Leader的一些事务并没有被这个新Leader继承。此时Learner需要先把自己多出的那部分事务回滚掉TRUNC再与Leader做增量同步DIFF。为什么需要TRUNC因为新Leader在选举时已被确认拥有本集群中部分最新数据但其事务日志是一条正常的历史链。如果一个节点保存了某一笔事务而新Leader中没有就说明这笔事务出自一个已经下台的Leader新Leader不会承认它。不截断的话这个节点会一直保留着一段伪造历史之后每次与其他节点的对比都会不一致。4.4 一个完整同步时序从Follower启动到对外服务为了把整个流程串起来这里给出一个典型时序假设新Follower D启动myid4它先进入LOOKING状态参与选举或直接加入现有集群。D与Leader建立连接发送FOLLOWERINFO携带其最大zxid0x3ff。Leader的LearnerHandler收到zxid后检查自己的日志发现D从0x3ff到最新比如0x3fc是落后状态实际上D缺失了0x400到0x40a这11笔事务。Leader判断这些事务仍在自己日志的保留窗口内于是选择DIFF策略。Leader向D发送DIFF标记然后逐条发送0x400到0x40a的事务。D按顺序写入日志、应用事务到内存。同步完成后Leader向D发送UPTODATE标记D转换到FOLLOWING状态开始对外提供读服务。如果D的zxid比Leader还高比如D拥有0x410而Leader到0x40a那么Leader会先让它TRUNC到0x40a再走DIFF补全0x40a之后的事务实际业务场景里通常是旧Leader遗留问题我在排查时看到这种情况的第一反应就是去查最近是否有过异常重启或人为数据操作。5. 生产环境实测排错链路与调参经验最后这部分是这篇文章里我个人觉得最值钱的部分。理论知识可以看文档也好但很多问题只有实际操作时才知道怎么下手。5.1 集群不可用的常见原因选举风暴线上最常见的问题之一就是集群频繁选主、服务大面积超时。根本原因往往是选举风暴多个节点同时发起选举选票交错导致整个集群长时间处于LOOKING状态。造成选举风暴的典型原因有三个第一是磁盘IO瓶颈。事务日志刷盘变慢同步请求超时Follower连续很久没收到Leader的心跳就会发起选举。尤其是在虚拟机环境磁盘IO不稳定时这问题极其常见。第二是GC停顿过长。JVM老年代回收时停顿几十秒会导致心跳超时其他节点认为Leader已死触发一轮选举。等到选举完成原Leader又恢复了它不服新Leader于是再发起一轮新的选举来回折腾。第三是网络抖动。交换机故障或者网卡流量打满导致节点间心跳延迟同样会触发选举。排查时我的建议是先把jstat和磁盘IO的监控打开看看GC和IO是不是周期性尖峰。如果GC问题突出重点是调整JVM堆大小和GC策略如果是磁盘问题优先考虑把dataLogDir放到单独的SSD上。5.2 数据同步卡住时怎么排查从CPU、网络到磁盘数据同步卡住的表现是某个节点一直处于leader-election或syncing状态客户端连接后读不到最新数据。排查链路我一般按这个顺序走看网络层用ping不过看延迟真正要抓的是大量重传。netstat -i看是否有明显丢包。如果丢包严重先解决网络问题。看日志ZooKeeper的日志中会有Have quorum of ...或者Notification time out之类信息。如果大量出现Notification time out说明节点间的通信链路不正常。看CPU如果CPU跑满先检查是不是有大量请求打过来还是GC线程异常。用top -H看具体线程的状态。看磁盘iostat -x 1确认util%是否接近100%。对于事务日志写入频繁的Leader节点磁盘直接决定同步速率。我遇到过一次非常典型的案例Follower同步完全卡住CPU不高、网络没丢包、磁盘也没满最后用lsof -p pid | grep deleted才发现事务日志目录所在的分区被写满后自动清理逻辑把正在使用的日志文件删了进程持有了一个已删除的文件句柄还在持续写入。这种问题如果你对文件系统层面不了解根本想不到去查。5.3 参数调优清单与日常巡检建议最后分享一份我实践下来比较常用的配置思路注意是思路不是万能模板tickTime基础时间单位默认2000ms。initLimitFollower启动后与Leader建立连接的最长时间建议根据实际网络延迟调整别用默认值硬扛跨机房场景。syncLimitFollower与Leader心跳超时上限。这个值设置太小时网络抖动容易误触发选举设置太大会让故障发现得晚。通常建议在10个tick以上。maxClientCnxns限制单台服务器能保持的最大客户端连接数。这个值很值得关注很多事故就是客户端连接数打满导致服务不可用。maxSessionTimeout会话超时上限。应用侧设置的sessionTimeout不能超过它否则会被限制在这个值。日常巡检的话我只看三样东西事务日志目录的磁盘剩余量、各节点角色是否稳定、JVM堆内存的占比趋势。只要这三样正常ZooKeeper集群出大问题的概率非常低。最后说点个人体会。ZooKeeper这套设计虽然在业界已经非常成熟但它并不是没有代价的——读写都走Leader、事务日志必须刷盘、半数确认才能提交这些强一致保证让它天生不适合高并发写入场景。如果你需要的是一个高性能注册中心可以把更轻量的方案纳入备选但如果你需要的是强一致的分布式协调底座那么搞懂服务端架构、选举和同步这套组合机制依然是扎根于分布式领域的基础功。我在实际排查中反复踩过的那些坑大多不是题目难而是对这三块机制之间的联动关系理解得太浅。