进程句柄数持续上涨:用 procfs 找到未关闭资源
来源:17golang原创
时间:2026-10-07 14:29:07 292浏览 收藏
Linux 进程的句柄数持续上涨,最直接的排查入口不是先安装工具,也不是立刻调高 nofile,而是查看 /proc/。这个目录为进程当前维护的文件描述符提供符号链接;再结合 fdinfo、limits 和系统级 file-nr,通常可以把增长归到未关闭的文件、网络连接、管道或事件监听器。
官方参考:https://www.kernel.org/doc/html/latest/filesystems/proc.html
排查目标是找到“哪一类描述符稳定累积、链接到什么资源、哪个模块应该关闭它”。单次句柄总数只能说明现象,按目标聚类后的持续增长才接近根因。
先区分进程上限和系统上限
文件描述符是进程访问文件、socket、pipe、eventfd、epoll 等内核对象时使用的整数索引。遇到 Too many open files 时,需要区分三个不同概念:
| 位置 | 含义 | 是否等于当前打开数 |
|---|---|---|
/proc/PID/fd | 目标进程当前可见的文件描述符链接 | 接近当前值,目录读取期间仍可能变化 |
/proc/PID/limits 的 Max open files | 目标进程 RLIMIT_NOFILE 的软、硬限制 | 否,这是上限 |
/proc/sys/fs/file-nr | 系统级已分配文件句柄等统计 | 不是某一个进程的数量 |
/proc/PID/status 的 FDSize | 当前分配的描述符槽位容量 | 否,不能拿来当打开数 |
先读取限制和系统水位,但不要修改它们:
pid=12345 # 查看目标进程的软限制与硬限制。 grep -E '^Max open files' "/proc/$pid/limits" # 查看系统级已分配、空闲和最大文件句柄统计。 cat /proc/sys/fs/file-nr cat /proc/sys/fs/file-max
如果单进程打开数持续增长,即使距离上限还很远,也应继续查泄漏。提高限制只能延后失败时间;只有确认业务确实需要更高并发句柄且生命周期正常时,调整上限才是容量规划。

