技术指南

Linux 文件描述符耗尽排障:区分进程、用户与 systemd 限制

从 EMFILE、file-max 与实际句柄快照出发,区分进程、登录会话和 systemd 服务限制,再做单变量调整与可验证回滚。

服务报 `Too many open files` 时,不要先把所有 nofile 数值改成一个更大的数。先判断是单个进程的 `RLIMIT_NOFILE` 达到软限制、内核全局 file-handle 上限,还是应用本身没有关闭连接、文件或监听对象。进程级耗尽通常返回 `EMFILE`;全局 file table 触顶则是另一条证据链。恢复动作必须先保留现场,再只改变承担故障的边界。

先把错误分到正确的限制

观察优先假设下一步
应用记录 `EMFILE` 或 `Too many open files`,只有一个 PID 的 fd 数逼近上限该进程的软 `RLIMIT_NOFILE` 已到边界,或应用泄漏句柄记录 `/proc/<pid>/limits`、实际 fd 数、类型和增长者,再检查服务启动来源
多个不相关服务同时失败,内核日志出现 `file-max limit ... reached`全局 file-handle 上限或宿主资源压力读取 `/proc/sys/fs/file-nr`、`file-max` 与同窗内核日志;不要只提高某个 unit 的 LimitNOFILE
文件描述符够用,但 inotify watch、进程数或连接跟踪耗尽这是另一种 per-user、进程或网络表的限制分别核对 `max_user_watches`、`TasksMax=`、端口/conntrack 与应用错误,不把所有失败归为 nofile
shell 中 `ulimit -n` 很高,systemd 服务仍显示较低值登录 PAM 限制没有传给该服务,或 unit 有自己的限制用服务 PID 的 `/proc/<pid>/limits` 和 `systemctl show` 验证实际生效值

先保存进程、unit 与主机基线

先把 example.service 换成真实 unit;服务可能有多个 worker PID,记录主 PID 只是起点。`/proc` 和路径列表可能包含内部文件名,证据副本应限制权限。
unit=example.service
stamp=$(date --utc --iso-8601=seconds)
printf 'incident_utc=%s\nunit=%s\n' "$stamp" "$unit"
systemctl status "$unit" --no-pager --full
systemctl show "$unit" \
  --property=MainPID,ControlGroup,LimitNOFILE,LimitNOFILESoft,LimitNOFILEHard,User,FragmentPath,DropInPaths
pid=$(systemctl show "$unit" -p MainPID --value)
test "$pid" != 0
cat "/proc/$pid/limits"
printf 'fd_count='; find "/proc/$pid/fd" -mindepth 1 -maxdepth 1 -type l 2>/dev/null | wc -l
cat /proc/sys/fs/file-nr
cat /proc/sys/fs/file-max
cat /proc/sys/fs/nr_open

`RLIMIT_NOFILE` 的 soft limit 是进程当前会被内核执行的值,hard limit 是该进程可提高到的上限;子进程会继承这些限制,`execve` 不会把它们重置。`RLIMIT_NOFILE` 指定的是可分配的最大描述符编号加一,超过它的 `open`、`pipe`、`dup` 等操作会失败并返回 `EMFILE`。不要用 shell 的值代替目标服务 PID 的实际值。

区分进程数量与内核 file table

