发布时间:2026-09-16 12:31 更新时间:2026-09-16 12:31 阅读量:0
不少站长遇到过这种场景:网站或后端服务刚启动一切正常,跑上几个小时甚至几天,日志里开始刷 Too many open files,接口报 500,重启一下服务又恢复正常,然后过一段时间再次复发。这个现象通常指向两个方向:一是进程能打开的文件句柄上限被耗尽,二是程序本身存在句柄泄漏(打开后没关闭)。排查的关键不是急着调大数字,而是按顺序核对「系统级限制 → 服务管理器限制 → 进程实际句柄数」这三层,先弄清到底卡在哪一层。
Linux 中对一个进程能打开的文件描述符数量,存在多层约束,最终生效的是其中较小的那个值:
第一层是内核参数 fs.file-max,它是整个系统所有进程能打开句柄的总量上限,通常默认值很大,除非是超高并发场景,一般不先动它。第二层是登录会话或服务管理器的限制,也就是 ulimit -n 所代表的 nofile(RLIMIT_NOFILE),它又分 soft limit 和 hard limit,普通进程只能把 soft 提到不超过 hard 的值。第三层是进程自己运行时实际占用的句柄数,用 /proc/<pid>/fd 目录计数就能看到。
很多教程一上来就让人改 /etc/security/limits.conf,但这个文件只对经过 PAM 登录的会话(如 SSH 登录后的 shell)生效。systemd 管理的服务默认不读 limits.conf,而是走 unit 文件里的 LimitNOFILE 或全局的 DefaultLimitNOFILE。这就是为什么很多人改了 limits.conf、重登服务器后 ulimit 看着变大了,服务却依然报错——因为服务根本不是从那个会话启动的。
排查建议从「现象最直接的一层」入手,也就是先看报错进程当前的上限和实际占用,再往上找是谁设的这个上限。
先找到目标进程 PID,并查看它当前生效的句柄上限:
# 假设服务名为 myapp.service,先取主进程 PID
systemctl show -p MainPID --value myapp.service
查看该进程的句柄上限(Max open files 一行)
cat /proc/<PID>/limits | grep -i "open files"
统计该进程当前实际打开的句柄数量
ls /proc/<PID>/fd | wc -l
如果 ls /proc/<PID>/fd | wc -l 的结果已经贴着 Max open files 的 soft limit,基本可以确认是句柄耗尽。此时再进一步看这些句柄都指向什么,判断是正常业务量增长还是泄漏:
# 按句柄指向的目标归类统计,找出占用最多的类型
ls -l /proc/<PID>/fd | awk '{print $NF}' | sort | uniq -c | sort -rn | head -20
如果输出里大量是 socket:[...],说明连接没被正确关闭,属于典型的连接泄漏,调大上限只是延后爆掉的时间;如果大量是同一个日志文件或 /tmp 下的临时文件,则要看代码或配置里的文件打开逻辑。注意,不同内核版本 ls -l /proc/<PID>/fd 的输出格式略有差异,具体以实际环境为准。
确认是上限不够之后,再判断这个上限是谁给的。对 systemd 服务,直接查它继承的限制:
systemctl show myapp.service | grep -i limit
或查看全局默认
systemctl show | grep -i DefaultLimitNOFILE
如果服务由 systemd 管理,正确做法是在 unit 文件里显式声明,而不是依赖 limits.conf。可以编辑服务自己的 unit 文件,在 [Service] 段落加入:
[Service]
LimitNOFILE=65535
改完执行 systemctl daemon-reload,再 systemctl restart myapp.service,然后用前面的 cat /proc/<PID>/limits 复核是否生效。注意 soft 和 hard 可以分别写,例如 LimitNOFILE=65535:65535;只写一个数时,systemd 会把它同时作为 soft 和 hard。具体取值上限还受内核 fs.nr_open 约束,改到超出可能启动失败,建议先小步调整验证。
如果服务是普通用户通过 SSH 会话手动或脚本启动,那么 limits.conf 才起作用。在 /etc/security/limits.conf 中追加(注意域名/用户名写法与通配符的适用范围):
# 对指定用户放开句柄上限
myuser soft nofile 65535
myuser hard nofile 65535
对 www-data 之类的服务账号同样可单独配置
修改后需要重新登录(或重启相关会话)才会生效。有些发行版还会在 /etc/security/limits.d/ 下放独立的 conf 文件,且加载顺序可能覆盖主文件,排查时别漏看这个目录。另外要记住:limits.conf 对已经在运行的进程无效,改完必须重启对应服务或会话。
最后补充一点,如果确认是程序泄漏(句柄数随时间单调上涨、重启即回落),那么无论把上限调到多大都只是权宜之计。此时应结合日志和代码定位未关闭的连接或文件,从根因修复;调大上限只是为修复争取时间的临时缓冲。
遇到 Too many open files,推荐的核对顺序是:先用 /proc/<PID>/fd 确认实际占用与上限是否逼近,再用 systemctl show 或 /proc/<PID>/limits 判断限制来自 systemd 还是登录会话,最后才对症修改 LimitNOFILE 或 limits.conf,改完务必重启服务并用命令复核。把「句柄数随时间是否持续上涨」作为区分容量不足与泄漏的分水岭,能让排查少走很多弯路。日常也可以给关键服务加一条定时采集句柄数的监控,在真正报错前就发现异常增长。
| 📑 | 📅 |
|---|---|
| Nginx 后端真实 IP 获取:X-Forwarded-For 与 real_ip 配置 | 2026-09-16 |
| rsync 增量同步与断点续传实战:exclude、--delete 与限速 | 2026-09-16 |
| MySQL 主从延迟排查:Seconds_Behind_Master 忽高忽低怎么定位 | 2026-09-16 |
| 容器内存超限被 OOM Kill 排查:指标、cgroup 与堆参数对应 | 2026-09-16 |
| Nginx 499 状态码排查实战:客户端断连与 upstream 超时的区别 | 2026-09-16 |
| PostgreSQL 连接数与内存调优:max_connections 与 shared_buffers 怎么配 | 2026-09-16 |
| 服务器时间不同步导致证书与定时任务异常:NTP/chrony 校时配置与排查 | 2026-09-16 |
| Linux内存被缓存吃满:buff/cache该不该手动释放 | 2026-09-17 |
| SSH 连接频繁断开与卡顿排查:心跳、MTU 与 DNS 反解 | 2026-09-17 |
| MySQL表空间与ibdata1膨胀处理:独立表空间、碎片整理与磁盘回收 | 2026-09-17 |