记录一次句柄泄漏排查经历(too many open files)

今天元宵节,新年即将结束,年前由于每天在观察服务器日志和维护,没有出现过什么大问题。但是在过年放假期间,出现了日志过大的情况。平时每天产生1M的日志是正常的,但是年中有一天我查看服务器的时候,发现已经有42G日志了,线上功能也有些不正常,暗自心中一紧,迅速开启了这次排查之旅。

一、 备份服务器异常日志,恢复线上功能
使用linux系统cp命令,将异常日志复制备份。

cp [OPTION]... SOURCE... DIRECTORY

重启服务器进程,功能恢复正常。

二、 查看日志,初步定位

功能恢复以后,开始寻找异常原因。由于异常日志过大,下载很慢,于是再使用split命令分割日志文件,每块大小1G。但是1个G还是很大,我再次对第一个小块再次分割,每块100M。

split -b 100M mylog.txt

最后再下载第一个100M文件进行查看。
最后下载下来的日志显示如下:

java.net.SocketException: Too many open files
at sun.nio.ch.Net.socket0(Native Method)
at sun.nio.ch.Net.socket(Net.java:411)
at sun.nio.ch.DatagramChannelImpl.(DatagramChannelImpl.java:142)
at sun.nio.ch.SelectorProviderImpl.openDatagramChannel(SelectorProviderImpl.java:46)
at java.nio.channels.DatagramChannel.open(DatagramChannel.java:182)
at lbms.plugins.mldht.kad.utils.AddressUtils.getDefaultRoute(AddressUtils.java:254)
at lbms.plugins.mldht.kad.RPCServerManager.startNewServers(RPCServerManager.java:147)
at lbms.plugins.mldht.kad.RPCServerManager.refresh(RPCServerManager.java:51)
at lbms.plugins.mldht.kad.DHT.update(DHT.java:929)
at lbms.plugins.mldht.kad.DHT.lambda$started$11(DHT.java:767)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)

java.net.SocketException: Too many open files
at sun.nio.ch.Net.socket0(Native Method)
at sun.nio.ch.Net.socket(Net.java:411)
at sun.nio.ch.DatagramChannelImpl.(DatagramChannelImpl.java:142)
at sun.nio.ch.SelectorProviderImpl.openDatagramChannel(SelectorProviderImpl.java:46)
at java.nio.channels.DatagramChannel.open(DatagramChannel.java:182)
at lbms.plugins.mldht.kad.utils.AddressUtils.getDefaultRoute(AddressUtils.java:254)
at lbms.plugins.mldht.kad.RPCServerManager.startNewServers(RPCServerManager.java:147)
at lbms.plugins.mldht.kad.RPCServerManager.refresh(RPCServerManager.java:51)
at lbms.plugins.mldht.kad.DHT.update(DHT.java:929)
at lbms.plugins.mldht.kad.DHT.lambda$started$11(DHT.java:767)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
...

如上所示,日志中不断在循环写入java.net.SocketException: Too many open files异常,并且一直未停止。于是到各大IT网站上搜索了相关问题,并尝试了一些方案解决。
大概的描述就是说:
linux系统文件句柄不够,已经被使用完了。
按照网上的方案,使用ulimit -a查询和永久修改了文件句柄配置文件,再次查询数量设置为了65535.
在这里插入图片描述
我想这下应该可以了。

三、 猜想泄漏,印证猜想
然而,事情并非如此。服务器继续运行一周以后,再次出现了这个异常。
于是重新思考了这个异常,可能是句柄泄漏造成的,并根据这个网址提供的思路进行排查。
侦测程序句柄泄露的统计方法
根据提示,在linux系统上编写了监控进程句柄的后台程序进行监控:
记录一次句柄泄漏排查经历(too many open files)_第1张图片
其中下面的命令是查询该进程当前占用的句柄数量。

 ls -l /proc/进程ID/fd |wc -l 

设置每600秒做一次句柄占用统计,等待一天,并用excel表格做出了折现走势图:
记录一次句柄泄漏排查经历(too many open files)_第2张图片
通过该折线图,进一步印证了是句柄泄漏的原因。

四、 定位泄漏位置,解决问题

