Skip to content
Published on

Too many open files 彻底解决 — 为什么调高 ulimit 也没用

分享
Authors

开篇 — accept4() failed (24: Too many open files)

症状是这样出现的。

2026/07/25 14:02:11 [alert] 14022#14022: accept4() failed (24: Too many open files)
2026/07/25 14:02:11 [alert] 14022#14022: accept4() failed (24: Too many open files)

如果是 Java,则是这样。

java.net.SocketException: Too many open files
	at java.base/sun.nio.ch.Net.accept(Native Method)
Caused by: java.io.IOException: Too many open files

到这一步几乎所有人都会做同一件事。执行 ulimit -n 65536,重启服务,然后看到一模一样的报错。原因在于上限不止一个而是三个,而且你在 shell 里改的值根本不会应用到服务上

errno 24 就是 EMFILE。文字必须读准确。

errno 24 23
# EMFILE 24 Too many open files
# ENFILE 23 Too many open files in system

带上 in system 就是另一个问题了。仅这一处差别就把诊断分掉了一半。

上限不止一个,而是三个

三者各自独立存在,只要撞上其中任何一个就打不开文件。

上限范围超限时的 errno确认方式设置位置
RLIMIT_NOFILE soft单个进程EMFILE (24)/proc/PID/limitsulimit -n、limits.conf、systemd LimitNOFILE
RLIMIT_NOFILE hard单个进程设置被拒绝/proc/PID/limits同上,非特权只能调低
fs.nr_open内核全局EPERM/EINVALsysctl fs.nr_open/etc/sysctl.d/
fs.file-max整个系统ENFILE (23)cat /proc/sys/fs/file-nr/etc/sysctl.d/

把它们的关系理一理是这样。

fs.file-max   : 整个系统同时能打开的文件数量 (所有进程之和)
   └─ fs.nr_open : 单个进程的 RLIMIT_NOFILE 无法越过的天花板
        └─ RLIMIT_NOFILE hard : 管理员为该进程设定的上限
             └─ RLIMIT_NOFILE soft : 实际生效的值。进程可以自行把它提升到 hard

一次性看清当前状态。

sysctl fs.file-max fs.nr_open
# fs.file-max = 6553600
# fs.nr_open = 1048576

cat /proc/sys/fs/file-nr
# 13984	0	6553600

file-nr 的三个数字依次是已分配的文件句柄数、已分配但未使用的数量 (在较新的内核上恒为 0)、以及最大值。第一个接近第三个就是系统全局的问题,此时冒出来的就是 ENFILE。上面的例子里是 13984 对 655 万,余量压倒性地充足。在大多数现代 Linux 上 fs.file-max 并不是瓶颈。因为内核会按内存大小成比例自动计算它。即便如此,很多调优文档还是让你先去调这个值。调高无害,但症状纹丝不动。

fs.nr_open 不一样。超过默认值 1048576 的 ulimit -n,设置动作本身就会失败。

ulimit -n 2000000
# bash: ulimit: open files: cannot modify limit: Operation not permitted

sudo sysctl -w fs.nr_open=2097152
# fs.nr_open = 2097152
ulimit -n 2000000   # 这次就成功了

确认真正生效的值 — 不是 ulimit,而是 /proc/PID/limits

这里是核心。ulimit -n 显示的是你此刻所在的这个 shell的值。不是出问题那个进程的值。两者经常对不上。

ulimit -n
# 1048576

pgrep -x nginx
# 14022
# 14025

sudo grep 'Max open files' /proc/14025/limits
# Max open files            1024                 524288               files

shell 是一百万,而 nginx worker 的软限制是 1024。这个落差正是问题的真身。今后再碰到这个症状,ulimit -n 干脆别看,先跑下面这条命令。

# 按进程名一次性查看
for pid in $(pgrep -x nginx); do
  printf '%6s  %s\n' "$pid" "$(awk '/Max open files/ {print $4, $5}' /proc/"$pid"/limits)"
done
#  14022  1024 524288
#  14025  1024 524288

顺便看看现在用掉了多少个。

# 实际打开的 fd 数量 (最准确)
sudo ls /proc/14025/fd | wc -l
# 1021

# 用一行输出使用率
for pid in $(pgrep -x nginx); do
  used=$(sudo ls /proc/"$pid"/fd 2>/dev/null | wc -l)
  soft=$(awk '/Max open files/ {print $4}' /proc/"$pid"/limits)
  printf '%6s  %6s / %-8s (%d%%)\n' "$pid" "$used" "$soft" $((used * 100 / soft))
done
#  14022    1021 / 1024     (99%)
#  14025    1019 / 1024     (99%)

