记一次奇怪的文件句柄泄露问题

记录并分享一下最近工作中遇到的 Too many open files 异常的解决过程。

问题背景

产品有个上传压缩包并导入配置信息到数据库中的功能,主要流程如下:

  1. 用户上传压缩包;
  2. 后端解压存放在临时目录,并返回列表给用户;
  3. 用户选择需要导入哪些信息;
  4. 后端按需插入数据库中,完成后删除临时目录。

这个功能上线两三年了,一直没出现问题,最近测试在功能回归时,导入的时候出现Too many open files异常。

但是通过lsof -p pid | wc -l查看当前进程打开的句柄数时,又是正常的。

Too many open files是Linux系统中常见的错误,字面意思就是说打开了太多的文件,超过了系统的限制。

这里的文件(file)更准确的意思是文件句柄,或者是文件描述符。可以说,Linux系统里的一切都是文件,包括网络连接、端口等等。
lsof -p pid命令可以查看指定进程当前打开的文件信息。wc -l命令指按行统计。

问题分析

当时的第一反应是该系统的文件句柄数配置的太低了,因为在其他环境都是正常的。

通过ulimit -a命令,可以查看当前系统对各种资源的限制。

shell 复制代码
 [uyong@linuxtest ~]# ulimit -a
 core file size          (blocks, -c) 0
 data seg size           (kbytes, -d) unlimited
 scheduling priority             (-e) 0
 file size               (blocks, -f) unlimited
 pending signals                 (-i) 31767
 max locked memory       (kbytes, -l) 64
 max memory size         (kbytes, -m) unlimited
 open files                      (-n) 4096
 pipe size            (512 bytes, -p) 8
 POSIX message queues     (bytes, -q) 819200
 real-time priority              (-r) 0
 stack size              (kbytes, -s) 8192
 cpu time               (seconds, -t) unlimited
 max user processes              (-u) 31767
 virtual memory          (kbytes, -v) unlimited
 file locks                      (-x) unlimited

open files 那行就是表示打开的文件数限制,即一个进程最多可以同时打开的文件数。

也可以通过ulimit -n直接查看最大打开文件数量。

当时查出来的配置是4096,查看其他没问题的环境上的配置,数量都是远远大于这个数。而且系统重新启动后,没做任何操作时,通过lsof -p pid | wc -l查看文件占用,只有100多个。在好的环境上执行导入成功后,再次查看,文件占用数不变。在有问题的环境上导入失败后,查看文件占用数也是不变。

虽然当时的压缩包文件很大,但4096按说也够的。有点奇怪,为了不影响测试进度,只能先临时修改系统配置,增大文件数限制,ulimit -n 65535命令执行后,再次导入没问题了。

命令ulimit -n 65536只能临时临时调整文件数量,系统重启后配置就会失效。

如果要永久生效,需要在配置文件/etc/security/limits.conf里增加如下两行:

复制代码
 * soft nofile 65536
 * hard nofile 65535

问题到此就结束了吗,NO😒,治标不治本,而且测试还顺手提了个遗留缺陷🐛

问题解决

出现这个问题,不用怀疑,肯定就是打开的文件太多了,又没有及时释放,文件越多,占用的句柄就越多。

撸了一遍整个流程的代码,有两个接口:

一个接口用于上传压缩包,解压后,返回资源列表,伪码如下:

java 复制代码
 public List<Item> upload(MultipartFile zipFile) {
     try {
         // 解压压缩包到临时目录
         unzip(zipFile, tmpDir);
         // 搜集所有的资源返回给前端
         return collectItems(tmpDir);
     } catch (Exception e) {
         // 只要发生了异常,就把临时目录清理了
         clear(tmpDir);
     }
 }

一个接口用于处理用户选择的资源并导入到数据库,伪码如下:

java 复制代码
 public void importDb(List<Item> selected) {
     try {
         // 读取文件并插入数据库
         readFilesAndImportDb(tmpDir, selected);
     } finally {
         // 处理完成后,不管失败与否,都把临时目录清理了
         clear(tmpDir);
     }
 }

不管哪个接口,都在最后清理了资源,也就解释了不管导入成功还是失败,查看文件占用情况都是正常的。

逐个方法排查后,最终确定是unzip方法有问题,这里贴一下解压代码,看看你有没有发现问题所在👀

java 复制代码
 import java.io.*;
 import java.util.*;
 import org.apache.commons.io.IOUtils;
 ​
 /**
 * @param in zip输入流
 * @param outDir 解压后文件存在目录
 */
 public void unzip(InputStream in, String outDir) throw IOException {
     try (ZipInputStream zipIn = new ZipInputStream(new BufferedInputStream(in))) {
         ZipEntry zipEntry;
         while((zipEntry = zipIn.getNextEntry()) != null) {
             File outItemFile = new File(outDir, zipEntry.getName);
             if (zipEntry.isDirectory()) {
                 outItemFile.mkdirs();
             } else {
                 outItemFile.getParentFile().mkdirs();
                 outItemFile.createNewFile();
                 IOUtils.copy(zipIn, new FileOutputStream(outItemFile));
             }
         }
     }
 }

上述这段代码就会导致,压缩包里的文件越多,所占用的文件句柄就越多。

最终,只要再加一行代码就解决问题了:

java 复制代码
 try (FileOutputStream fos = new FileOutputStream(outItemFile)) {
     IOUtils.copy(zipIn, fos);
 }

根本原因就是apache的IOUtils.copy方法并不会主动关闭输出流。

总结

  1. 常见的工具方法,能用现成的就用现成的,轮子可以自己慢慢刨析,私下里学习研究重造;
  2. 要了解所使用的第三方工具方法,它们会不会影响入参的状态,比如这里的copy方法,会不会主动关闭输入输出流。

参考资料

  1. Linux下Too many open files问题排查与解决
  2. ulimit命令详解:如何设置和查看系统资源限制
相关推荐
未若君雅裁20 分钟前
MyBatis 一级缓存、二级缓存与清理机制
java·缓存·mybatis
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第65题】【JVM篇】第25题:谈谈对 OOM 的认识
java·开发语言·jvm
阿维的博客日记1 小时前
Nacos 为什么能让配置动态生效?(涉及 @RefreshScope 注解)
java·spring
雨辰AI1 小时前
SpringBoot3 + 人大金仓读写分离 + 分库分表 + 集群高可用 全栈实战
java·数据库·mysql·政务
辰海Coding3 小时前
MiniSpring框架学习-完成的 IoC 容器
java·spring boot·学习·架构
小小编程路3 小时前
C++ 多线程与并发
java·jvm·c++
AI视觉网奇3 小时前
linux 检索库 判断库是否支持
java·linux·服务器
她的男孩3 小时前
从零搭一个企业后台,为什么我把能力拆成 Starter 和 Plugin
java·后端·架构
RainCity3 小时前
Java Swing 自定义组件库分享(七)
java·笔记·后端
Sam_Deep_Thinking3 小时前
连锁门店的外卖订单平台对接
java·微服务·架构·系统架构