1 次浏览0 条回复

适用于 Linux 服务报 Too many open files、新连接失败或无法打开文件的场景。命令基线为 Linux 5.4+、systemd 245+、procps-ng 3.3+;prlimit 来自 util-linux,lsofstrace 为可选工具。读取其他用户进程的 /proc、套接字归属和跟踪系统调用通常需要 root。示例单元 app.service 与 PID 必须替换为实际值。先保留故障窗口,不要一开始就放大限制或重启服务。

1. 先保留错误原文并区分 EMFILE 与 ENFILE

date -u
uname -r
systemctl --version | head -n 1
systemctl status app.service --no-pager -l
journalctl -u app.service --since '-15 min' --no-pager -o short-iso
journalctl -k --since '-15 min' --no-pager -o short-iso \
  | grep -Ei 'too many open files|file-max|ENFILE|EMFILE'

EMFILE 表示调用进程触及自己的文件描述符上限,ENFILE 表示系统范围的打开文件表耗尽;日志里的通用文案可能没有保留 errno,不能仅凭一句 Too many open files 判断是哪一层。若应用日志不显示 errno,可在获准的短窗口内用运行时诊断或 strace 观察失败调用;附加跟踪会影响时序,不应长时间留在线上。

文件描述符不只代表普通文件,也包括套接字、管道、epoll、inotify、eventfd 等内核对象。因此磁盘空间正常不能排除描述符耗尽。

2. 固定实际进程与生效限制

PID=$(systemctl show app.service -p MainPID --value)
printf 'PID=%s\n' "$PID"
ps -o pid,ppid,user,lstart,etime,cmd -p "$PID"
cat "/proc/$PID/limits"
prlimit --pid "$PID" --nofile
systemctl show app.service \
  -p LimitNOFILE -p LimitNOFILESoft -p MainPID -p ControlGroup
systemctl show app.service -p FragmentPath -p DropInPaths
systemctl cat app.service

/proc/PID/limitsprlimit 显示的运行中进程值为准。shell 中的 ulimit -n 只说明当前 shell,不代表 systemd 已启动服务的限制。单元文件里写了 LimitNOFILE= 也不等于当前进程已经采用该值:服务可能尚未重启,或实际工作进程由其他管理器、容器入口或包装脚本创建。

还要确认 PID 是否在故障期间发生过变化;拿新进程的低占用解释旧进程的失败会丢失关键证据。多 worker 服务的限制按进程生效,应分别检查工作进程,不能只看主进程。

3. 统计占用并判断是否持续增长

单次计数可先建立上限占用比例:

find "/proc/$PID/fd" -mindepth 1 -maxdepth 1 -printf '.' | wc -c
cat "/proc/$PID/limits" | grep -F 'Max open files'

/proc/PID/fd 在程序运行时不断变化,计数只能视为采样值。为了识别泄漏,应在不跨越进程重启的前提下按固定间隔采样,并与请求量、连接数和错误时间对齐:

while kill -0 "$PID" 2>/dev/null; do
  printf '%s ' "$(date -u +%FT%TZ)"
  find "/proc/$PID/fd" -mindepth 1 -maxdepth 1 -printf '.' | wc -c
  sleep 10
done

连续上升且负载回落后不下降,才更像泄漏;高并发时短暂升高并随后回落可能是正常峰值。采样间隔与持续时间应覆盖原告警周期,避免用两三个点拟合趋势。

4. 找出主要描述符类型与归属

lsof 可做只读快照:

sudo lsof -nP -p "$PID"
sudo lsof -nP -p "$PID" | awk 'NR > 1 {print $5}' | sort | uniq -c | sort -nr
ss -tanp
ss -uanp

第二条按 lsof 的 TYPE 列做粗略聚合,执行前先确认本机输出列格式;不要把它当成跨版本稳定接口。重点区分大量 REGFIFOIPv4/IPv6anon_inode,并结合应用行为继续定位:

  • 大量处于相同远端或相同状态的 socket:检查连接池上限、连接关闭路径、超时与重试放大。
  • 大量指向同类路径的普通文件:检查文件轮转、异常分支与打开后未关闭。
  • 大量 pipeeventpollinotify:检查子进程、事件循环或目录监听器是否被重复创建。
  • (deleted) 的文件:说明路径已删除但仍被进程持有;它会占用描述符,也可能继续占用磁盘块。

lsof 本身是瞬时视图,而且遍历大型进程可能有开销。需要精确归因时,应优先使用应用运行时自带的连接池、句柄或资源诊断指标,并只在受控窗口补充系统调用跟踪。

5. 检查系统范围容量

sysctl fs.file-max
cat /proc/sys/fs/file-nr

/proc/sys/fs/file-nr 第一列是已分配文件句柄数,第三列是系统上限;第二列的具体表现与内核实现有关,不应单独作为告警依据。第一列接近第三列且多个无关进程同时失败,才支持系统范围耗尽分支。若只有一个服务接近自身 soft limit,而系统仍有充足余量,应回到该进程的限制和占用来源。

不要把系统范围句柄数与某个进程 /proc/PID/fd 的数量直接相加或要求一一对应;内核对象共享与计数口径不同。

6. 修复与闭环验证

若确认是泄漏,应先修复创建与释放不对称、连接池无界或重试堆积。提高 LimitNOFILE= 只能增加缓冲时间,还会放大服务对内存、端口和下游连接的占用;调整前应估算峰值并检查依赖容量。若确需修改 systemd 单元,先确认配置来源,使用 drop-in 保存变更,并在维护窗口执行 daemon-reload 与受控重启。

变更后至少同时验证:

PID=$(systemctl show app.service -p MainPID --value)
prlimit --pid "$PID" --nofile
find "/proc/$PID/fd" -mindepth 1 -maxdepth 1 -printf '.' | wc -c
cat /proc/sys/fs/file-nr
journalctl -u app.service --since '-10 min' --no-pager -o short-iso

闭环标准不是“服务能启动”,而是运行中进程的限制符合预期、描述符数量在代表性负载下达到稳定平台并保留余量、原入口不再出现打开文件或建连失败,且系统范围句柄数没有持续逼近上限。