lsof -p PID | wc -l 得到的数字会更大。lsof 把当前工作目录、根目录、可执行文件、内存映射的库也一并数了进去,而这些并不计入 RLIMIT_NOFILE。要和上限做比较时,请使用 /proc/PID/fd 的数量。

systemd 为什么忽略 ulimit 设置

这是最常见的坑。如果你曾经加上下面这份配置、重启之后却什么都没变,本节就是答案。

cat /etc/security/limits.d/99-nofile.conf
# *    soft    nofile    1048576
# *    hard    nofile    1048576

这个文件由 PAM 模块 pam_limits 读取。而 pam_limits 只在登录会话中起作用。SSH 登录、sulogin 属于这一类。systemd 在开机时拉起的系统服务不是登录会话,所以它根本不会读这个文件。

决定服务上限的是 systemd。优先级依次是单元里的 LimitNOFILE,没有就取 /etc/systemd/system.conf 里的 DefaultLimitNOFILE,还没有就用 systemd 的内置默认值。

# 应用到单元上的值 (硬限制和软限制分开暴露)
systemctl show -p LimitNOFILE -p LimitNOFILESoft nginx.service
# LimitNOFILE=524288
# LimitNOFILESoft=1024

# 全局默认值
systemctl show -p DefaultLimitNOFILE -p DefaultLimitNOFILESoft
# DefaultLimitNOFILE=524288
# DefaultLimitNOFILESoft=1024

这里有必要了解 systemd 240 之后的默认行为。systemd 把硬限制放得很大,设为 524288,却把软限制保持在 1024 这样的低水平。理由是兼容性。select() 处理不了 1024 及以上的 fd 编号,而一部分老程序启动时会跑一个从 0 到软限制逐个关闭所有 fd 的循环,如果那个上限是一百万,启动就要花上好几秒。所以 systemd 的设计意图是"需要的程序自己调高了再用"。

实际上相当多的现代运行时就是这么做的。Go 的运行时会在启动时把软限制抬到硬限制,于是同一台机器上会出现 Go 服务安然无恙、只有其他服务报 EMFILE 的局面。

正确的设置方式是这样。

sudo systemctl edit nginx.service
# 在打开的编辑器里写入下面的内容
# [Service]
# LimitNOFILE=65536

sudo systemctl daemon-reload
sudo systemctl restart nginx

有两点要强调。systemctl reload 改不了 rlimit。这个值是在进程创建时刻生效的,所以必须是 restart。另外像 LimitNOFILE=65536 这样只写一个值时,软限制和硬限制都会变成那个值。想分别指定,就用 LimitNOFILE=65536:524288 的写法。

确认已经生效。

sudo grep 'Max open files' /proc/$(pgrep -x nginx | head -1)/limits
# Max open files            65536                65536                files

有时候应用自身还另有一份设置。nginx 单独有一个叫 worker_rlimit_nofile 的指令,而这个值无法超过 systemd 给它的硬限制。

# /etc/nginx/nginx.conf
worker_rlimit_nofile 65536;
events {
    worker_connections 32768;
}

worker_connectionsworker_rlimit_nofile 大是没有意义的。一个连接可能要用掉一个客户端套接字和一个上游套接字,所以如果是反向代理,把 worker_rlimit_nofile 设成 worker_connections 的两倍以上才稳妥。

也有不重启就先灭火的办法。

sudo prlimit --pid 14025 --nofile=65536:65536
sudo prlimit --pid 14025 --nofile
# RESOURCE DESCRIPTION                             SOFT  HARD UNITS
# NOFILE   max number of open files               65536 65536 files

prlimit 修改的是运行中进程的上限。不过如果应用在启动时读取上限并据此确定了内部数据结构的大小 (nginx 的连接池就是如此),它可能不起作用。真正的解决办法是修改单元文件并重启。

容器的规则又不一样。容器内进程的 RLIMIT_NOFILE 来自 OCI 运行时规范的 process.rlimits,而这个值由容器运行时的守护进程决定。shell 的 ulimit 也好,宿主机的 limits.conf 也好,都插不上手。

# 在容器内部确认是唯一可靠的方法
docker exec api-server cat /proc/1/limits | grep 'Max open files'
# Max open files            1048576              1048576              files

# 按容器单独指定
docker run --ulimit nofile=65536:65536 myimage

# 守护进程的全局默认值
cat /etc/docker/daemon.json
# {
#   "default-ulimits": { "nofile": { "Name": "nofile", "Soft": 65536, "Hard": 65536 } }
# }

