一次 Netty 代码不健壮导致的大量 CLOSE_WAIT 连接原因分析

news2024/11/25 0:22:37

背景

我们线上有一个 dubbo 的服务,出现大量的 CLOSE_WAIT 状态的连接,这些 CLOSE_WAIT 的连接出现以后不会消失,这就有点意思了,于是做了一下分析记录如下。

首先从 TCP 的角度看一下 CLOSE_WAIT

CLOSE_WAIT 状态出现在被动关闭方,当收到对端 FIN 以后回复 ACK,但是自身没有发送 FIN 包之前。

所以这里的原因就很清楚了,出现永远存在的 CLOSE_WAIT 的连接是因为,收到了对端的 FIN 包,但是自己一直没有回复 FIN。通过抓包确实验证了这个的想法。

问题就落在了为什么没有回复 FIN,这是一个健康检查探测的请求,三次握手成功以后,探测服务会马上发送 FIN,理论上 dubbo 服务也会立刻回复 FIN,但是没有任何反应。

对于 dubbo 底层使用的 netty 来说,它就是一个普通的 tcp 服务端,无非就这几步:

  1. bind、listen
  2. 注册 accept 事件到 epoll
  3. epoll_wait 等待连接到来
  4. 连接到来时,调用 accept 接收连接
  5. 注册新连接的 EPOLLIN、EPOLLERR、EPOLLHUP 等事件到 epoll
  6. epoll_wait 等待事件发生

如果是没有发送 fin,有几个比较明显的可能原因。

  1. 第 2 步没有做,压根没有注册 accept 事件(可以排除,肯定有注册)
  2. 第 4 步没有做,连接到来时,netty 「忘了」调用 accept 把连接从内核的全连接队列里取走。这里的「忘」可能是因为逻辑 bug 或者 netty 忙于其他事情没有时间取走,这个待会验证
  3. 第 5 步没有做,取走了连接,三次握手真正完成,但是没有注册新连接的后续事件

第 2 个原因可以通过半连接队列、全连接队列的积压来确认。ss 命令可以查看全连接队列的大小和当前等待 accept 的连接个数。

ss -lnt | grep :9090
State      Recv-Q Send-Q Local Address:Port               Peer Address:Port
LISTEN     51     50           *:9090                     *:*
复制代码
  • 处于 LISTEN 状态的 socket,Recv-Q 表示当前 socket 的完成三次握手等待用户进程 accept 的连接个数,Send-Q 表示当前 socket 全连接队列能最大容纳的连接数
  • 对于非 LISTEN 状态的 socket,Recv-Q 表示 receive queue 的字节大小,Send-Q 表示 send queue 的字节大小

通过 ss 命令确认过 Recv-Q 为 0,全连接队列没有积压。

至此最大的嫌疑在第 3 个原因,netty 确实调用了 accept 取走了连接,但是没有注册此连接的任何事件,导致后面收到了 fin 包以后无动于衷。

为什么 netty 没有能注册事件?

到这里暂时陷入了僵局,但是有一个跟此次问题强相关的现象浮出了水面,就是业务实例在凌晨 1 点有个定时任务,一开始就 load 了大量的数据到内存中,导致堆内存占满,持续进行 fullgc

netty 线程也有打印 oom 异常。