句柄泄漏肯定是程序引起的。句柄通常是指文件、socket或者其他,句柄泄漏肯定是java中的文件操作流或者socket未关闭导致的。于是再次反复检查每一个设置文件操作的位置,发现并没有未关闭的情况。
将战地转移到本地IDE,使用windows任务进程管理器调试,反复测试每一个业务,并观察句柄和线程使用情况。
记录一次句柄泄漏排查经历(too many open files)_第3张图片
在反复测试每项业务的过程中,各项业务的线程和句柄占用在业务结束后都会释放,只有一项业务,即使结束了,线程和句柄占用数也一直未释放。因此,再次深入测试这项业务,并使用JVM栈命令查询各个线程的使用情况:

	jstack -l 进程ID

在这项业务结束后,我发现大量的线程依然处于RUNNABLE状态,并且在执行接收socket数据包,如下:
记录一次句柄泄漏排查经历(too many open files)_第4张图片
根据这些线程的提示,重新回到代码上,最终发现是在项目的该业务逻辑分支过程中,runtime对象执行超时情况下,并未做关闭处理,导致了许多用户在使用这项业务过程中,即使出现了超时,前端返回了提示,后端也并没有进行关闭回收,一直处于运行状态。

五、 验证泄漏解决
在打好这个补丁后,重新发布线上观察,并画出折线图:
记录一次句柄泄漏排查经历(too many open files)_第5张图片

句柄占用稳定在600以内,本次句柄泄漏排查告一段落。

六、 总结
通过本次经历,重新敬仰代码。遇到问题也不可怕,深入研究,一点一点啃也能解决。
但愿我的经历能给遇到问题的人提供一些排查思路和参考。

七、后续
我以为这个漏洞解决了,然鹅我发现并没有,虽然之前的这个也算是一个漏洞。但是好像
并没有完全解决这个问题。时间过了三个月,最近上线的程序发现问题依然存在。
通过linux命令发现句柄数还是越来越大。

ll /proc/64214/fd |wc -l   64214是进程id

我查看了fd文件夹的内容,大部分是socket文件。这次我开始怀疑是第三方包的问题。我登上Github联系了开发者大佬,并在社区提了这个问题,目前还在等待中。
除此之外,我导出了jstack文件,发现了大量的线程(超过3000)。接下来我准备从线程栈入手,一步一步的找到这个问题的原因。

时间:2021-07-27
这个问题得到了新的进展,由于报错是redis问题,于是问题定位在redis版本上,同时通过查询服务器的连接数情况,发现如下:
记录一次句柄泄漏排查经历(too many open files)_第6张图片
服务器上有大量的连接处于CLOSE_WAIT状态,通过分析TCP协议的状态,查出这是由于对方关闭了连接,但我方未关闭连接造成的。即我方的socket未进行手动close。 问题的原因找到了,接下来就要排查问题的位置了。由于猜测是redis问题,于是我更换了redis的线程池,由lettuce换成了jedis。
经过一段时间后…抛出JedisConnectionException: java.net.SocketTimeoutException: Read timed out异常。
直到此时,我猜想可能并不是由Redis造成的流未关闭。而是第三方包。
带着这个猜想,我取消了所有用redis的地方(也并不多),改用内存储存。观察是否还存在该情况。
经过一段时间后,服务器上依然有大量的连接处于CLOSE_WAIT状态。
于是尘埃落定,可能就是第三方包的问题造成了流未关闭。接下来问题排查的重点将在第三方包上,静待后续。

时间来到2021.10.06

正是国庆节期间,这几天我收到了来自Github大佬的回信。事情结果如下:
今年出现问题以来,我不断的在思考和解决这个问题,但是当前并没有一个很好的办法,只是用了一些临时办法处理。
我在Github上向开发者大佬提出了这个问题,经过反复几次的描述和确认,开发者大佬确认是这个开源代码存在BUG,一些代码在任务完成后并没有关闭任务相关的网络连接,造成了该功能的泄露,大佬给了我一些临时解决方案,并且会在后面解决这个问题。以下是部分截图:
记录一次句柄泄漏排查经历(too many open files)_第7张图片

至此! 这个问题正式收官。
根据大佬的提示我修改了这个jar包,并且服务可以正常运行了,句柄不再泄漏。
关于这个BUG的详情可以参考Github:
[QUESTION] Why do process handles keep increasing. #185

你可能感兴趣的:(问题排查,java,linux)