Linux 服务报 Too many open files,先分清进程上限和系统上限

186 次浏览8 条回复

适用于 Linux 5.4+、systemd 245+。示例单元要替换;读取其他用户进程的 /proc 和 socket 归属通常需要 root 或相应权限。先保留报错原文:EMFILE 是进程自己的描述符用完,ENFILE 才是系统文件表耗尽,两者处理方向不一样。

unit=app.service
pid=$(systemctl show "$unit" -p MainPID --value)
systemctl is-active "$unit"
printf 'pid=%s\n' "$pid"
systemctl show "$unit" -p LimitNOFILE -p LimitNOFILESoft
cat "/proc/$pid/limits"
find "/proc/$pid/fd" -maxdepth 1 -type l -printf . 2>/dev/null | wc -c
cat /proc/sys/fs/file-nr
cat /proc/sys/fs/file-max

MainPID=0 时服务当前没有主进程,别继续读 /proc/0LimitNOFILESoft/proc/$pid/limits 里的 soft limit 更接近进程实际撞到的门槛;file-nr 的三列分别是已分配、已分配但未使用、系统上限,第三列也应和 file-max 对照。

数量接近 soft limit 时,再看主要是哪类描述符。下面按目标做一个粗分组;网络和 Unix socket 的进程信息可能仍需更高权限:

for fd in /proc/$pid/fd/*; do readlink "$fd"; done 2>/dev/null \
  | sed -E 's/^socket:\[[0-9]+\]$/socket/; s/^pipe:\[[0-9]+\]$/pipe/; s/^anon_inode:.*$/anon_inode/' \
  | sort | uniq -c | sort -nr | head -n 20
ss -tanp
ss -xap

socket 持续增加时查连接生命周期和超时,普通文件增加时查未关闭的文件、日志或临时文件,anon_inode 增加则继续定位 epoll、eventfd 等对象。不要只把 LimitNOFILE 调大就结束:修复后应在相同流量窗口连续采样,确认描述符数量会回落或稳定在 soft limit 以下,日志不再出现 EMFILE/ENFILE,并用真实业务请求验证服务正常。

再补一个容易误判的点:MainPID 只代表主进程。prefork 或多 worker 服务里,描述符可能是某个 worker 在涨,主进程的计数却很稳定。cgroup v2(stat -fc %T /sys/fs/cgroup 输出 cgroup2fs)可以把该服务子树里的 PID 都扫一遍:

cg=$(systemctl show "$unit" -p ControlGroup --value)
find "/sys/fs/cgroup$cg" -name cgroup.procs -type f -exec cat {} + 2>/dev/null \
  | sort -nu \
  | while read -r p; do
      used=$(find "/proc/$p/fd" -mindepth 1 -maxdepth 1 -printf . 2>/dev/null | wc -c)
      soft=$(awk '$1=="Max" && $2=="open" && $3=="files" {print $4}' "/proc/$p/limits" 2>/dev/null)
      printf 'pid=%s used=%s soft=%s\n' "$p" "$used" "$soft"
    done

读取别的用户进程仍需要相应权限。连续采样时也要重新取 PID 列表,不然服务重启后旧 /proc/$pid 消失,很容易被误看成描述符已经自行回落。

再补两个容易踩的点:lsof -p PID 的总行数不能直接当作已打开 fd 数,里面还有 cwdrtdtxtmem 等记录。判断是否逼近 RLIMIT_NOFILE,还是以 /proc/PID/fd 的数量为准,lsof 更适合拿来定位对象。

另外,修改 unit 里的 LimitNOFILE= 再执行 daemon-reload,不会改变已经运行的进程;要按变更流程重启服务,再用 systemctl show app.service -p LimitNOFILE -p LimitNOFILESoftcat /proc/$pid/limits 复核实际继承值。若程序或依赖还在用 select(2),也别盲目把上限抬得很高:它通常受 FD_SETSIZE=1024 限制,拿到更大的 fd 反而可能出错。

还可以顺手看一下 fs.nr_open,它和 fs.file-max 不是一回事:前者限制单个进程能设置的 RLIMIT_NOFILE hard limit 上界,后者是系统级文件句柄上限。Linux 5.4+ 上可只读核对:

cat /proc/sys/fs/nr_open
prlimit --pid "$pid" --nofile

应用通过 setrlimit(2)prlimit(2) 把 hard limit 提到 nr_open 以上会失败;所以准备调大 LimitNOFILE= 时,最好把 nr_open、unit 配置和运行中进程的 soft/hard limit 三处一起对照。prlimit 读取其他用户进程同样需要相应权限。

普通文件这一类如果在涨,可以先把已经 unlink、但仍被进程持有的 fd 单独挑出来。日志轮转后程序没重新打开文件时比较常见:

find "/proc/$pid/fd" -maxdepth 1 -type l -lname '* (deleted)' \
  -printf '%f -> %l\n' 2>/dev/null
command -v lsof >/dev/null && lsof -nP -a -p "$pid" +L1

(deleted) 本身不等于泄漏,关键还是连续采样时数量是否增长,以及对应进程是否按预期 reopen。处理后除了复核 fd 数,也可以对照 df 是否回落;不要直接往 /proc/$pid/fd/N 截断,可能破坏应用正在写的数据。读取其他用户进程仍需要相应权限。

anon_inode 这一类建议别一开始全折叠,eventpolleventfdtimerfdinotify 的处理方向不一样。可以先保留原始目标做计数:

for fd in /proc/$pid/fd/*; do readlink "$fd"; done 2>/dev/null \
  | sort | uniq -c | sort -nr | head -n 30

如果主要是 anon_inode:inotify,还要对照 inotify 自己的限制:

cat /proc/sys/fs/inotify/max_user_instances
cat /proc/sys/fs/inotify/max_user_watches
for info in /proc/$pid/fdinfo/*; do
  n=$(grep -c '^inotify ' "$info" 2>/dev/null)
  [ "$n" -gt 0 ] && printf 'fd=%s watches=%s\n' "${info##*/}" "$n"
