服务跑着跑着突然开始报Too many open files,新连接建不上,重启进程能撑一阵,过几小时又犯。这类故障绝大多数跟磁盘、inode都没关系,是单个进程打开的文件描述符(fd)数量打到了自己的软限制。排查顺序其实是固定的:先拿到进程真实生效的上限值,再决定去哪一层改,最后确认到底是配置小还是代码在漏。
报错有两种errno,先看是24还是23
Too many open files这个串对应两个不同的errno。单进程超限是EMFILE,编号24,绝大多数情况撞的是这个;整机超限是ENFILE,编号23,有些语言栈打出来的原话会带后半句Too many open files in system。判断成本几乎为零,但方向差别很大:前者改进程自身的配置,后者要么整机在用的fd数真的顶到了fs.file-max,要么是有别的进程在疯狂漏fd,把整台机器拖下水。
看真实上限,别看ulimit -n
这一步最容易让人白忙。命令行里敲ulimit -n,看到的是当前这个shell的限制,跟正在报错的那个进程毫无关系。systemd起的服务、容器里的进程,各自的限制写在各自的配置里,通常就是默认的软限制1024。要拿到目标进程真实生效的值,直接读/proc:
# 目标进程实际生效的软限制与硬限制 cat /proc/1234/limits | grep -i "open files" # 它现在占着多少个fd ls /proc/1234/fd | wc -l # 整机级别:已分配数、已分配未使用数、上限 cat /proc/sys/fs/file-nr
用lsof -p 1234 | wc -l也能数fd,但进程开了几万个的时候这条命令要跑十几秒,直接数/proc/PID/fd这个目录快得多,也更适合放进监控。
systemd起的服务,改LimitNOFILE
/etc/security/limits.conf只对PAM登录会话生效,ssh进来才走的那套。systemd管理的服务根本不读这个文件,这是改完配置却毫无效果的第一常见原因。实测多台机器上systemd给服务进程的默认值都是软限制1024、硬限制524288,源码里写成DefaultLimitNOFILE=1024:524288。进程自己有权把软限制抬到硬限制,但多数程序压根不去做,于是卡在1024就把fd用光了。
正确做法是给服务挂一段override,别直接改/usr/lib/systemd/system下那个原始文件,免得下次升级包的时候被覆盖:
systemctl edit nginx
# 编辑器里写入并保存 [Service] LimitNOFILE=65535
systemctl daemon-reload systemctl restart nginx systemctl show nginx -p LimitNOFILE
这里有个坑:nginx -s reload不行。worker进程是从master fork出去的,master的限制没变,reload重新拉起的worker继承的还是老值,只有把服务整个restart,systemd才会用新的限制重新exec一次。
容器内还有一层
Docker、containerd这类运行时在容器启动那一刻会给进程设自己的ulimit,容器里看到的软限制经常就是1024,而且它每次重启都会回来。要改就写在启动参数里:
docker run --ulimit nofile=65535:65535 ...
用Compose的话是这一段:
services: app: ulimits: nofile: soft: 65535 hard: 65535
容器内部用ulimit -n往上抬,超过硬限制会直接报cannot modify limit: Operation not permitted,所以别指望在启动脚本里临时提一句能救场。
nginx自己的两个参数要一起抬
worker_connections默认512。做反向代理的时候一个请求要占两个连接(客户端一侧加上游一侧),512其实只顶得住256路并发代理请求,这个数字比多数人以为的低不少。抬的时候两个参数一起抬:
worker_rlimit_nofile 65535;
events {
worker_connections 16384;
}
worker_connections大于进程实际fd上限时,fd上限赢。撑不住的时候error.log里的字样是accept4() failed (24: Too many open files)。Ubuntu 22.04、24.04、25.10上跑nginx 1.18.0、1.24.0、1.28.0,报错形态完全一致,改完一样要用/proc/PID/limits确认worker进程真的拿到了新值。
怎么区分配置小和真泄漏
抬上限之前先看一眼趋势。隔五分钟跑两次同一条命令,把数记下来:
ls /proc/1234/fd | wc -l sleep 300 ls /proc/1234/fd | wc -l
流量没什么变化而fd数只涨不跌,那就是泄漏,抬上限只是把崩的时刻往后推,推到半夜而已。常见的漏点:HTTP响应体忘了关(Go里的resp.Body、Node里的stream没destroy)、每个请求new一个连接池而不是全局复用、inotify监听重复注册。socket类型的fd单独也能数出来:
ls -l /proc/1234/fd | grep -c socket ss -antp | grep -i close-wait | wc -l
CLOSE_WAIT堆着,说明对端早就把连接关了,是本端的close()一直没被调用,这是相当明确的应用侧信号。之前接手过一台机器,fd数一直贴着上限晃了三个月没人发现,装上监控回头看才有结论:一个每五秒跑一次的定时任务,每次执行都新建http client,从来不带复用。这种事光看报错是看不出来的。
系统级那一层基本不用动
fs.file-max在现代发行版里按内存自动估算,通常大到不必管,真撞上了会在内核日志里看到VFS: file-max limit reached。真正容易挡路的是fs.nr_open,默认1048576,它是单进程硬限制值的天花板,想把ulimit -n设到两百万会被直接拒绝:
sysctl fs.file-max fs.nr_open cat /proc/sys/fs/file-nr
file-nr那三个数是:已分配的句柄数、已分配但未使用的句柄数(新内核恒为0)、系统上限。第一个数逼近第三个数时才算整机吃紧,日常Server上几乎见不到。
数据截至2026年10月,上面这些默认值在Debian、Ubuntu、RHEL系列的近期版本上都保持一致,真正变了的是容器套了一层之后,很多人改完systemd发现还是1024。