登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

进程句柄数持续上涨:用 procfs 找到未关闭资源

来源:17golang原创

时间:2026-10-07 14:29:07 292浏览 收藏

Linux 进程的句柄数持续上涨,最直接的排查入口不是先安装工具,也不是立刻调高 nofile,而是查看 /proc//fd。这个目录为进程当前维护的文件描述符提供符号链接;再结合 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

如果单进程打开数持续增长,即使距离上限还很远,也应继续查泄漏。提高限制只能延后失败时间;只有确认业务确实需要更高并发句柄且生命周期正常时,调整上限才是容量规划。

进程文件描述符表与 procfs 的 fd、fdinfo、limits 和系统文件句柄统计关系
图1:进程描述符表、procfs 观测入口与系统限制的静态结构说明图,不是运行截图。

连续采样 /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。否则虽然句柄数暂时下降,却可能引入并发使用已关闭资源的问题。

普通文件、删除文件、socket、pipe 和 anon_inode 与资源拥有者及关闭责任的关系
图2:不同 procfs 链接特征与资源拥有者、关闭责任的静态映射说明图,不是运行截图。

修复后做同负载复查

修复后用相同负载、相同采样间隔和相同持续时间重跑计数。理想结果不是句柄数绝对不变,而是达到稳定区间:请求或任务开始时上升,完成后回落,长期基线不再单调增长。还应分别覆盖成功、超时、取消、解析失败、远端断开和服务停止等路径。

  • 数量:/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 用来判断容量余量,而不是掩盖未关闭资源。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>