Linux 文件描述符与 ulimit 调优实战:Too many open files 完整排查指南

「Too many open files」到底是什么错

Linux 运维里有一个报错,几乎每个把网站跑起来的站长迟早都会撞上:Too many open files。它可能出现在 Nginx 的错误日志里,可能出现在 PHP 的警告里,也可能出现在 MySQL 的日志中。更麻烦的是,它的表现千奇百怪——有时是网站突然 502,有时是数据库连不上,有时只是图片加载失败,而日志里那一行英文又很容易被忽略。要根治它,得先搞清楚 Linux 里「文件描述符」这个概念。

在 Linux 的设计哲学里,一切皆文件。不只是磁盘上的文档叫文件,网络连接、管道、设备、甚至你打开的 socket,都用一个统一的抽象来表示,叫「文件描述符」(File Descriptor,缩写 FD)。它是一个非负整数,本质上是一张表里的索引:进程拿着这个号码去表里查,就能找到对应的资源。所以当你的浏览器打开一个网页,Nginx 要同时持有对客户端连接的 FD、对后端 PHP-FPM 的 FD、对磁盘上静态文件的 FD、对日志文件的 FD……一个请求可能就消耗掉好几个。

关键在于,Linux 对每个进程能持有的 FD 数量是有上限的。这个限值不是防止系统崩溃的硬性物理限制,而是一道可调的安全阀。默认值通常偏保守:很多发行版里普通用户软限制只有 1024。对于一个日访问量几千的个人站,1024 在低峰期够用,可一旦遇到爬虫集中抓取、或者流量突然上涨,FD 瞬间被吃光,新的连接就再也建不起来——于是 502、连接超时接踵而至。这就是问题的全貌。

第一步:确认现在的限值是多少

排查之前先看清现状。查看一个进程的 FD 限制,最直接的方式是读它的 limits 文件。假设我们要查 Nginx 主进程(master)的 PID,可以这样操作:

# 找到 nginx master 进程 PID
PID=$(cat /run/nginx.pid)

# 查看该进程所有资源限制
cat /proc/$PID/limits

# 只看「打开文件数」(Max open files)这一项
grep -i "open files" /proc/$PID/limits

输出会分两列:Soft Limit(软限制)和 Hard Limit(硬限制)。软限制是当前生效的实际上限,硬限制是软限制可以调到的天花板。普通进程可以自行把软限制往硬限制以内调高,但想突破硬限制,就必须有 root 权限(或者由 systemd 这样的 init 系统在启动时设好)。

除了看单个进程,还可以看系统整体的统计:

# 系统级「已分配」与「上限」的 FD 总数
cat /proc/sys/fs/file-nr
# 输出三个数字:已分配 / 未使用(=0) / 系统上限
# 第三个数字来自 fs.file-max,是整个系统所有进程加起来的上限

cat /proc/sys/fs/file-max

如果 file-nr 的第一个数字已经逼近第三个数字,说明系统级也快撑不住了,需要同时抬高 fs.file-max。不过对个人站来说,瓶颈几乎总是出在单进程的 ulimit 上,系统级的上限(通常是几十万甚至上百万)很少成为约束。

第二步:搞清 ulimit 的三处来源

「明明改了却没用」是这类问题最让人抓狂的地方,原因在于 ulimit 的设置分散在几个地方,优先级还有讲究。理清这三处,问题就明朗了:

来源一:PAM 全局配置 /etc/security/limits.conf这是传统上最常用的地方,针对用户或用户组设置。写法是四列:域、类型、项目、值:

# /etc/security/limits.conf
# 域    类型   项目        值
*       soft   nofile      65535
*       hard   nofile      65535
root    soft   nofile      65535
root    hard   nofile      65535
nginx   soft   nofile      65535
nginx   hard   nofile      65535

注意 nofile 就是「打开文件数」的别名。这里有个大坑:PAM 的 limits.conf 只对「通过 PAM 登录的会话」生效。也就是说,你手动 SSH 登录后启动的进程能读到,但由 systemd 直接拉起的服务(绝大多数现代发行版里的 Nginx、MySQL、PHP-FPM 都是)根本不走 PAM,读不到这个文件。这就是无数人「改了 limits.conf 重启服务依然无效」的根源。

来源二:systemd 的服务单元配置。现代发行版里,服务由 systemd 启动,需要在对应的 unit 里单独设置。有两种方式,一种是编辑 unit 文件加 LimitNOFILE

# /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=65535

推荐用 systemctl edit nginx 命令来生成这个 override 片段,它会自动放到正确的路径,并在重新加载时覆盖主 unit 里的默认值。另一种方式是在全局的 /etc/systemd/system.conf 里设 DefaultLimitNOFILE=65535,对所有服务生效——但改这个影响面大,一般不建议。

来源三:内核全局上限 fs.file-max这是整个系统的天花板,通过 sysctl 调整,它是所有进程加起来的总额,通常不需要动,除非你在一台小内存 VPS 上跑了很多服务。

第三步:按服务类型正确调优

明确了来源,调整就有的放矢了。下面针对最典型的几个服务分别给出做法。