Kubernetes 的 Pod 规格里没有 ulimit 字段。要按 Pod 粒度调整,只能在容器入口点用 ulimit -n 调低 (只能降到硬限制以内)、改节点上的 containerd 配置,或者调整容器运行时单元的 LimitNOFILE。很多发行版的运行时单元是 LimitNOFILE=infinity,这种情况下容器会继承到一个非常大的值。看上去很宽裕很美好,但对于前面说的那种跑"关闭全部 fd"循环的程序,它就成了启动要花几十秒的原因。

到底是什么在吃 fd — 按套接字、文件、管道细分

在调高上限之前,得先看清是什么占着它。/proc/PID/fd 里符号链接的目标会告诉你种类。

sudo ls -l /proc/14025/fd | awk '{print $NF}' \
  | sed -E 's#^(socket|pipe|anon_inode):.*#\1#; s#^/.*#regular-file#' \
  | sort | uniq -c | sort -rn
#   61204 socket
#    3891 regular-file
#     412 anon_inode
#       8 pipe
#       3 /dev/null

套接字有六万个,那就是网络这一侧的问题。继续深挖是什么样的套接字。

# 按状态统计套接字数量
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
#   38210 ESTAB
#   21044 CLOSE-WAIT
#    2118 TIME-WAIT
#     412 LISTEN

CLOSE-WAIT 有两万个。这就是 fd 泄漏的决定性证据。CLOSE-WAIT 是指对端发了 FIN 关闭了连接,而我们这一侧的应用没有调用 close() 的状态。内核会一直等应用来关,无限期地停在这个状态。没有超时。不要和 TIME-WAIT 混淆。TIME-WAIT 由内核自行清理,也不消耗 fd。

看清是和哪个对端的连接,就能把出问题的代码范围收窄。

sudo ss -tanp state close-wait | head -5
# Recv-Q Send-Q   Local Address:Port      Peer Address:Port  Process
# 1      0        10.0.3.14:44120        10.0.9.31:6379     users:(("java",pid=21874,fd=1042))
# 1      0        10.0.3.14:44122        10.0.9.31:6379     users:(("java",pid=21874,fd=1043))

# 按对端地址聚合
sudo ss -tan state close-wait | awk 'NR>1 {print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn
#   20981 10.0.9.31
#      63 10.0.9.42

全都是同一个对端 (Redis)。这说明 Redis 客户端连接池没有归还连接,代码评审的目标到这里就定下来了。

如果凶手在文件这一侧,就数一数是哪些文件。

sudo ls -l /proc/21874/fd | awk '$NF ~ /^\// {print $NF}' \
  | sed -E 's#/[^/]+$##' | sort | uniq -c | sort -rn | head -5
#    3204 /var/lib/app/uploads/tmp
#     512 /data/kafka/logs/orders-0
#      88 /usr/lib/x86_64-linux-gnu

/var/lib/app/uploads/tmp 下面开着 3204 个。这说明有一条代码路径创建了临时文件却不关闭。

inotify 与 epoll 是另一套独立上限

anon_inode 这一类也会消耗 RLIMIT_NOFILE,但它们各自还有独立的上限。所以才会出现 fd 明明还很宽裕却依然失败的情况。

inotify 的上限有三个。

sysctl fs.inotify
# fs.inotify.max_queued_events = 16384
# fs.inotify.max_user_instances = 128
# fs.inotify.max_user_watches = 65536

max_user_instances 是每个用户能创建的 inotify 实例 (inotify_init) 数量,超限就是 EMFILE。ulimit -n 报出的错误文字完全一样,但原因不同max_user_watches 是每个用户能监视的路径数量,超限会给出 ENOSPC,也就是 "No space left on device"。它和磁盘毫无关系,看上去却像磁盘错误。

要紧的是这两个上限都是按用户计算的。以同一个 UID 运行的几十个容器共享同一份预算。往节点上加 Pod 之后基于监视的工具突然开始挂掉,这个典型模式就出自这里。

当前用量这样统计。

# 持有实例的进程
sudo find /proc/[0-9]*/fd -lname 'anon_inode:*inotify*' 2>/dev/null \
  | cut -d/ -f3 | sort | uniq -c | sort -rn | head -5
#      42   9931
#       8   3311
#       2   1042

