在排查文件描述符(FD)泄漏时,以下几类 FD 记录需要第一时间提高警惕:第一类是持续递增的数字编号,例如 1024r、1025r;第二类是 TYPE=REG 且 NAME 中包含 (deleted);第三类是 TYPE=anon_inode 或 pipe,并且数量与线程数呈明显相关;第四类是 TYPE=sock、STATE=ESTABLISHED,但业务代码中又找不到对应连接;第五类是 TYPE=DIR 且指向临时目录;第六类是 NAME 为空,或直接显示为 socket:[xxx]。这些现象往往都是 Linux 文件句柄泄漏、FD 异常增长的重要信号。

lsof -p PID 输出里哪些 FD 行值得立即警惕
正常进程的 FD 列通常集中在 0u、1w、2w、cwd、mem、DEL,以及少量数字句柄(如 15u)。如果在 lsof -p PID 输出中发现下面这些模式,基本就可以怀疑存在文件句柄泄漏或资源未释放问题:
FD列持续出现大量递增数字(如1024r、1025r、1026w…),并且NAME都指向同一类路径(例如/tmp/xxx.log、/var/log/app/*.log),这通常说明同类文件被反复打开却没有及时关闭TYPE=REG+NAME包含(deleted),且数量还在不断增加 → 说明文件已经被unlink删除,但对应句柄仍未关闭,最终会导致磁盘空间无法释放TYPE=anon_inode或pipe的数量与线程数高度相关(例如每增加一个线程就多出 2 个pipe+ 1 个eventpoll)→ 这类情况常见于未退出的 Looper、epoll,或者异步 I/O 初始化后没有正确清理TYPE=sock且STATE=ESTABLISHED,但业务逻辑里找不到对应连接 → 很可能是 HTTP 客户端连接池没有正确close,或者 socket 没有执行shutdownTYPE=DIR指向临时目录(如/tmp/upload_abc)→ 需要重点检查代码里是否调用了Files.list()、new File(dir).listFiles()后没有关闭Stream或DirectoryStreamNAME为空,或者显示为socket:[1234567]→ 这类句柄通常无法仅靠表面信息判断,需结合/proc/PID/fd/下的实际符号链接进一步反查,可使用ls -l /proc/PID/fd/ | grep socket
为什么单次 lsof 看不到泄漏,但 still get “Too many open files”
这是排查 Linux 打开文件过多(Too many open files)时最容易踩坑的地方:lsof 默认展示的只是当前时刻打开的句柄,而很多泄漏场景表现为“瞬时创建 + 长时间持有”,单次快照往往无法反映真实趋势。更重要的是,lsof 自身还可能因为 DNS 解析阻塞,导致输出变慢、失真,甚至漏掉关键信息。
- 排查时必须加上
-n(禁用 DNS 解析)和-P(禁用端口名解析),否则定位文件句柄泄漏的过程中反而可能引入额外干扰 - 单次执行
lsof -p PID | wc -l只能看到一个静态快照,无法判断 fd 数量是否持续上涨。更稳妥的方式是运行watch -n 5 'lsof -n -P -p PID | wc -l',观察 30 秒内句柄数是稳定、波动还是缓慢爬升(如果增长速度 >5 行/秒,通常就不正常) - 同时执行
lsof -n -P -p PID 2>/dev/null | grep -E "^(ja va|python|node)" | head -20,确认新增句柄是否集中在某一种资源上,例如全是.jar、.pyc、socket等 - 如果怀疑是日志轮转导致的旧文件未释放,重点对比
lsof -n -P -p PID | grep "access.log"与ls -la /var/log/nginx/access.log*的结果,确认进程是否还在持续写入已经轮转的旧日志文件
怎么用 /proc/PID/fd 验证 lsof 没显示的句柄
lsof 有时会过滤掉部分内核抽象资源,例如某些 anon_inode,或容器环境里的部分 fd;而 /proc/PID/fd/ 则是内核直接暴露出来的符号链接视图,通常更接近真实状态,也更适合用来验证 lsof 没显示的文件句柄。
- 执行
ls -l /proc/PID/fd/,你通常会看到类似1024 -> socket:[1234567]、1025 -> anon_inode:[eventfd]这样的条目,这些信息对于追踪 fd 泄漏非常关键 - 针对可疑 fd,可使用
readlink /proc/PID/fd/1024获取其原始类型;再配合cat /proc/PID/status | grep -i fdsize查看当前 fd 总数是否已经接近ulimit -n的限制值 - 如果发现大量
socket:[*]或anon_inode:[*],并且数量与线程数、连接数基本对应,那么大概率是代码中没有正确调用close(),或者没有触发资源的自动释放机制(例如 Ja va 中没有使用 try-with-resources) - 需要注意的是,普通用户访问
/proc/PID/fd/必须拥有目标进程的属主权限,否则会提示Permission denied;在这种情况下应使用sudo进行排查
定位到泄漏源头后,怎么确认是不是代码没关资源
仅仅知道问题出在哪一类 fd 上还不够,更关键的是确认它是否真的来自你可控的代码路径。很多文件句柄泄漏并不直接写在业务代码表面,而是隐藏在标准库封装、连接池、异步线程或日志框架内部,例如 Files.list()、ZipInputStream、HTTP 客户端默认连接池,甚至异步刷盘线程都可能成为泄漏源头。
- Ja va 场景下,重点排查
Files.list()、Files.walk()、ZipFile、SocketChannel.open()—— 这些接口返回的都是需要显式关闭的资源,不能仅依赖 GC 自动回收 - Python 场景下,要检查
open()是否与close()成对出现,或者是否使用了with;此外,os.scandir()同样需要close(),subprocess.Popen的stdout/stderr也非常容易漏关 - Node.js 场景下,
fs.createReadStream()、http.request()、net.createConnection()都需要显式调用.destroy(),或正确监听并处理close事件 - 不要只盯着文件路径看——很多 FD 泄漏根本不会直接出现在
NAME列,而是表现为TYPE=sock或TYPE=anon_inode。这时必须继续回溯调用栈,确认是否在循环中反复 new client、初始化连接、创建事件对象,却没有执行 close
socket:[1234567] 或 anon_inode:[eventpoll] 到底是谁创建的、由谁负责关闭、又为什么没有关闭。lsof 和 /proc/PID/fd/ 只是 Linux 文件句柄泄漏排查的入口,最终仍然要结合代码上下文、调用链以及资源生命周期管理规则,才能彻底定位并修复问题。