这里的 OOM 异常上面的一个 warning 引起了同事斌哥的主意,去 netty 源码中一搜索,发现出现在 org.jboss.netty.channel.socket.nio.NioServerBoss#process 方法中(netty 版本很古老 3.7.0.final)

  1 @Override
  2 protected void process(Selector selector) {
  3 Set<SelectionKey> selectedKeys = selector.selectedKeys();
  4 if (selectedKeys.isEmpty()) {
  5     return;
  6 }
  7 for (Iterator<SelectionKey> i = selectedKeys.iterator(); i.hasNext();) {
  8     SelectionKey k = i.next();
  9     i.remove();
 10     NioServerSocketChannel channel = (NioServerSocketChannel) k.attachment();
 11
 12     try {
 13         // accept connections in a for loop until no new connection is ready
 14         for (;;) {
 15             SocketChannel acceptedSocket = channel.socket.accept(); // 调用 accept 从全连接队列取走连接
 16             if (acceptedSocket == null) {
 17                 break;
 18             }
 19             registerAcceptedChannel(channel, acceptedSocket, thread); // 为新连接注册事件
 20         }
 21     } catch (CancelledKeyException e) {
 22         // Raised by accept() when the server socket was closed.
 23         k.cancel();
 24         channel.close();
 25     } catch (SocketTimeoutException e) {
 26         // Thrown every second to get ClosedChannelException
 27         // raised.
 28     } catch (ClosedChannelException e) {
 29         // Closed as requested.
 30     } catch (Throwable t) {
 31         if (logger.isWarnEnabled()) {
 32             logger.warn(
 33                     "Failed to accept a connection.", t);
 34         }
 35
 36         try {
 37             Thread.sleep(1000);
 38         } catch (InterruptedException e1) {
 39             // Ignore
 40         }
 41     }
 42 }
 43 }
复制代码

第 15 行 netty 调用 accept 从全连接队列取走连接,第 19 行调用 registerAcceptedChannel,将当前 fd 设置为非阻塞同时为新连接 fd 注册事件,具体的逻辑是在 org.jboss.netty.channel.socket.nio.NioWorker.RegisterTask#run中。

从错误日志中可以知道,这个方法确实抛出了 java.lang.OutOfMemoryError 异常。

因此这里的原因就很清楚了,netty 这里的处理确实不健壮,一个 try-catch 包裹了 accept 连接和注册事件这两个逻辑,当第 15 行 accept 成功,但在 19 行 registerAcceptedChannel 内部尝试注册事件时因为线程 OOM 排除异常时就凉凉了,没有close 这个新连接,就导致了后面收到 fin 以后根本不会回复任何包(epoll 里压根没有这个 fd 的感兴趣事件)。

模拟复现

有几种方法,直接字节码注入一下,抛出异常或者直接改 netty 源码重新构建一下。因为本地有 netty 的源码,采用了此方法更快。

重新构建项目,然后用 nc 模拟健康检查握手然后 ctrl-c 断开连接。

这个 CLOSE_WAIT 就一直存在了直到 netty 进程退出。再来一次 nc 然后断开就又多了一个 CLOSE_WAIT

因为我们线上的服务的健康检查一直在进行,导致 OOM 期间 CLOSE_WAIT 持续增加。写一个最简单的 go 程序模拟持续的健康检查

func main() {
	for i := 0; i < 200; i++ {
		println(i)
		conn, err := net.Dial("tcp", "192.168.31.197:20880")
		if err != nil {
			println(err)
			time.Sleep(time.Millisecond * 1500)
			continue
		}
		conn.Close()
		time.Sleep(time.Millisecond * 1500)
	}
	time.Sleep(time.Minute * 20000000)
}
复制代码

确实会出现大量 CLOSE_WAIT

到这里的问题就很清楚了,总结就是 netty 的代码不够健壮,一个 try-catch 包裹的逻辑太多,在 OOM throwable 异常处理时,没能成功注册事件也没有 close 已创建的连接,导致连接存在但是没有人监听事件处理。

可能有人会的一些疑问,为什么没有人监听事件了,收到 fin 包,还是会回复 ACK?

因为回复 ACK 是内核协议栈的行为,不需要应用参与,也不需要关心是否有人感兴趣。

如何修改

修改就很简单了,在 catch 的 throwable 逻辑里关闭一下就可以了,这里就不贴代码了。

最新版本的 netty 代码这部分代码看起来应该是完善了(没有去做实验),它把 accept 和注册事件拆分开了,感兴趣的同学可以试试。

后记

学好 TCP、网络编程是解决这些类似问题的利器,隔离在家一起学起来。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/45761.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据分析之金融数据分析

