0结论1问题背景2现象2.1环境配置2.2现象3排查过程3.110号3888端口的状态3.210号的jstack状态3.36号的3888端口也挂了4原因分析4.1网上求证4.2高版本优化5总结0结论结论先行:ZK在3.4.6及以下版本存在选举端口(默认3888)失效而无法选举的重大漏洞。相关issue如下:issue-3016:FollowerQuorumCnxManager$Listenerthreaddiedduetoincorrectclientpacket[1]issue-2186:QuorumCnxManager#receiveConnectionmaycrashwithrandominput[2]简单来说,ZK的选举端口3888在收到错乱的数据包时,可能会因创建负数大小的数组而抛出NegativeArraySizeException,导致选举端口的监听线程QuorumCnxManager$Listener整体退出,从而无法选举。集群还能正常读写,但无法选举,一旦有节点重启就加入不了,坑不坑!!!1问题背景12月20日,一个阳光明媚的寒冬午后,刚从午睡中醒来,两眼朦胧,运维部大C带着一脸坏笑走过来,如下是我俩的对话。大C:云杰,咱们的ZK集群有两个节点加入不了集群了。我:(立马就清醒了)5个挂了俩,现在就剩下3个工作,再挂1个集群就整体挂了啊!怎么回事?大C:是啊。我们也没改啥配置,就是正常重启下,发现加入不了了,以前也都没事。我:不会吧?这么神奇!关键我们对ZK也没经验啊!大C:就是这么神奇!ZK是Java写的,我们运维基本不用Java,你们架构肯定更专业点。我:好吧,你说的好像也没错。抱着求知的心态,开启了本篇的探索之旅。2现象2.1环境配置5个节点的zoo.cfg配置如下:server.6=10.40.xx.81:2888:3888server.7=10.40.xx.41:2888:3888server.8=10.40.xx.51:2888:3888server.9=10.40.xx.111:2888:3888server.10=10.40.xx.121:2888:38882.2现象6号和8号已重启,但无法加入;7号、9号和10号未重启,仍能正常读写,10号为Leader。2.2.1已重启节点6号ZK日志报错如下:表明无法与未重启节点的3888进行选举通信。在6号上使用zkCli.sh登陆本机也失败:表示无法在本机上读写,完全处于游离状态。2.2.2未重启节点10号结节上使用zkCli.sh登陆本机仍能正常读写:3排查过程3.110号3888端口的状态重启节点无法与未重启节点的3888端口进行选举通信,那10号的3888端口是什么状态呢?我们在10号上用netstat命令看下3888端口的状态:表明3888端口仍处于LISTEN的状态,并伴有大量的CLOSE_WAIT连接。大C说这些10.177开头的IP是用于安全扫描的机器,心里也在嘀咕是不是跟这个有关。既然3888是LISTEN状态,那我们telnet看下建立连接的状态,如下所示:完了,虽然是LISTEN状态,但根本无法建立连接!!!我们再从telnet端用netstat看下TCP连接的情况:我去,都是SYN_SENT状态!也就是说,SYN包发送成功了,但根本没收到10号3888端口的建连ACK。这也就表明10号的3888已处于假死状态,根本不会响应。查看10号的日志,已经滚动26GB,也没发现什么有效的异常。我们又telnet下6号的3888端口,发现却正常:3.210号的jstack状态到了这里,虽然确定10号3888端口假死,但再往下走毫无头绪。这时灵光一现,我们ITCP联盟有个架构群,可以看看大家有没有相关的经验,于时紧急求助。果然,群里去哪儿网架构的小伙伴也来了兴趣,积极响应。去哪儿小伙伴也拿他们ZK节点的jstack跟我们的10号节点对比了下,果然发现了问题:跟他们相比,我们缺少了个QuorumCnxManager$Listener线程:这个线程是负责监听3888端口,并accept选举请求的。我们未重启节点里这个线程都没了,怪不得不能接收6号的选举请求!!!3.36号的3888端口也挂了到了这里,已经知道是因为QuorumCnxManager$Listener线程挂了,但什么原因导致的还没头绪。这时,突然发现6号的3888端口也telnet不通,并出现跟10号一样有大量CLOSE_WAIT:我们赶紧看了下6号的最近日志,果然发现了一条报错:报了NegativeArraySizeException异常,导致3888的监听线程挂掉。在6号jstack下,发现果然没有QuorumCnxManager$Listener线程。紧接着我们把6号重启下,果然发现又有了:4原因分析4.1网上求证有了这么多信息(QuorumCnxManager$Listener、NegativeArraySizeException等),我们足可以去网上检索求证了。果然,搜索前列即是结论中的issue。那是什么原因导致抛出NegativeArraySizeException呢?public boolean receiveConnection(Socket sock) { Long sid = null;... sid = din.readLong(); // next comes the #bytes in the remainder of the message int num_remaining_bytes = din.readInt(); byte[] b = new byte[num_remaining_bytes]; // remove the remainder of the message from din int num_read = din.read(b);从代码上看,是因为QuorumCnxManager$Listener接收到数据包后,会调用receiveConnection()进行处理。该函数从数据包里读出一个int类型到变量num_remaining_bytes,并据此创建一个byte数组。但读到的num_remaining_bytes为负数,从而导致创建数组失败,抛出NegativeArraySizeException异常。为什么读到的是负值呢?那肯定是接收到错乱的数据包了!这时我们就联想到10号上有大量10.177IP的CLOSE_WAIT连接,原来是安全扫描的锅!!!4.2高版本优化看issue-2186说,3.4.7版本已经修复了,我们对比了下目前在用的3.4.6:果然,对num_remaining_bytes值进行了判断。至此,经过近6小时的排查,原因也水落石出了:安全扫描的错乱数据包把ZK的3888选举端口搞挂了!解决方案就是升级高版本ZK!!运维大C也投来了敬佩的目光:同时也不由感慨ZK的QuorumCnxManager$Listener监听线程太脆弱了:去哪儿大佬小黑哥更直观形象的表示用telnet再发送个-1就可能把ZK集群选举搞挂!5总结遇到认知外的问题是好事,勇敢面对,要有刨根问底的决心,过程全是成长;最大的成长不是把问题解决了,而是排查思路;借用雷总的一句话:“99%的问题,都有标准答案,找个懂的人问问”。及时向懂的人求助也不失为一个好办法,在此也感谢ITCP联盟小伙伴的及时相助;很多看似强大的东西遇到点意外可能会很脆弱。关于作者杜云杰,高级架构师,转转架构部负责人,转转技术委员会执行主席,腾讯云TVP。负责服务治理、MQ、云平台、APM、IM、分布式调用链路追踪、监控系统、配置中心、分布式任务调度平台、分布式ID生成器、分布式锁等基础组件。微信号:waterystone,欢迎建设性交流。道阻且长,拥抱变化;而困而知,且勉且行。参考资料[1]issue-3016:https://issues.apache.org/jira/browse/ZOOKEEPER-3016[2]issue-2186:https://issues.apache.org/jira/browse/ZOOKEEPER-2186