用实际 PID 替换 1234。计数是瞬时快照,需在同一时间窗重复采样;不要把匿名 inode、socket 和普通文件名称当作业务根因。
pid=1234
printf '[limits]\n'
grep -E 'Max open files' "/proc/$pid/limits"
printf '[fd count]\n'
find "/proc/$pid/fd" -mindepth 1 -maxdepth 1 -type l 2>/dev/null | wc -l
printf '[fd types]\n'
for fd in /proc/$pid/fd/*; do readlink "$fd"; done 2>/dev/null | sed -E 's/^[^[]+//' | sort | uniq -c | sort -nr | head -n 30
printf '[kernel file table]\n'
cat /proc/sys/fs/file-nr
printf '[kernel maximum]\n'
cat /proc/sys/fs/file-max

`/proc/<pid>/fd` 统计的是一个进程当前可见的描述符,`/proc/<pid>/limits` 展示该进程的 soft/hard 上限。`/proc/sys/fs/file-max` 则是内核允许分配的 file handles 上限,`file-nr` 用于观察已分配数量;全局触顶会影响多个进程。`fs.nr_open` 是单进程 hard `RLIMIT_NOFILE` 可提高到的内核上界,不能通过把 unit 写成 `infinity` 绕过。

沿句柄类型找真正的增长者

类型或现象要核对的证据不要先做
大量 TCP socket应用连接池、keep-alive、TIME_WAIT、上游错误与重试速率把连接数上限和 nofile 一起无限提高
大量 regular file 或 deleted file日志轮转、临时目录、打开文件的链接数和持有 unit直接 truncate 数据库、journal 或 `/proc/<pid>/fd`
大量 pipe/eventfd/epollworker/异步库生命周期、任务队列与父子进程关系只增加限制而不查未关闭对象
一个 PID 稳定接近上限应用的并发模型、单连接资源消耗和异常路径把同一数值复制到所有服务
需要足够权限才能看到其他用户的进程和 socket;`lsof` 输出可能包含路径、地址和命令参数。`ss` 结果只是网络对象的一面,不能替代应用连接池和关闭逻辑。
pid=1234
lsof -nP -p "$pid"
lsof -nP -p "$pid" | awk 'NR>1 {print $5}' | sort | uniq -c | sort -nr
ss -Htanp 2>/dev/null | grep "pid=$pid," | head -n 50

如果 fd 数在请求量下降后仍不回落,先找应用或库的关闭路径、连接池回收和异常处理;提高上限只能延后失败。若增长来自受控高并发,容量调整必须同时给出每个连接的内存、CPU、上游和恢复预算。把 `select(2)` 作为兼容性检查:systemd 当前文档提醒,部分程序不能正确处理高于 1023 的描述符;现代应用若使用 `epoll` 等接口才适合更高 soft limit。

system service 用 unit 设定实际边界

在编辑器中创建 drop-in;不要直接改发行版提供的主 unit 文件。
sudo systemctl edit example.service
`LimitNOFILE=` 的 soft:hard 值只是候选配置;数值应来自已记录的峰值、增长余量和应用兼容性,而不是通用模板。
[Service]
LimitNOFILE=8192:65536
verify 只能检查 unit 文件和引用的静态问题;重启前保存旧值、业务窗口和回滚负责人。重启后必须以目标 PID 的 limits 为准。
sudo systemd-analyze verify /etc/systemd/system/example.service.d/override.conf
sudo systemctl daemon-reload
systemctl show example.service --property=LimitNOFILE,LimitNOFILESoft,LimitNOFILEHard
sudo systemctl restart example.service
pid=$(systemctl show example.service -p MainPID --value)
grep -E 'Max open files' "/proc/$pid/limits"

systemd 的 system service 不会因为你在某个交互式 shell 中执行 `ulimit -n` 就得到同样的值。`LimitNOFILE=` 适用于 unit;user service 还受启动 user manager 时已有的 hard limit 约束,通常需要先调整用户会话或承载 user manager 的 `[email protected]`,再重启该 manager。不要同时让 PAM、systemd 和配置管理写入互相覆盖的限制。

登录会话才用 limits.d,先确认 PAM 链路

只有确认目标是登录会话并且该 PAM service 加载 `pam_limits.so` 后,才考虑 limits.d;示例文件不要直接填入未经容量评估的数值。
sudo install -m 0644 /dev/null /etc/security/limits.d/example-nofile.conf
sudoedit /etc/security/limits.d/example-nofile.conf
limits.conf 的 domain/type/item/value 语法;`example` 应换成实际账号或明确的 group。修改后必须新建登录会话,旧 shell 和已运行服务不会自动获得新限制。
example soft nofile 8192
example hard nofile 65536

Linux-PAM 的 `pam_limits.so` 应用的是登录会话限制,配置入口是 `/etc/security/limits.conf` 和 `limits.d`;它不是 systemd system service 的通用启动器。确认 `/etc/pam.d` 对目标登录路径包含 `session required pam_limits.so`,并用新会话的 `ulimit -Sn`/`ulimit -Hn` 和服务 PID 分别验收。不要把 `* hard nofile unlimited` 当成故障修复。

只有证据指向全局上限才看 fs.*

这些读取不会改变系统;先记录发行版、内核版本和 UTC 时间。`file-max` 与 `nr_open` 的作用不同,不要将它们当作单个服务的并发预算。
cat /proc/sys/fs/file-nr
cat /proc/sys/fs/file-max
cat /proc/sys/fs/nr_open
sysctl fs.file-max fs.nr_open

当多个独立服务同时报告 `EMFILE`,或内核明确记录 file-max 到达时,才把全局 file table 纳入容量修复。提高 `fs.file-max` 需要结合宿主内存、句柄对象和未来负载;提高单进程 hard limit 还受 `fs.nr_open` 与权限约束。任何 sysctl 变更都要通过发行版的配置管理持久化,并准备恢复原值的窗口和观察指标。

一次只改一个边界并留下回滚

  1. 先保存 unit/drop-in、服务 PID 的 limits、fd 类型计数、file-nr、file-max、nr_open、版本和业务错误时间窗。
  2. 若根因是泄漏,先修复关闭、连接池或轮转路径;若确实是容量不足,按服务或明确用户设置 soft/hard,并记录计算依据。
  3. systemd drop-in 变更后做静态 verify、daemon-reload、一次受控重启,再用目标 PID 和真实健康检查验收。
  4. 仅当全局证据支持时才调整 fs.file-max;不要为掩盖一个 unit 的泄漏而放大整机边界。

用增量和业务结果完成验收

观察间隔应按真实流量和故障增长速度调整;示例只读取状态,不制造连接或写入业务数据。
unit=example.service
pid=$(systemctl show "$unit" -p MainPID --value)
for sample in 1 2 3; do
  printf '\n[utc=%s sample=%s]\n' "$(date --utc --iso-8601=seconds)" "$sample"
  grep -E 'Max open files' "/proc/$pid/limits"
  printf 'fd_count='; find "/proc/$pid/fd" -mindepth 1 -maxdepth 1 -type l 2>/dev/null | wc -l
  cat /proc/sys/fs/file-nr
  systemctl show "$unit" -p ActiveState -p SubState -p Result -p NRestarts
  sleep 60
done
  • 同一 workload 下 fd 增长率、错误率、连接数和延迟回到预期;没有用一次较大的上限掩盖持续增长。
  • 服务重启后仍加载预期 unit、drop-in、用户和版本,soft/hard 值与记录一致;旧 shell 的 ulimit 不作为服务验收。
  • file-nr 没有逼近 file-max,内核没有新的 file-max 警告,其他服务和宿主管理通道没有受到副作用。
  • 若错误继续增长、内存压力异常或兼容性检查失败,恢复旧 drop-in/sysctl 和应用版本,再按同一窗口复验。

监控句柄而不是只监控一个数字

为关键 unit 记录 fd 使用量与 soft/hard 限制、按类型的 socket/file/epoll 分布、连接池和请求并发、`file-nr`/`file-max`、应用 `EMFILE` 计数及关闭后的回落速度。告警应同时包含增长率和用户影响;高上限本身不是健康指标。升级 systemd、切换 PAM、引入容器或改变异步库后,重新检查限制继承和 `select(2)` 兼容性。

本文命令在 2026-08-19 按 Linux man-pages、Linux-PAM、systemd 与内核文档完成语义和语法审阅,没有在读者的 VPS、生产 unit 或一次性服务中执行。发行版默认值、cgroup/容器命名空间、user manager、非 Linux userland 和应用 I/O 模型会改变可见路径;执行前先确认 `uname -a`、`systemctl --version`、PAM 会话链和实际服务 PID。

返回知识库