done

这两个限制按真实用户 ID 统计,不只看单个进程;因此 inotify_init1(2)EMFILE 时,也可能是该用户的 inotify instance 上限,而不是当前进程的 fd 已贴近 RLIMIT_NOFILE。读取其他用户进程仍需要相应权限。

socket 数量上涨时,TIME-WAITCLOSE-WAIT 最好分开看:

ss -Htanp state close-wait
ss -Htan state time-wait | wc -l

TIME-WAIT 通常是连接已经被应用关闭后由内核维护,不再占用该进程的 fd,数量多不等于触发了 RLIMIT_NOFILECLOSE-WAIT 表示对端已关闭、而本端还没完成 close;如果它和 /proc/$pid/fd 里的 socket 数在同一流量窗口持续一起增长,才更值得沿应用连接释放路径查。ss -p 显示进程归属可能需要更高权限,也要避免把瞬时峰值直接当成泄漏。

补一个 file-nr 的读法:在 Linux 2.6+(包括帖子里的 5.4+)内核里,文件句柄按需分配,第二列“已分配但未使用”通常一直是 0。看到 0 不代表系统已经没有余量,也不适合拿第一列减第二列来估算压力。

判断系统级耗尽,还是连续采样第一列是否逼近第三列,并和原始 ENFILE、内核日志放在同一时间窗口对照;单进程的 EMFILE 则继续以 /proc/$pid/limits/proc/$pid/fd 为准。这样能避免把正常的第二列 0 当成故障信号。

还有个监控指标很容易看错:/proc/$pid/status 里的 FDSize 不是当前已打开的 fd 数,而是内核为该进程分配的 fd 表槽位数。它可能按扩容步长跳涨,关闭一批 fd 后也不一定马上缩回,不能拿来判断是否接近 RLIMIT_NOFILE

grep -E '^(Name|Pid|Threads|FDSize):' "/proc/$pid/status"
find "/proc/$pid/fd" -mindepth 1 -maxdepth 1 -printf . 2>/dev/null | wc -c

Linux 5.4+ 上排查和告警仍应使用第二条得到的实际打开数量,并与 /proc/$pid/limits 的 soft limit 在同一采样时刻对照。FDSize 更适合解释 fd 表容量变化,不适合作为使用量。读取其他用户进程仍需要相应权限。