Nginx。Nginx 有两个相关的配置:worker_rlimit_nofileworker_connections。前者告诉 Nginx 在启动时主动把 FD 上限调到某个值(需要硬限制允许),后者是每个 worker 能处理的连接数。经验关系是:worker_connections 不应该超过 worker_rlimit_nofile,而整个进程能承接的连接数上限,大致等于 worker_processes × worker_connections。因为每个客户端连接还会占用后端 FD、文件 FD,所以预留余量很关键:

# nginx.conf 顶层(main 上下文)
worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 16384;
    multi_accept on;
    use epoll;
}

同时别忘了 systemd 那一层,否则 worker_rlimit_nofile 想调高的请求会被硬限制挡住:systemctl edit nginx 里加上 LimitNOFILE=65535,然后 systemctl daemon-reload && systemctl restart nginx

MySQL。MySQL 的 FD 消耗主要来自连接数、表缓存、以及打开的表文件。如果 MySQL 报 Can't open fileToo many open files,先看 open_files_limit 这个变量:

SHOW VARIABLES LIKE 'open_files_limit';
SHOW GLOBAL STATUS LIKE 'Open_files';

如果状态里的 Open_files 逼近变量值,就要抬高。MySQL 8.0 的 open_files_limit 是只读变量,需要在 systemd 的 unit 里设 LimitNOFILE=65535,MySQL 启动时会据此自动计算。同时把 table_open_cache 控制在合理范围——它太大虽然能加快打开表的速度,但每个缓存项都会占用一个 FD,反而可能把限额吃光。

PHP-FPM。PHP-FPM 每个子进程在处理请求时也会打开 FD(连接 MySQL、打开日志、读写文件)。如果日志出现 Too many open files,除了抬高 LimitNOFILE,还要检查 pm.max_children 是不是设得过大——子进程数量乘以每进程的 FD 消耗,就是总量。过低配的机器上盲目调大 max_children 反而会引发内存和 FD 双重饥饿。

第四步:验证调整是否真的生效

改完配置,一定要验证,而不是「看起来改了就完事」。验证分几个层面:

# 1. 确认进程实际拿到的限制(这才是真相)
grep "Max open files" /proc/$(cat /run/nginx.pid)/limits

# 2. 统计某进程当前真实打开的 FD 数量
ls /proc/$(cat /run/nginx.pid)/fd | wc -l

# 3. 看系统整体 FD 使用情况
cat /proc/sys/fs/file-nr

第一条命令的输出才作数——如果它显示的还是 1024,那说明你的 systemctl edit 没生效,或者服务没真正重启(reload 不会重新读取 systemd 的 Limit 配置,必须 restart)。这是最容易犯的错:以为 reload 一下就行,其实 Limit 类配置只在进程启动时读取,必须重启服务。

第二条命令能让你看到实时用量。如果它逼近限制值,就要继续抬高;如果远低于限制,那说明「Too many open files」另有原因(比如是程序泄漏 FD,或者是别的进程的问题),别在限额上白费力气。

看不见的麻烦:FD 泄漏

有一种情况比限额不够更棘手——程序把 FD 泄漏了。表现是:限额明明调到了 65535,但 FD 用量还是稳定爬升,直到撞顶再崩一次。这类问题多半出在代码或配置:比如程序打开文件后忘了关闭,或者连接池把断掉的连接一直挂着不放。

定位泄漏的思路是「盯住增长」:每隔几秒采样一次某进程的 FD 数量,画一条曲线,看它是否只涨不跌。如果持续单调上升,就是泄漏无疑。查泄漏源头可以用 lsof 看进程到底打开了哪些文件,按类型归类:

# 看某进程打开的文件,按类型统计
lsof -p $(cat /run/nginx.pid) | awk '{print $5}' | sort | uniq -c | sort -rn

如果大量 FD 指向的是已删除的文件(lsof 里显示 (deleted)),那通常是日志被切割后进程没重开句柄——用 logrotate 时配置 copytruncate 或者给 Nginx 发送 USR1 信号重开日志即可解决。如果大量 FD 指向 socket 处于 CLOSE_WAIT 状态,那是对端关闭而本端没关,属于应用层的连接管理缺陷,需要从代码或超时配置入手。

调优的边界意识

最后要说一个容易被忽略的原则:并不是把 nofile 调到越大越好。FD 上限本质上是内存资源的映射,每个打开的文件、每个 socket 在内核里都要占用结构体内存。在一台 1GB 内存的小 VPS 上盲目把限额抬到百万级,一旦真的被大量连接占满,可能先把内存耗尽,触发 OOM Killer 把服务杀掉——这比 FD 不够更难排查。

合理的做法是:根据服务器内存、预期并发量估算,把 Nginx 的 worker_connections 和系统 nofile 设在一个有机动余量、但又不远超物理承受能力的水平。65535 对绝大多数个人站已经绰绰有余;再高,收益微乎其微,风险却在增加。调优的目标是「够用且留有余量」,而不是追求数字上的极限。

把排查流程串一遍:先 /proc/PID/limits 确认现状,再理清 PAM、systemd、内核三处来源,按服务分别设置,最后用 /proc/PID/fdfile-nr 验证。遇到「改了没用」,第一反应就该是「是不是 systemd 那层没设」和「是不是只 reload 没 restart」。掌握这套方法,Too many open files 这类报错就从玄学变成了可预测、可根治的普通配置问题。

Last modification:September 14th, 2026 at 12:31 pm

Leave a Comment