记一次Zookeeper的IO Hang问题

过程记录

       最近在进行zookeeper的Failover测试,测试过程中,发现当频繁的对leader角色进行断网操作时,zookeeper会长时间停止服务,直到leader节点网络恢复。 zookeeper是一种典型的Paxos协议衍生品,满足CAP理论的A和P两个特性, 是可以Handle这种比较常见的出现网络分区场景的。当Leader节点出现网络故障时,整个quorum的状态变化应该是如下图 A -> B -> C进行,如果故障节点后面网络重新恢复,quorum还会到达D状态。如下图所示:

记一次Zookeeper的IO Hang问题


     上面的状态变化是Leader发生网络分区时,应该出现的状态变化。 事实上,绝大多数情况下,Leader出现网络分区时,zookeeper确实是按照上面的状态变化进行的。 然而,测试出现的两个节点不能选举出新的Leader是什么原因呢?


问题剖析

       通过查看剩余两个节点的日志,看到当时一台follower节点在leader断网一定时间后,是感知到和leader的连接异常的,并退出角色,进入选举状态。然而,不符合预期的是,另一台follower节点一直没有退出角色,当它收到选票后,回复自己当前是follower,并且leader当前是xxx。那台进入选举状态的节点epoch值大于这台异常的follower节点,所以会一直选举。 

       现在问题清楚了,是一台follower节点出现了异常,导致无法退出角色。进行到这里,下面我们要开始捉虫子(find the bug)了。

在zookeeper源码中判断Follower在和Leader通信健康与否,是一个followLeader的方法实现的,该方法实现较为简单:

       定期从leader读取数据,并处理,部分数据会写回给leader。其中,读取数据和回写数据,均是通过一个数据通道进行,对应一个socket连接。 

       实现中,也对该socket进行了setSotimeout和setTcpNodelay为true的操作,确保read调用在期望时间内可以返回,write调用非阻塞。看起来该部分并不会出现hang住的问题。难道是线程所在的cpu-core不工作了(这个概率可以认为是买**连续3次中五百万的概率)。  

记一次Zookeeper的IO Hang问题

       这部分逻辑很简单,比较容易调试,怀疑是processPacket方法hang住了。 通过增加日志trace point,复现该现象,和我们的猜测一样,hang在了processPacket中synchronized一个套接字对象上,该对象会被FollowerRequestProcessor等多个线程共享,其他线程使用该对象也均是用来发送数据。 所以,可以推断是其他线程持有该对象后,hang住了,进而导致followLeader无法持有该对象锁。  再次查看问题发生时的日志,当故障节点恢复时,FollowerRequestProcessor线程抛出socket异常,可以推断FollowerRequestProcessor线程在向leader转发数据时hang住了。也就是socket的write方法hang住了。

      基于该猜测,寻求Google,果不其然,socket提供的write方法在下面一种情况下是会hang住的:写缓冲区满

      事实上,socket在底层包含两个缓冲区,输出缓冲区和输入缓冲区,也可称作写和读缓冲区。当输出缓冲区有空间时,write方法将数据放入缓冲区后,立即返回,但如果输出缓冲区已经满了,write方法就会阻塞住。

      在我们的Failover场景中,当断网Leader后,大量的client会将请求转移至两个follower节点,follower节点也会将部分请求转发给leader(放入输出缓冲区),当输出缓冲区满时,继续write会被hang住。  那么,在followLeader线程刚从leader读取数据,尚未进行写回时,这个时候如果FollowRequestProcessor写入大量数据,就会将缓冲区写满。从而造成该socket上的所有write方法hang住。 当然这个概率不是很高, 数千次断网Leader测试,才会撞到几次,想必这也是这个问题一直未发现的原因之一。


问题反思

       通过这个过程,有点心得,对于没有明确说明的接口,不要去臆测它的行为是什么。对于核心路径上的逻辑,一定要反复推敲,确保每一次调用在任何情况下都是符合预期的。同时,需要对这些核心路径的运行逻辑进行监控,当检测到核心路径不再按照预期工作时,应该将其尽快恢复到安全状态。