开篇 — 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/limits | ulimit -n、limits.conf、systemd LimitNOFILE |
| RLIMIT_NOFILE hard | 单个进程 | 设置被拒绝 | /proc/PID/limits | 同上,非特权只能调低 |
| fs.nr_open | 内核全局 | EPERM/EINVAL | sysctl 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 登录、su、login 属于这一类。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_connections 比 worker_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 |
|---|---|---|---|
| 普通 fd | RLIMIT_NOFILE | 进程 | EMFILE (24) |
| 系统全部文件 | fs.file-max | 系统 | ENFILE (23) |
| inotify 实例 | fs.inotify.max_user_instances | 用户 | EMFILE (24) |
| inotify watch | fs.inotify.max_user_watches | 用户 | ENOSPC (28) |
| epoll watch | fs.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。
把诊断顺序压缩一下是这样。
- 把报错文字读准确。带
in system就是fs.file-max,否则就是进程上限。 - 用
/proc/PID/limits看真正生效的值,用ls /proc/PID/fd | wc -l看实际用量。只要有这两样,是不是上限问题立刻就能判定。 - 如果是 systemd 服务,不要动 limits.conf,用
systemctl edit设置LimitNOFILE然后restart。reload是改不动的。 - 容器的值由运行时守护进程决定。请在容器内部直接确认
/proc/1/limits。 - 把 fd 的构成按套接字、文件、管道拆开看。大量
CLOSE-WAIT是泄漏的确凿证据。 - 如果用量与负载无关地单调增长,调高上限也没用。抬高上限只是为排查争取时间的措施,不是解决。
- 如果报 "Too many open files" 但 fd 还有余量,请检查 inotify 实例上限。它们的文字是一样的。
현재 단락 (1/144)
症状是这样出现的。