# 实际的 watch 数量 (fdinfo 里 inotify 行的数量就是 watch 数)
sudo grep -c '^inotify' /proc/9931/fdinfo/* 2>/dev/null | grep -v ':0$' | head
# /proc/9931/fdinfo/18:31204
# /proc/9931/fdinfo/22:8102
cat /etc/sysctl.d/60-inotify.conf
# fs.inotify.max_user_watches = 524288
# fs.inotify.max_user_instances = 1024
sudo sysctl --system

epoll 也有另外一套上限。

cat /proc/sys/fs/epoll/max_user_watches
# 3618421

这个值是每个用户能注册的 epoll 监视条目数,内核会按低端内存的大约 4% 自动推算。一个条目要占用几十字节的内核内存,所以它不可能是无限的。实战中撞上这个上限的情况很少见,但如果是要处理数百万连接的代理,它就属于必查项。

归纳起来是这样。

资源上限单位超限时的 errno
普通 fdRLIMIT_NOFILE进程EMFILE (24)
系统全部文件fs.file-max系统ENFILE (23)
inotify 实例fs.inotify.max_user_instances用户EMFILE (24)
inotify watchfs.inotify.max_user_watches用户ENOSPC (28)
epoll watchfs.epoll.max_user_watches用户ENOSPC (28)

真正的原因多半是泄漏 — 确认方法与应急处置

调高上限就能解决的情况只有两种。一种是确实需要那么多并发连接,另一种是默认值低得离谱。其余全都是泄漏,而对泄漏调高上限,只是把故障发生的时刻往后推几个小时而已。

泄漏的定义很简单。如果 fd 用量与负载无关地单调增长,那就是泄漏。如果它随负载一起起伏,那是正常的容量问题。要分清这两者,几分钟的采样就够了。

PID=21874
while sleep 60; do
  printf '%s  fd=%-7s estab=%-7s close_wait=%s\n' \
    "$(date +%H:%M:%S)" \
    "$(sudo ls /proc/$PID/fd | wc -l)" \
    "$(ss -tan state established | wc -l)" \
    "$(ss -tan state close-wait | wc -l)"
done
# 14:02:11  fd=18204   estab=8102    close_wait=9931
# 14:03:11  fd=19388   estab=8044    close_wait=11102
# 14:04:11  fd=20511   estab=7981    close_wait=12290
# 14:05:11  fd=21702   estab=8120    close_wait=13401

连接数 (estab) 在 8000 附近是平的,fd 却每分钟涨 1200。而且这个增量和 close_wait 的增量完全一致。套接字泄漏已经坐实,如果上一节的 ss -tanp 又把对端也锁定了,那么要排查的代码范围就非常窄了。

想进一步抓到是哪条代码路径在打开,就需要追踪。

# 被打开的文件和调用栈 (bcc 工具)
sudo opensnoop-bpfcc -p 21874

# 打开 fd 的系统调用频率
sudo bpftrace -e 'tracepoint:syscalls:sys_exit_openat /pid == 21874 && args->ret > 0/ { @[ustack] = count(); }'

# strace 开销很大,只能短时间运行
sudo timeout 5 strace -f -e trace=openat,socket,close -p 21874 -c
# % time     seconds  usecs/call     calls    errors syscall
# ------ ----------- ----------- --------- --------- ----------------
#  61.02    0.184203          18     10233           socket
#  38.44    0.116041          21      5488           openat
#   0.54    0.001632           4       402           close

socket 调用了 10233 次,而 close 只有 402 次。光看比例答案就出来了。

应急处置有三个选项,而且是有顺序的。

# 1) 重启进程 — 最可靠,但服务会中断
sudo systemctl restart myapp

# 2) 用 prlimit 只调高上限来争取时间 (无需重启)
sudo prlimit --pid 21874 --nofile=200000:200000

# 3) 强制关闭特定 fd — 最后的手段,应用可能被弄坏
sudo gdb -p 21874 -batch -ex 'call (int)close(1042)'

第 3 项是在应用仍然认为那个 fd 有效的状态下把它关掉,所以必须做好数据损坏或崩溃的心理准备。只在它是生产环境上最后剩下的选项时才用。

结语 — 三个上限与一个真正的原因

如果只记住一件事,那就记住这条。ulimit -n 是你此刻所在这个 shell 的值,和服务毫无关系。该看的永远是 /proc/PID/limits

把诊断顺序压缩一下是这样。

  1. 把报错文字读准确。带 in system 就是 fs.file-max,否则就是进程上限。
  2. /proc/PID/limits 看真正生效的值,用 ls /proc/PID/fd | wc -l 看实际用量。只要有这两样,是不是上限问题立刻就能判定。
  3. 如果是 systemd 服务,不要动 limits.conf,用 systemctl edit 设置 LimitNOFILE 然后 restartreload 是改不动的。
  4. 容器的值由运行时守护进程决定。请在容器内部直接确认 /proc/1/limits
  5. 把 fd 的构成按套接字、文件、管道拆开看。大量 CLOSE-WAIT 是泄漏的确凿证据。
  6. 如果用量与负载无关地单调增长,调高上限也没用。抬高上限只是为排查争取时间的措施,不是解决。
  7. 如果报 "Too many open files" 但 fd 还有余量,请检查 inotify 实例上限。它们的文字是一样的。