如果你也在用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,欢迎建设性交流。

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

相关推荐
ServBay7 小时前
月之暗面 Kimi Code 0.4.0 发布,终端 AI 编码助手全面采用 TypeScript,实现毫秒级启动
后端·aigc·ai编程
小江的记录本7 小时前
【JVM虚拟机】垃圾回收GC:垃圾回收算法:标记-清除、标记-复制、标记-整理、分代收集(附《思维导图》+《面试高频考点清单》)
java·jvm·后端·python·算法·安全·面试
小江的记录本7 小时前
【JVM虚拟机】垃圾回收GC:垃圾收集器:G1:Region分区、Mixed GC、回收流程、适用场景(高频)(附《思维导图》+《面试高频考点清单》)
java·jvm·后端·python·spring·spring cloud·面试
欧雷殿8 小时前
从「吸引子引导工程」看我的「一人公司」实践
前端·人工智能·后端
卷无止境9 小时前
用一个电影院售票厅,把 SimPy 的条件事件讲透
后端
日月云棠9 小时前
9 Double 与 Float —— IEEE 754 浮点数在 Java 中的实现
java·后端
日月云棠9 小时前
5 StringBuffer —— 线程安全的可变字符串
java·后端
砍材农夫9 小时前
物联网 基于netty核心实战-会话管理
后端
元宝骑士9 小时前
MySQL 8.0 递归 CTE:树形结构一键生成层级 Path 并更新回表
后端·mysql