如果你也在用ZK,那这个导致集群挂掉的坑一定得注意!

0 结论

结论先行:ZK在3.4.6及以下版本存在选举端口(默认3888)失效而无法选举的重大漏洞。相关issue如下:

简单来说,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配置如下:

ini 复制代码
server.6=10.40.xx.81:2888:3888
server.7=10.40.xx.41:2888:3888
server.8=10.40.xx.51:2888:3888
server.9=10.40.xx.111:2888:3888
server.10=10.40.xx.121:2888:3888

2.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.1 10号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.2 10号的jstack状态

到了这里,虽然确定10号3888端口假死,但再往下走毫无头绪。这时灵光一现,我们ITCP联盟有个架构群,可以看看大家有没有相关的经验,于时紧急求助。

果然,群里去哪儿网架构的小伙伴也来了兴趣,积极响应。

去哪儿小伙伴也拿他们ZK节点的jstack跟我们的10号节点对比了下,果然发现了问题:

跟他们相比,我们缺少了个QuorumCnxManager$Listener线程: 这个线程是负责监听3888端口,并accept选举请求的。我们未重启节点里这个线程都没了,怪不得不能接收6号的选举请求!!!

3.3 6号的3888端口也挂了

到了这里,已经知道是因为QuorumCnxManager$Listener线程挂了,但什么原因导致的还没头绪。

这时,突然发现6号的3888端口也telnet不通,并出现跟10号一样有大量CLOSE_WAIT

我们赶紧看了下6号的最近日志,果然发现了一条报错: 报了NegativeArraySizeException异常,导致3888的监听线程挂掉。在6号jstack下,发现果然没有QuorumCnxManager$Listener线程。

紧接着我们把6号重启下,果然发现又有了:

4 原因分析

4.1 网上求证

有了这么多信息(QuorumCnxManager$ListenerNegativeArraySizeException等),我们足可以去网上检索求证了。果然,搜索前列即是结论中的issue。那是什么原因导致抛出NegativeArraySizeException呢?

java 复制代码
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,欢迎建设性交流。

道阻且长,拥抱变化;而困而知,且勉且行。

相关推荐
运维@小兵3 小时前
SpringBoot获取用户信息常见问题(密码屏蔽、驼峰命名和下划线命名的自动转换)
java·spring boot·后端
佳腾_3 小时前
【分布式系统中的“瑞士军刀”_ Zookeeper】三、Zookeeper 在实际项目中的应用场景与案例分析
分布式·zookeeper·云原生
问道飞鱼5 小时前
【springboot知识】配置方式实现SpringCloudGateway相关功能
java·spring boot·后端·gateway
樽酒ﻬق5 小时前
打造美观 API 文档:Spring Boot + Swagger 实战指南
java·spring boot·后端
ErizJ5 小时前
Golang | 位运算
开发语言·后端·golang·位运算
冼紫菜6 小时前
[特殊字符] Docker 从入门到实战:全流程教程 + 项目部署指南(含镜像加速)
运维·分布式·后端·docker·云原生·容器
秋野酱7 小时前
基于Spring Boot+Vue 网上书城管理系统设计与实现(源码+文档+部署讲解)
vue.js·spring boot·后端
编程毕设8 小时前
【含文档+PPT+源码】基于SpringBoot电脑DIY装机教程网站的设计与实现
java·spring boot·后端
caihuayuan58 小时前
IOS 国际化词条 Python3 脚本
java·大数据·spring boot·后端·课程设计
我的golang之路果然有问题9 小时前
案例速成GO+Socket,个人笔记
开发语言·笔记·后端·websocket·学习·http·golang