一 前言 金融业是一个持续发展的行业&#xff0c;金融业正在使用数据分析进行金融&#xff0c;以最大程度地减少管理各种金融活动所需的精力和时间。这些公司正在利用数据分析和机器学习原理的力量。这有助于他们发现金融行业各个领域所需的进步&#xff0c;以重塑其业务战略。…

虹科分享 | 网络仿真器 | 预测云中对象存储系统的实际性能

对象存储是一种在云中存储非结构化数据的方法&#xff0c;从理论上讲&#xff0c;它使得以其原始格式存储几乎无限量的数据成为可能。在这种存储架构中&#xff0c;数据被作为对象进行管理&#xff0c;而传统的系统则将数据作为块或分层文件进行处理。对象存储可以在内部使用&a…

一条Select语句在MySQL-Server层的执行过程

select customer_id,first_name,last_name from customer where customer_id14;先连接到数据库&#xff0c;连接器 负责跟客户端建立连接、获取权限、维持和管理连接。 客户端再次发送请求&#xff0c;就会使用同一个连接&#xff0c;客户端如果长时间没动静&#xff0c;就会断…

用R Shiny生态快速搭建交互Web网页APP应用

什么是Shiny&#xff1f; Shiny包可以快速搭建基于R的交互网页应用。对于web的交互&#xff0c;之前已经有一些相关的包&#xff0c;不过都需要开发者熟悉网页编程语言&#xff08;html,CSS,JS&#xff09;。最近我们被客户要求撰写关于R Shiny的研究报告&#xff0c;包括一些…

使用 Huggingface Trainer 对自定义数据集进行文本分类

文本分类是一项常见的 NLP 任务&#xff0c;它根据文本的内容定义文本的类型、流派或主题。Huggingface&#x1f917; Transformers 提供 API 和工具来轻松下载和训练最先进的预训练模型。Huggingface Transformers 支持 PyTorch、TensorFlow 和 JAX 之间的框架互操作性。模型还…

JAVA学习-java基础讲义01

java基础讲义一 java语言1.1 java语言介绍1.1.1 什么是java1.1.2 java之父1.1.3 java语言发展史1.2 java语言的特点二 java环境搭建相关2.1 Java环境介绍2.1.1 虚拟机介绍2.1.2 JVM介绍2.2 Java跨平台2.2.1 跨平台2.2.2 跨平台原理2.3 java运行过程2.4 JDK、JRE、JVM关系图2.4.…

JaVers:自动化数据审计

在开发应用程序时&#xff0c;我们经常需要存储有关数据如何随时间变化的信息。此信息可用于更轻松地调试应用程序并满足设计要求。在本文中&#xff0c;我们将讨论 JaVers 工具&#xff0c;该工具允许您通过记录数据库实体状态的更改来自动执行此过程。 Javers如何工作&#x…

RT-thread lts-v3.1.x版本,GD32F450以太网,上电之后有一定概率ping不通问题处理。

先给结论 官方驱动没有按照GD32F4XX手册要求&#xff0c;等待ENET_DMA_CTL第20bit清0后再写 synopsys_emac.c 文件&#xff0c;void EMAC_FlushTransmitFIFO(struct rt_synopsys_eth * ETHERNET_MAC)函数&#xff0c;增加一句判断即可解决。 /*** Clears the ETHERNET transm…

Kotlin高仿微信-第4篇-主页-消息

Kotlin高仿微信-项目实践58篇详细讲解了各个功能点&#xff0c;包括&#xff1a;注册、登录、主页、单聊(文本、表情、语音、图片、小视频、视频通话、语音通话、红包、转账)、群聊、个人信息、朋友圈、支付服务、扫一扫、搜索好友、添加好友、开通VIP等众多功能。 Kotlin高仿…

Android Studio / IDEA 调试金手指:live template自动打印方法名以及所有变量

ctrl alt s 搜设置&#xff0c;template&#xff0c;结果是在 live template 区域设置代码模板的&#xff0c;不知这功能和直播有何关系&#xff0c;live stream&#xff1f; live template 就是自动完成一段代码。比如输入 fori&#xff0c;然后ctrl空格补全循环体&#xf…

