前阵子帮朋友做模拟面试,对方把Zookeeper的四种Server状态——LOOKING、FOLLOWING、LEADING、OBSERVING——背得很流利,结果面试官追问了一句“LOOKING期间客户端请求到底去哪了”,他当场卡壳。这个场景我见过太多次了。很多人对Zookeeper状态的理解停留在“背出四个英文单词”的层面,但真正的面试追问和线上排障,考的全是状态背后的状态机流转、选举逻辑、以及生产环境里的表现。这篇文章就把这四种状态彻底拆开,从状态机的角度、选举的细节、Observer的特殊性、再到监控和故障判读,适合准备Zookeeper面试的同学,也适合正在维护ZK集群的SRE和架构师参考。
1. 状态机里的四宫格:LOOKING、FOLLOWING、LEADING、OBSERVING各自承担什么
1.1 一张表先分清四个角色
关于Zookeeper集群,有一个最常见的误解:以为服务器状态只有“Leader”和“Follower”两种。实际上Zookeeper官方把Server状态划分为四种,而且这四种状态并不完全对等。我先用一张表把它们的定位说清楚:
| Server状态 | 角色定位 | 是否参与投票 | 是否处理写请求转发 | 是否同步数据 | 典型场景 |
|---|---|---|---|---|---|
| LOOKING | 选举中 | 是,投出自己的票 | 否 | 否,等待新Leader产生 | 刚启动、Leader失联、集群重建 |
| FOLLOWING | 跟随者 | 是 | 是,转发给Leader | 是 | 正常集群中的非Leader节点 |
| LEADING | 领导者 | 是 | 是,直接处理 | 是,主导数据同步 | 正常集群中唯一的Leader |
| OBSERVING | 观察者 | 否 | 是,转发给Leader | 是,单向接收 | 读扩展、跨机房容灾、不下线扩集群 |
这张表里最值得玩味的是LOOKING。其他三种状态都代表“集群里已经有Leader、服务在正常运行”,只有LOOKING是一个“不稳定状态”——节点正在寻找或者参与选举一个新的Leader。虽然Zookeeper的ServerState枚举里LOOKING排在第一个,但它绝不是简单的“刚启动”,而是集群可用性最脆弱的时期。
面试官最爱的追问点就在这里:LOOKING状态有没有对应的“正常”阶段?答案是没有。Zookeeper集群一旦出现长时间处于LOOKING的节点,说明它已经脱离正常服务状态,要么集群正在发生Leader切换,要么集群已经不满足“过半存活”的容错条件。
1.2 状态转换的触发条件,比单纯背状态更值钱
理解了四宫格的定位,下一步要看它们之间如何流转。这部分是很多面试解析一笔带过、但实际考察频率非常高的地方。
正常运行的节点只有三种终点:初始启动进入选举、享受稳定服务、异常后回到选举。展开来看分为几条路径:
- 启动链路:服务端进程启动后,先加载本地快照(snapshot)和事务日志(txnlog),把数据恢复到内存,然后自然进入LOOKING,参与集群选举。选举完成后,胜出的节点进入LEADING,其余节点进入FOLLOWING或OBSERVING。
- Follower失联链路:FOLLOWING节点与Leader之间靠心跳维持关系,一旦超过阈值没有收到Leader的有效消息,Follower会自己进入LOOKING,重新发起选举,而不是原地等待。
- Leader降级链路:LEADING节点如果发现自己已经失去多数Follower的支持(比如网络分区,半数以上投票节点联系不上了),它会主动放弃Leader身份,进入LOOKING,避免出现“自己也联系不上多数,却还强行对外提供写服务”的脑裂状态。
- Observer的特例:OBSERVING节点不参与选举,所以Leader失联时它不会进入LOOKING,而是一直跟随当前集群状态,直到新Leader诞生后继续同步数据。
这里有一个容易踩坑的地方:很多人以为心跳超时就是tickTime,实际上Zookeeper把心跳超时拆成了两个参数。initLimit用于Follower启动时与Leader完成首次数据同步的上限;syncLimit用于运行过程中Leader与Follower之间心跳和同步的容忍上限。生产环境中这两个参数如果设置得不合理,会直接影响状态切换的触发时间。比如网络抖动频繁的机房,syncLimit按默认值2倍tickTime往往不够,容易导致Follower误判Leader失联,反复进入LOOKING。
1.3 服务端启动时那个“隐藏的LOADING阶段”
说四种状态,其实严格是指QuorumPeer.ServerState枚举里的四个值。但在真实服务端启动流程里,还有一个不在枚举中却客观存在的阶段——数据加载阶段,有些资料和源码注释里也会把它描述为LOADING。
节点在进入LOOKING之前,需要把本地磁盘上的快照文件和增量事务日志加载进内存,这个过程可能花费数百毫秒甚至更久,取决于数据量大小。这个阶段节点虽然端口已经监听,但它不参与选举、不处理请求,对外表现和“还没准备好”是一样的。面试时如果能把“在进入LOOKING之前还有一个数据加载阶段,所以伪集群中即使所有节点同时启动,也不是所有节点立刻都在LOOKING状态”这句话说出来,会明显比同龄候选人更有区分度。
我见过有人用“四字命令ruok”去判断节点是否在LOOKING,结果发现进程明明活着、命令返回imok,但集群就是选不出Leader。原因就是只看到了进程存活,没有看状态机的实际阶段。这个问题后面监控部分会详细展开。
2. LOOKING阶段才是一切的开始:Leader选举中的投票与收敛逻辑
2.1 选举投票究竟比的是什么:epoch、zxid、myid
当节点进入LOOKING后,会触发Fast Leader Election(FLE)算法。这个算法的核心不是“谁先喊谁是Leader”,而是所有LOOKING节点各自投票,然后相互交换投票结果,按照一套全局统一的比较规则收敛。这套规则值得一个单独的H2,因为面试官追问的“隐藏细节”基本都藏在这里。
每张投票里其实包含了三个关键信息:逻辑时钟(epoch)、事务ID(zxid)、服务器ID(myid)。比较规则优先级如下:
- 先比epoch,epoch越大,说明这个节点经历过的Leader任期越新,选它;
- epoch相同时比zxid,zxid越大,说明节点上数据越完整,选它;
- epoch和zxid都相同时比myid,myid越大,选它。
为什么要把zxid放在myid前面?因为zxid里不只是简单的递增数字。高32位是Leader任期编号,低32位是该任期内的事务序号。这意味着zxid越大,代表不仅事务序号新,还代表这个节点完整保留了最近一个Leader任期内所有已提交操作。如果让一个zxid小的节点当选,它后面还得向其他节点拉数据,麻烦得多。
举个实际例子:三节点集群,A的zxid是0x300000002,B的zxid是0x200000005,C的zxid是0x300000003。A和C都在epoch为3的任期内有事务,B还停留在epoch为2的旧任期。那么A和B比较时,A胜出;A和C比较时,C胜出;最终C会被选为Leader,因为它在最新任期内的zxid更大。
这里有个特别容易被忽略的细节:Zookeeper的选举并不是“一轮定生死”。每个节点在自己的逻辑时钟周期内只能投出一张有效票,一旦收到更好的票就更新自己的投票并重新广播。这个“不断比较、不断更新、直到某个节点发现自己获得了超过半数的选票”的过程,就是FLE的收敛机制。收敛时间取决于网络延迟和投票跳数,这也是为什么生产环境要注意不同机房节点之间的RTT,过高的网络延迟会成倍拉长LOOKING持续时间。
2.2 半数机制与脑裂防线:为什么偶数节点反而是浪费
关于半数机制,网上已经讲烂了,但和Server状态结合起来的追问其实很有杀伤力。Zookeeper集群中,判定一个Leader是否合法,不是看“得到票数最多”,而是看获得票数是否超过参与投票节点总数的一半。这个设计直接决定了集群容错能力。
假设集群有N个参与投票的节点,那么可以宕机存活的上限是floor((N-1)/2)。举例来说:
- 3台投票节点:最多挂1台,剩下2台过半,仍能选出Leader;
- 4台投票节点:过半门槛是3台,最多也只能挂1台,因为挂2台后只剩2台存活,没过半;
- 5台投票节点:最多挂2台,剩下3台过半;
- 6台投票节点:过半门槛是4台,最多挂2台,挂3台后只剩3台,同样不可用。
所以4台和3台的容错能力完全一样,都是最多挂1台。但4台集群需要维护更多节点、更多连接、更多同步开销,唯一好处是数据副本数量多一份,在某些读多场景下有一点点并列读的收益。真正合理的做法是:如果一定要凑4台,把第4台配成Observer,不参与投票,这样核心投票集合仍然是3台,既保持同样的容错,又额外获得一个只做数据同步的读扩展节点。
那脑裂是怎么被防住的?设想一个5节点集群被网络切成了2+3两个分区。2节点分区里,它们各自投票,发现无论如何都凑不到3票,于是一直处于LOOKING;3节点分区里能凑到3票,选出一个合法Leader继续服务。最终集群只存在一个对外可写的Leader。反过来如果允许“超过自身分区内多数”就能成为Leader,两个分区会各自选出Leader,数据就会彻底分叉。这就是为什么LOOKING状态可以看作脑裂防线的“哨兵”——节点在凑不齐多数时,宁可一直挂在LOOKING里不提供服务,也不要让自己变成被分区抛弃的伪Leader。
2.3 LOOKING期间对外表现:端口通、功能不可用
把LOOKING和“宕机”划等号不对,把LOOKING当成“正常运行之一”也不对。真实表现介于两者之间:网络层仍然在监听客户端端口,TCP连接可以建立,但整个集群尚未产生Leader,所以任何写请求都找不到处理者。
这个细节在面试和排障中都很关键。客户端连接串里通常会配置多台Zookeeper地址,如果客户端正好连到了一个处于LOOKING阶段的节点,它的读取请求也会被卡住,因为节点要等选举结束后才能确定自己是否具备对外服务资格。很多人会觉得“Zookeeper读请求是本地快照读,应该不受选举影响”,这个认知在集群正常时成立,但LOOKING阶段是例外:节点此时连自己的角色都没定,自然不会把本地数据直接提供给客户端。
我在生产环境里遇到过一类典型故障:某台机器因为磁盘IO卡顿被误判失联,触发Leader切换,整个集群进入恢复期。期间所有客户端报连接断开、请求超时,虽然另外两台节点还在,但它们在完成新Leader选举和数据同步之前,客户端体验就是“短暂不可用”。这不是Zookeeper设计缺陷,而是CAP里一致性和可用性的必然取舍。恢复耗时通常在几百毫秒到几秒之间,如果超过这个量级还停在LOOKING,那就要按故障处理了。
3. OBSERVING是被低估的状态:不投票的节点为什么能撑起读扩展
3.1 设计根源:所有Follower都投票,会拖慢写路径
Observer可能是四种状态里最被低估的。很多初学者以为它就是个“只读的Follower”,其实它和Follower的差别不只是“不能当Leader”这么简单,而是从设计根源上就不参与写投票。
Zookeeper的写请求走的是ZAB协议,Leader收到写请求后要广播给所有Follower,等大多数投票节点确认(ACK)后,才向客户端返回成功。注意这里的“大多数”只计算参与投票的节点。如果集群里只有3台Follower,写确认只需要2台反馈;如果扩到10台Follower,写确认则需要6台反馈。哪怕只多了一个参与投票的节点,整个写路径的广播成本和等待时间都会上升,而且Leader还要多做一份事务日志同步。
Observer的诞生就是为了解决这个矛盾:既想扩充读能力,又不愿意让写路径变重。Observer同样从Leader同步全部数据,但它不对写请求做ACK确认,也不参与选举投票。对Leader来说,Observer就像是一个“只听广播、不举手表决”的旁观者。因此,就算集群里挂了几个Observer,只要参与投票节点还满足过半条件,写路径完全不受影响。
有人可能问:那是不是可以把Observer加得越多越好?也不行。Observer同步数据仍然要占Leader的网络带宽和内存,Leader向外推送数据要遍历所有节点,Observer数量过大,一样会拖垮Leader的网络IO。所以Observer适合“几十个以内”的读扩展,而不是无限横向扩容。
3.2 读写分流的真实表现与一致性边界
生产环境中,Observer最常见的用途就是“读流量扩展”和“跨机房就近读”。客户端连接Observer,写请求会被Observer原样转发给Leader,读请求则直接从Observer本地返回。因为Observer持续从Leader同步数据,所以大部分读请求能在本地命中。
但要特别强调一个一致性边界:Observer的本地读是最终一致的。Zookeeper的同步是异步的,Observer落后Leader一小段时间是完全正常的。如果你在Observer上发起一个紧跟写操作的读请求,很可能读到旧值。这一点在很多面试场景里都容易被追问。
如果想在Observer上拿到相对新一点的数据,可以调用Zookeeper客户端的sync()接口,它会强制客户端与Leader做一次同步,让后续读请求能读到同步点之后的数据。不过sync()并不是“强一致读”的银弹,它只是把读取位置推进到接近最新,实际延迟还是受网络影响。所以严谨地说,Zookeeper的读请求从来就不是严格线性一致的,只是顺序一致性被保证在写路径上。
如果业务对读一致性要求很高,方案应该是全部走Leader读,代价是牺牲扩展性;或者用Zookeeper之外的一致性缓存层。这是另一个话题,但面试时能把“Observer适合最终一致读模型,不适合强一致读模型”这个边界说清楚,就已经比大多数人强了。
3.3 Observer的配置方式与生产坑
Observer的配置非常简单,核心是在配置文件里给目标节点声明角色。比如一个节点在配置文件里的格式是:
server.1=zk1:2888:3888 server.2=zk2:2888:3888 server.3=zk3:2888:3888 server.4=zk4:2888:3888:observer关键点在第4台后面的:observer后缀。没有这个后缀,它就是普通的参与投票节点;加上这个后缀,这台机器才会以Observer身份加入集群。注意这里需要说明的是,Observer为什么仍然需要2888和3888两个端口,因为2888用于和Leader之间同步数据,3888用于参与集群内部通信(虽然不投票,但也要接收选举广播结果)。
生产环境中配置Observer常见两个坑:
- 第一个坑:把Observer和参与投票节点混淆,导致容错预期错误。比如一个5台集群里配了2台Observer,实际投票节点只有3台,容错上限仍然是挂1台。如果挂了2台投票节点,即便另外3台Observer都活着,集群也选不出 Leader,整体不可用。
- 第二个坑:忘记Observer也参与四字命令监控范围。
stat、mntr输出里Observer的Mode或zk_server_state显示为observer,如果监控脚本只认 leader/follower 两种状态,就会把Observer误判为异常。
另外,一旦节点配置为Observer,它就永远不会主动成为Leader。哪怕其他所有节点都宕机了,只剩一个Observer和一个Follower,这个Observer也只是等待Follower那边选出状态,自己不会去抢Leader。这个特性在脑裂和降级场景中一定要想清楚,别把“Observer存活”当作“选举还能正常进行”的依据。
4. 生产环境怎么盯状态:四字命令、日志与监控指标的实际读法
4.1 stat/srvr/mntr的输出怎么读
前面讲的都是原理,落地到运维环节,最重要的就是“能看见状态”。Zookeeper提供了一组四字命令,最常用的三个是stat、srvr、mntr。每台Zookeeper节点上都可以这样执行:
echo mntr | nc 127.0.0.1 2181输出大致长这样(不同版本字段略有差异):
zk_version 3.9.1 zk_server_state leader zk_znode_count 1234 zk_watch_count 8 zk_ephemerals_count 5 zk_approximate_data_size 54321关键字段就是zk_server_state,只可能出现leader、follower、observer或者启动初期的looking几种值。stat和srvr的输出里也有一个Mode字段,含义相同,但srvr更精简,适合快速确认当前节点角色。
这里有一个很常见的坑:较新版本的Zookeeper默认没有开放全部四字命令,需要在配置里加白名单,否则执行echo mntr | nc会直接返回空或xxx is not executed because it is not in the whitelist。要放行什么命令,可以自己按需配置:
4lw.commands.whitelist=stat,srvr,mntr,ruok,conf要注意ruok这个命令非常具有迷惑性。它只判断“当前进程是否活着”,能正常返回imok只代表JVM没挂,不代表节点已经完成选举或者数据已同步。生产环境里用它做健康检查,不如看mntr里的zk_server_state更可靠。
读这些输出时,我的习惯是做一个汇总表:
| 命令 | 关键输出 | 用途 |
|---|---|---|
| stat | Mode、Zxid、Connections | 快速排查节点角色和连接数 |
| srvr | Mode、Sent/Received、Pending Syncs | 判断节点同步压力 |
| mntr | zk_server_state、磁盘使用率等指标 | 批量监控和采集 |
| ruok | imok | 只适合进程存活探测,不适合状态机判断 |
| conf | serverId、tickTime、dataDir | 核对配置是否生效 |
4.2 日志里的状态变化线索
四字命令只能看到“当下状态”,想看状态变化的完整过程,还得结合日志。Zookeeper服务端的日志里,状态切换是有明确标志的。
节点启动后会打印加载快照和事务日志的信息,加载完成,进入选举阶段时会有类似LOOKING的日志关键字。之后胜选节点会打印类似LEADING - LEADER ELECTION TOOK - 17ms的信息,表示它成为Leader并计算了选举耗时;Follower节点则会出现FOLLOWING相关日志,Observer节点会体现为以Observer身份连接Leader。
对于排障来说,日志最大的价值在“状态来回跳”的场景。如果日志里频繁出现FOLLOWING然后很快又出现LOOKING,说明节点反复与Leader失联又恢复,这通常和网络抖动、负载过高、或者syncLimit设置过小有关。我曾经在排查一个集群Leader频繁切换的故障时,就是靠搜索日志里所有LEADER ELECTION TOOK的时间间隔,发现每10分钟左右就触发一次重新选举,再配合系统监控定位到是某台机器上的GC暂停导致心跳超时。
建议每个Zookeeper集群都接日志采集,至少把*.log里包含LOOKING、LEADING、FOLLOWING、OBSERVING四个关键字的行上报到监控平台,一旦出现同一节点在短时间内多次LOOKING,立刻触发告警。
4.3 结合Hadoop HA看真实场景
讲完命令和日志,用一个真实场景把它们串起来:Hadoop的高可用(HA)架构,这是Zookeeper在实际业务中最大的应用之一。
HDFS的NameNode高可用通过ZKFC(ZooKeeper Failover Controller)实现。每个NameNode会有一个ZKFC进程,它会在Zookeeper集群里创建临时节点。正常情况下,只有Active NameNode对应的ZKFC持有这个临时节点,Standby NameNode的ZKFC在另一边持续监控它。一旦Active NameNode异常,Zookeeper中的临时节点消失,Standby NameNode立即接管并尝试成为新的Active。
这套机制里,Zookeeper节点自身的Server状态决定了它能对外提供多可靠的服务。如果Zookeeper集群里某台节点进入了LOOKING,对外表现就是临时节点可能在短时间内不可访问,ZKFC会重试连接。如果整个Zookeeper集群因为投票节点不足而陷入长时间LOOKING,NameNode的自动故障转移就会失败,HDFS只能进入不可用状态。
所以监控Zookeeper状态时,“当前没有Leader的秒数”是最核心的健康指标。可以写一个简单的状态轮询脚本,每5秒对所有节点执行一次mntr,统计zk_server_state等于looking的节点数量,只要这个数量不为0,就说明集群正在经历选举或已经异常。如果持续超过设定阈值还没有恢复,就触发告警。这个脚本逻辑简单,但比单纯检查进程存活有效得多,也是我在实际运维里非常推荐的做法。
5. 从四种状态延伸到面试必杀的追问链:会话、请求与故障判读
5.1 状态切换对客户端会话和请求的影响
面试官问完四种状态是什么,紧接着一定会问:“状态切换时,客户端会怎样?”这个问题的答案,藏着理解Zookeeper一致性和容错机制的关键。
先看写请求。客户端发起一个写操作,如果连接的是Leader,服务端直接走ZAB广播;如果连接的是Follower或Observer,请求会被转发到Leader。当集群发生Leader切换时,处于FOLLOWING状态的旧节点发现自己联系不上Leader后,会进入LOOKING。它不会继续接收新请求,已经接收但还没提交的事务会暂时挂起。客户端这边看到的表现通常是ConnectionLoss异常,需要在客户端代码里做重试。但注意,Zookeeper的写请求重试不保证“恰好一次”,也就是说,同一个写操作可能因为超时被客户端重试,服务端可能已经执行了,导致重复提交。这需要业务层自行保证幂等。
再看会话。Zookeeper客户端和服务端之间是通过Session(会话)维系的,Session信息主要保存在Leader和Follower上。Leader切换后,新Leader会从多数节点中选择数据最完整的那个作为基准,未提交的事务会被丢弃或回滚,但已提交的事务必须保留。客户端如果配置了多个Zookeeper地址,在重连时会被引导到新Leader上,只要Session仍然有效,就能继续使用。这里有个细节:Session过期时间一般由minSessionTimeout和maxSessionTimeout框定,生产环境如果设置过短,切换期间稍有停顿就会导致会话过期,客户端大量报错。
所以面试时,这个问题可以这样分三层回答:连接层会出现ConnectionLoss;数据层旧Leader上未提交的事务会被ZAB协议丢弃;会话层只要客户端重连及时,会话可以保留,但重试需要考虑幂等。能说到这个程度,基本就覆盖了面试官想听的核心。
5.2 故障判读速查:长时间LOOKING说明了什么
最后聊一个运维中特别实用的场景:什么时候可以判定Zookeeper集群已经“病入膏肓”?我给的指标是:大多数投票节点长时间处于LOOKING,并且无法选出一个Leader。
导致这种局面的常见原因有三类。第一类是投票节点数量不足,比如5台投票节点挂了3台,剩下2台永远凑不够3票;第二类是网络分区,所有节点各自为战,任何一方的存活节点都凑不够多数;第三类是磁盘或负载异常导致节点反复崩溃,始终无法完成数据加载和选举。
排查链路我建议按这个顺序来:
- 对每台节点执行
mntr,收集所有节点的zk_server_state,画出当前状态分布; - 搜索所有节点日志中的
LOOKING和LEADER ELECTION TOOK,观察选举是否反复发生; - 检查网络层,看投票节点之间的2888端口是否是通的,延迟是否稳定;
- 查看磁盘空间、IO负载和JVM监控,排除节点自身“假死”导致的心跳失联。
这里我特别想分享一个真实教训:一套5台Zookeeper集群里配了2台Observer,某次机房割接后,有2台投票节点和1台Observer之间的网络出现了严重丢包。我一开始只盯着observer节点的状态看,发现它是looking还是正常,结果迟迟找不到根因。后来才意识到,我一直忽略了一个基础事实——Observer不参与投票,它的存活完全不能弥补投票节点不足的问题。那次割接里恰好宕掉2台投票节点,剩下的3台中又有2台处于网络隔离区,凑不够3票,整个集群陷入不可用。事后复盘,监控上其实早就报警了,但那个“无Leader持续秒数”的指标当时没有配置,导致故障被延迟发现。
现在我的标准配置是,任何Zookeeper集群都必须监控“无Leader秒数”和“投票节点存活数”这两个指标,前者反映选举是否完成,后者反映容错余量。Observer的监控单独做,只关注同步延迟和读负载,不要混进投票健康度里。
这四种状态背后的状态机、选举协议和一致性模型,单独拆开其实都不难,但它们组合起来就是Zookeeper面试的完整考察面。我面过不少人,也带过几个新同学,最后发现能把OBSERVING的投票边界和启动时那个隐藏加载阶段主动讲清楚的人,才是真正摸过源码、跑过集群的人。建议你自己搭一个三节点伪集群,手动kill掉Leader,看一遍状态切换的完整日志,再结合四字命令观察每个阶段的变化,这一遍下来,比背十遍知识点都管用。