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

记录并分享一下最近工作中遇到的 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按说也够的。有点奇怪,为了不影响测试进度,只能先临时修改系统配置,增大文件数限制,ulimt -n 65535命令执行后,再次导入没问题了。

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

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

shell 复制代码
* 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命令详解:如何设置和查看系统资源限制
相关推荐
重生之我要进大厂12 分钟前
LeetCode 876
java·开发语言·数据结构·算法·leetcode
_祝你今天愉快15 分钟前
技术成神之路:设计模式(十四)享元模式
java·设计模式
小筱在线1 小时前
SpringCloud微服务实现服务熔断的实践指南
java·spring cloud·微服务
luoluoal1 小时前
java项目之基于Spring Boot智能无人仓库管理源码(springboot+vue)
java·vue.js·spring boot
ChinaRainbowSea1 小时前
十三,Spring Boot 中注入 Servlet,Filter,Listener
java·spring boot·spring·servlet·web
小游鱼KF1 小时前
Spring学习前置知识
java·学习·spring
扎克begod1 小时前
JAVA并发编程系列(9)CyclicBarrier循环屏障原理分析
java·开发语言·python
青灯文案11 小时前
SpringBoot 项目统一 API 响应结果封装示例
java·spring boot·后端
我就是程序猿1 小时前
tomcat的配置
java·tomcat
阳光阿盖尔1 小时前
EasyExcel的基本使用——Java导入Excel数据
java·开发语言·excel