连续采样 /proc/PID/fd
单次计数无法区分正常波动和持续泄漏。应在负载形态相近的窗口连续采样,例如同一批请求、同一消费速率或同一后台任务周期。下面的脚本每隔 10 秒读取一次目录项数量:
#!/usr/bin/env bash
set -u
pid="${1:?请传入目标 PID}"
interval="${2:-10}"
# 进程退出或权限不足时停止,避免把空目录误记为 0。
while [[ -d "/proc/$pid/fd" ]]; do
if count=$(find "/proc/$pid/fd" -mindepth 1 -maxdepth 1 -printf '.' 2>/dev/null | wc -c); then
printf '%(%F %T)T\tpid=%s\tfd=%s\n' -1 "$pid" "$count"
else
printf '无法读取 /proc/%s/fd,请检查 PID 与权限\n' "$pid" >&2
exit 1
fi
sleep "$interval"
done
procfs 是活动进程的即时视图,打开和关闭可能恰好发生在枚举期间,因此相邻样本出现少量抖动并不异常。更有意义的信号是:相同负载下基线不断抬高,空闲后也不回落,且某一类目标数量同步增加。
长时间监控还要防止 PID 被复用。最简单的做法是同时核对 /proc/PID/exe 和服务管理器记录的主进程身份;若原进程已经退出,就结束这轮采样,不要把新进程拼到旧曲线上。
按符号链接目标聚类
/proc/PID/fd/N 是符号链接。普通文件通常指向路径,已删除但仍被进程持有的文件会带 (deleted),网络连接显示为 socket:[inode],管道显示为 pipe:[inode],epoll、eventfd、inotify 等常以 anon_inode: 开头。
pid=12345
snapshot="fd-targets-${pid}.tsv"
# 保存“描述符编号 + 链接目标”,读取失败的瞬时项直接跳过。
: > "$snapshot"
for fd in "/proc/$pid/fd/"*; do
[[ -e "$fd" || -L "$fd" ]] || continue
target=$(readlink "$fd" 2>/dev/null) || continue
printf '%s\t%s\n' "${fd##*/}" "$target" >> "$snapshot"
done
# 按资源大类汇总,先看哪一类随时间增长。
awk -F '\t' '
$2 ~ / \(deleted\)$/ {count["deleted"]++; next}
$2 ~ /^socket:\[/ {count["socket"]++; next}
$2 ~ /^pipe:\[/ {count["pipe"]++; next}
$2 ~ /^anon_inode:/ {count["anon_inode"]++; next}
{count["file_or_other"]++}
END {for (kind in count) print kind, count[kind]}
' "$snapshot" | sort
接着对连续两到三个快照做同样分类。如果只有 socket 数增长,就把注意力放在连接池、HTTP 响应体、失败重试与超时取消;如果 (deleted) 文件增长,优先检查日志轮转后旧文件是否仍被持有;如果 pipe 增长,检查子进程标准流和管道端点;如果 anon_inode 增长,则查看 epoll、eventfd、timerfd、inotify 等对象的创建和销毁。
用 fdinfo 缩小具体资源
链接目标只给出大类。/proc/PID/fdinfo/FD 至少可提供打开偏移 pos、八进制打开标志 flags、挂载 ID mnt_id 与 inode ino;某些对象还会暴露该类型专有字段。可以先挑选增长簇中的几个 FD 查看:
pid=12345 fd=57 # 先确认链接目标,再读取该描述符的内核元数据。 readlink "/proc/$pid/fd/$fd" cat "/proc/$pid/fdinfo/$fd"
排查时可把“目标路径 + inode + flags”作为同一资源簇的特征。大量描述符指向同一路径但 inode 相同,通常说明重复打开没有关闭;大量不同 socket inode 则更像连接生命周期问题。由于 FD 编号可被进程重复利用,不要只盯着数字 57 本身,应比较它指向的目标和元数据。
把资源簇映射回关闭责任
procfs 能告诉你“进程持有什么”,不能直接指出哪一行代码忘了关闭。下一步是把资源类型映射到明确的生命周期负责人:
| 增长特征 | 常见责任边界 | 优先检查 |
|---|---|---|
| 普通文件路径重复 | 文件读取、导出、上传、日志模块 | 成功、失败和提前返回路径是否都执行 close |
| 路径带 (deleted) | 日志轮转、临时文件、部署替换 | 旧文件是否仍被长寿命进程持有 |
| socket:[inode] 持续增加 | HTTP 客户端、数据库、消息队列、连接池 | 响应体、连接、超时与重试是否完整收尾 |
| pipe:[inode] 持续增加 | 子进程、流式处理、进程间通信 | 父子两端是否在所有退出路径关闭 |
| anon_inode 持续增加 | 事件循环、监听器、定时器 | watcher、epoll、eventfd 的注销与关闭 |
如果服务由多个工作线程共享同一资源管理器,关闭责任应归到拥有生命周期的组件,而不是在任意调用点补一个 close。否则虽然句柄数暂时下降,却可能引入并发使用已关闭资源的问题。

修复后做同负载复查
修复后用相同负载、相同采样间隔和相同持续时间重跑计数。理想结果不是句柄数绝对不变,而是达到稳定区间:请求或任务开始时上升,完成后回落,长期基线不再单调增长。还应分别覆盖成功、超时、取消、解析失败、远端断开和服务停止等路径。
- 数量:
/proc/PID/fd的基线是否停止抬高; - 类型:原先增长的资源大类是否不再累积;
- 目标:相同路径、socket 或 anon_inode 簇是否回落;
- 限制:稳定峰值是否与 Max open files 保留足够余量;
- 权限:监控账号是否只获得读取所需 procfs 信息的最小权限。
常见问题
为什么 lsof 数量和 /proc/PID/fd 不完全一致?
两者读取活动进程时都可能遇到并发打开和关闭,工具的分类、权限和统计口径也可能不同。排查趋势时固定一种方法更重要;本文以 procfs 目录项和链接目标为统一口径。
看到很多 socket 就能认定泄漏吗?
不能。高并发连接池本来就可能长期保持大量 socket。需要结合相同负载下的基线、空闲后的回落、socket 状态和应用生命周期判断,持续增加且不复用才更可疑。
删除日志文件后磁盘空间为什么没有释放?
如果进程仍持有已删除文件的描述符,目录项消失并不意味着对象立即释放。/proc/PID/fd 中带 (deleted) 的链接可帮助发现这种持有;正确做法是让日志组件关闭并重新打开目标文件,而不是直接终止无关进程。
容器里为什么看不到宿主机进程的 fd?
PID 命名空间、procfs 挂载方式和访问权限会限制可见范围。应在与目标进程相同的 PID 命名空间内观察,或通过受控的宿主机诊断通道读取,不要为了排查把容器长期改成过度特权模式。
小结
句柄泄漏的有效排查顺序是:先确认 /proc/PID/fd 的基线持续上涨,再按符号链接目标聚类,随后用 fdinfo 的 inode、flags、mnt_id 等元数据缩小资源簇,最后把资源簇映射到真正拥有生命周期的模块。limits 与 file-max 用来判断容量余量,而不是掩盖未关闭资源。
-
426 收藏
-
387 收藏
-
242 收藏
-
238 收藏
-
402 收藏
-
326 收藏
-
189 收藏
-
373 收藏
-
文章 · linux | 9小时前 | linux运维 · 故障排查 · 服务管理 · systemctl journalctl RestartSec StartLimitBurst Restart systemd 服务432 收藏
-
150 收藏
-
116 收藏
-
258 收藏
-
268 收藏
-
294 收藏
-
483 收藏
-
455 收藏
-
181 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习