Apache-DButils以及Druid(德鲁伊) 多表连接查询的解决方案:两种

Apache-DButils以及Druid(德鲁伊) 多表连接查询的问题 每博一文案 张爱玲说&#xff0c;于千万人之中&#xff0c;遇到你所要遇到的人&#xff0c;于千万年之中&#xff0c;时间的无涯的荒野里&#xff0c;没有 早一步&#xff0c;也没有晚一步&#xff0c;刚巧赶上了。 人生海…

iPhone开机密码什么时候会用到?忘记了怎么办?

iPhone的开机密码也是屏幕解锁密码&#xff0c;它的作用还是很重要的。一般用在&#xff1a; 解锁手机手机重启后解锁手机系统更新后第一次解锁手机手机连接电脑需要信任设备Face ID或指纹解锁失败三次后连接Apple Watch后第一次解锁手机 虽然我们现在经常使用其他的解锁方式&…

马斯克特斯拉内部邮件火了:痛恨开会,少说黑话

金磊 羿阁 发自 凹非寺量子位 | 公众号 QbitAI马斯克给员工的一封内部邮件火了。鼓励员工拒绝开会、公司规定不合理可以不遵守……俨然一个为员工着想的好老板。一开始人们还奇怪马斯克的画风怎么变这么快&#xff0c;后来才发现原来这是他6年前写的。对象也不是推特员工&#…

BCN点击试剂:1516551-46-4,BCN-succinimidylester,BCN NHS

●中文名&#xff1a;丙烷环辛炔-活性酯&#xff0c;BCN-琥珀酰亚胺酯 ●英文名&#xff1a;BCN-NHS&#xff0c; BCN-NHS 酯&#xff0c;BCN-活性酯&#xff0c;BCN-succinimidylester 【产品理化指标】&#xff1a; CAS号&#xff1a; 1516551-46-4 分子式&#xff1a;C15H17…

58 - 类模板的概念和意义

---- 整理自狄泰软件唐佐林老师课程 1. 思考 在C中是否能够将泛型的思想应用于类&#xff1f; 1.1 类模板 一些类主要用于存储和组织数据元素类中数据组织的方式和数据元素的具体类型无关 如&#xff1a;数组类、链表类、Stack类、Queue类&#xff0c;等 C中模板的思想应用于…

【LeetCode】No.103. Binary Tree Zigzag Level Order Traversal -- Java Version

题目链接&#xff1a;https://leetcode.com/problems/binary-tree-zigzag-level-order-traversal/ 1. 题目介绍&#xff08;Binary Tree Zigzag Level Order Traversal&#xff09; Given the root of a binary tree, return the zigzag level order traversal of its nodes’…

【网络编程】第二章 网络套接字(socket+UDP协议程序)

&#x1f3c6;个人主页&#xff1a;企鹅不叫的博客 ​ &#x1f308;专栏 C语言初阶和进阶C项目Leetcode刷题初阶数据结构与算法C初阶和进阶《深入理解计算机操作系统》《高质量C/C编程》Linux ⭐️ 博主码云gitee链接&#xff1a;代码仓库地址 ⚡若有帮助可以【关注点赞收藏】…

html实训大作业《基于HTML+CSS+JavaScript红色文化传媒网站(20页)》

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

群晖修改默认端口为80、443

写之前哔哔两句 我这个人是个有强迫症的人&#xff0c;本来群晖用的好好的&#xff0c;然后觉得为什么还要输入5000、5001端口呢&#xff1f; 然后我就尝试着去修改端口&#xff0c;想修改为40、443的时候&#xff0c;结果提示端口被保留&#xff0c;这我哪能忍&#xff0c;ss…

springboot整合canal

该篇博客是基于前两篇的基础上来实现的&#xff0c;如果没有看过可以看一下前面的步骤 使用docker搭建 MYSQL主从_极速小乌龟的博客-CSDN博客docker 上面搭建mysql主从服务器https://blog.csdn.net/qq_35771266/article/details/128101019?spm1001.2014.3001.5501 ShardingS…