"too many open files",这行报错几乎每个运维都见过,尤其在 Nginx、Tomcat、Redis、消息队列这类高并发服务上。它出现的根因是:进程能同时打开的文件描述符数量有上限,一旦并发连接数、日志句柄、socket 数量合计超过这个上限,程序就无法再打开新的文件或连接,业务开始报错。
这个上限看着简单,但要改对、改全并不容易,因为 Linux 里它有好几层:内核全局上限、单用户上限、会话级软硬限制、以及 systemd 服务的独立限制。最常见的坑是——你辛辛苦苦改了 /etc/security/limits.conf,重启服务却发现根本没生效,因为 systemd 启动的服务压根不读这个文件。这篇文章把这几层限制理清楚,给出每层的配置位置和验证方法,重点戳破那个"改了不生效"的经典坑。
文件描述符是什么,为什么会有上限
在 Linux 里"一切皆文件",这个哲学的直接后果是:打开一个文件、建立一个 socket 连接、创建一个管道、加载一个动态库,都会占用一个文件描述符(File Descriptor,简称 FD)。FD 是一个非负整数,是进程访问这些资源的句柄。
查看某个进程当前用了多少 FD:
# 当前值
ls /proc/PID/fd | wc -l
# 该进程的上限
cat /proc/PID/limits | grep -i 'open files'
高并发服务对 FD 的需求量很大。粗略量级参考(经验值,随配置变化):
| 服务类型 | FD 量级 | 主要消耗 |
|---|---|---|
| 单机 Nginx | 几千到几十万 | 每个连接一个 FD,反向代理则是双份 |
| Tomcat / Java 应用 | 几千到数万 | 连接 + 线程 + 日志 + jar 映射 |
| Redis | 约等于客户端连接数 | 每个客户端连接一个 FD |
| 消息队列(Kafka 等) | 数万 | 分区 + 连接 + 日志段 |
| 代理 / 网关 | 十万级 | 每秒新建连接数决定瞬时峰值 |
上限存在的原因很朴素:FD 是内核资源,每个都要消耗内存,不加限制的话,一个失控的程序(比如忘了关句柄的循环)能迅速耗尽系统资源,拖垮整台机器。
第一层:会话级的 ulimit
ulimit 是最直接的一层,它限制的是当前 shell 会话及其子进程能打开的 FD 数量。
# 查看软限制和硬限制
ulimit -n # 软限制(soft limit)
ulimit -Sn # 软限制
ulimit -Hn # 硬限制(hard limit)
# 在当前会话内临时调整
ulimit -n 65535 # 只能调软限制,且不能超过硬限制
ulimit -Hn 65535 # 提高硬限制需要 root
这里要理解软硬限制的区别:
- 软限制(soft limit):当前实际生效的限制,进程运行时受它约束。普通用户可以把它调高,但最高不能超过硬限制。
- 硬限制(hard limit):软限制的天花板。只有 root 能提高硬限制。普通用户可以在硬限制范围内任意调低软限制。
# 演示:软限制不能超过硬限制
ulimit -Hn 1024
ulimit -n 99999 # 报错:cannot modify limit: Operation not permitted
ulimit 改的是当前会话,关掉终端就失效,所以它只适合临时测试。要持久化,得靠下一层的配置文件。
第二层:用户级 PAM 配置(limits.conf 与 limits.d)
持久化的用户级限制,配置在 /etc/security/limits.conf 以及 /etc/security/limits.d/ 目录下的文件里。
# 查看现有配置
cat /etc/security/limits.conf
ls -l /etc/security/limits.d/
文件格式是四列:域名 类型 项目 值
# /etc/security/limits.conf 示例
# 域 类型 项目 值
* soft nofile 65535
* hard nofile 65535
nginx soft nofile 65535
nginx hard nofile 65535
# 也可以限制进程数,防止 fork 炸弹
* soft nproc 65535
* hard nproc 65535
几个要点:
域可以是用户名、@组名(加 @)或*(所有用户)。类型是soft、hard或-(同时设置软硬)。项目里和 FD 相关的是nofile,进程数是nproc。limits.d/里的文件优先级高于limits.conf里同项的配置,因为按字母序后加载。
RHEL/CentOS/Rocky 系默认会有一个 /etc/security/limits.d/20-nproc.conf,里面往往把 * 的 nproc 设成一个较大的值。要注意文件加载顺序,后加载的覆盖先加载的。
关键前提:/etc/security/limits.conf 只在通过 PAM 登录的场景下生效,也就是你登录 shell(本地 tty、SSH)时。它由 pam_limits.so 模块读取。确保 PAM 里加载了这个模块:
grep pam_limits /etc/pam.d/login
grep pam_limits /etc/pam.d/sshd
# 应能看到:session required pam_limits.so
第三层:systemd 服务的 LimitNOFILE(最重要的坑)
这是本文的核心,也是九成"改了 limits.conf 没生效"的根源。
现代 Linux 上,服务几乎都是 systemd 拉起来的。systemd 服务不经过 PAM 登录流程,因此根本不读 /etc/security/limits.conf。你在 limits.conf 里改得再对,systemctl restart 服务 后服务的 FD 上限还是默认值 1024(或 systemd 的默认值)。
怎么确认?看服务的实际限制:
# 用一个实际例子:查看 nginx 主进程的 FD 上限
sudo cat /proc/$(pgrep -n nginx)/limits | grep -i 'open files'
如果结果是 1024,那 limits.conf 里的 65535 就是没生效。
正确做法是用 systemd 的 LimitNOFILE 设置。推荐用 systemctl edit 做覆盖,而不是直接改 /usr/lib/systemd/system/ 下的原始文件(因为升级会被覆盖):
sudo systemctl edit nginx
这会打开一个 override 文件,写入:
[Service]
LimitNOFILE=65535
保存后重新加载并重启服务:
sudo systemctl daemon-reload
sudo systemctl restart nginx
# 再验证
sudo cat /proc/$(pgrep -n nginx)/limits | grep -i 'open files'
systemctl edit 生成的覆盖文件放在 /etc/systemd/system/nginx.service.d/override.conf,原始文件不动,升级也安全。
注意 Nginx 的双层限制:Nginx 除了 systemd 的 LimitNOFILE,还有自己配置文件里的 worker_rlimit_nofile 和 worker_connections。worker_rlimit_nofile 告诉 Nginx 自己把 FD 上限提到多少(前提是 systemd 允许),worker_connections 限制每个 worker 能处理的连接数。两者要匹配:
# /etc/nginx/nginx.conf
worker_rlimit_nofile 65535;
events {
worker_connections 10240;
use epoll;
}
理论最大连接数 ≈ worker_processes × worker_connections(反向代理场景每个连接占两个 FD,要除以二)。
风险提示:systemctl edit 写错会覆盖原配置。改完后如果服务起不来,可以删除覆盖文件 /etc/systemd/system/服务.service.d/override.conf 并 daemon-reload 回滚。
第四层:内核全局限制
上面几层是"每个进程"的限制,还有两个是整台机器的全局限制。
fs.file-max:系统级文件句柄上限
这是整个内核能分配的文件句柄总数(所有进程加起来,不只是 FD,还包括内核内部打开的句柄)。
# 查看
cat /proc/sys/fs/file-max
sysctl fs.file-max
# 查看当前使用情况
cat /proc/sys/fs/file-nr
# 输出三列:已分配 未使用 最大值
# 例如:1344 0 9223372036854775807
调整:
# 临时
sudo sysctl fs.file-max=2097152
# 永久
echo 'fs.file-max = 2097152' | sudo tee /etc/sysctl.d/99-file-max.conf
sudo sysctl --system
fs.nr_open:单进程 FD 上限的"天花板"
fs.nr_open 是单个进程能打开的 FD 的绝对上限,也就是 ulimit -Hn 和 systemd LimitNOFILE 不能超过的那个天花板。
cat /proc/sys/fs/nr_open
# 默认常见值:1048576
如果你把 LimitNOFILE 设成比 fs.nr_open 还大,服务会启动失败或限制被自动压回。所以调大单进程限制时,要确保不超过 fs.nr_open。
| 参数 | 作用范围 | 典型默认值 | 调整方式 |
|---|---|---|---|
fs.file-max |
整个内核的句柄总数 | 与内存相关,通常很大 | sysctl / /etc/sysctl.d/ |
fs.nr_open |
单进程 FD 上限的天花板 | 1048576 | sysctl / /etc/sysctl.d/ |
nofile(limits.conf) |
用户会话内软硬限制 | 1024 | limits.conf |
LimitNOFILE(systemd) |
单个 systemd 服务 | 1024 或继承 | systemctl edit |
验证:怎么确认真的生效了
改完别急着说"好了",一定要验证。方法按层次来。
验证当前会话:
ulimit -n
ulimit -Hn
验证某个运行中进程(最权威):
cat /proc/PID/limits | grep -i 'open files'
# 输出形如:
# Max open files 65535 65535 files
# 两列分别是软限制和硬限制
验证 systemd 服务:
# 查看 systemd 认为该服务应该有的限制
systemctl show nginx -p LimitNOFILE
# 输出:LimitNOFILE=65535
验证内核实时使用量:
# 系统整体
watch -n1 'cat /proc/sys/fs/file-nr | awk "{print \"allocated:\", \$1}"'
# 单个进程
sudo ls /proc/PID/fd | wc -l
一次性综合排查脚本:
PID=$(pgrep -n nginx)
echo "== 进程 $PID 的限制 =="
cat /proc/$PID/limits | grep -i 'open files'
echo "== systemd 设置 =="
systemctl show nginx -p LimitNOFILE
echo "== 内核上限 =="
echo "fs.file-max = $(cat /proc/sys/fs/file-max)"
echo "fs.nr_open = $(cat /proc/sys/fs/nr_open)"
echo "== 当前 FD 使用 =="
echo "本进程已用: $(sudo ls /proc/$PID/fd | wc -l)"
echo "系统已用: $(awk '{print $1}' /proc/sys/fs/file-nr)"
一个完整的实操案例
假设有一台跑 Nginx 的服务器,业务量增长后开始报 too many open files。完整的处理流程:
# 1. 确认现象和当前上限
sudo cat /proc/$(pgrep -n nginx)/limits | grep -i 'open files'
# 假设显示 1024
# 2. 查看当前连接数,估算需要多少 FD
ss -s
# 或
ss -tn state established | wc -l
# 3. 调整 systemd 服务限制(关键步骤)
sudo systemctl edit nginx
写入:
[Service]
LimitNOFILE=65535
# 4. 调整 Nginx 自身配置内联的上限
sudo vi /etc/nginx/nginx.conf
worker_rlimit_nofile 65535;
events {
worker_connections 10240;
}
# 5. 重新加载并重启
sudo systemctl daemon-reload
sudo nginx -t # 先校验配置
sudo systemctl restart nginx
# 6. 验证
sudo cat /proc/$(pgrep -n nginx)/limits | grep -i 'open files'
# 应显示 65535
systemctl show nginx -p LimitNOFILE
同时如果预计会超过内核默认,也一并调大 fs.file-max 和 fs.nr_open。
监控与 FD 泄漏
调大限制之后别忘了监控,否则只是把问题往后推。有两类情况要分开看:一是容量确实不够,需要按业务增长持续调大;二是程序泄漏 FD,限制调多大都会被耗光,这时要治本。
判断是不是泄漏,看某进程 FD 数量的趋势:
# 连续观察某个进程的 FD 数量,看是否持续单调增长
for i in $(seq 1 12); do
echo "$(date +%T) $(sudo ls /proc/$PID/fd | wc -l)"
sleep 5
done
如果业务平稳时 FD 数量却一路向上、从不回落,基本可判定是句柄泄漏,典型原因是程序打开文件或连接后没有关闭。这时要看它到底打开了什么:
# 按文件类型统计当前进程打开的句柄分布
sudo ls -l /proc/$PID/fd | awk '{print $11}' | sort | uniq -c | sort -rn | head
# 看是否有大量指向同一个文件的重复句柄
sudo lsof -p $PID | awk '{print $9}' | sort | uniq -c | sort -rn | head
监控上,建议把 /proc/sys/fs/file-nr 的第一个数字(系统已分配句柄数)和它的上限做对比,设置告警阈值,比如达到上限的百分之七十就提醒。Nginx、Java 这类服务的 FD 使用量通常和连接数强相关,把它和连接数指标一起看,能提前发现异常。
常见问题
改了 limits.conf 为什么服务还是报 too many open files?
因为服务是 systemd 启动的,systemd 不读 limits.conf。要用 systemctl edit 服务名 加 LimitNOFILE=65535,然后 daemon-reload + restart。这是最高频的坑。
普通用户 ulimit -n 65535 报 Operation not permitted?
你改的是软限制,超过了硬限制。要么用 root 提高硬限制(ulimit -Hn 65535),要么在 limits.conf 里把 hard 也设大,然后重新登录。注意:limits.conf 是登录时生效,改完要退出重新登录。
硬限制和软限制,改了哪个生效?
进程实际受软限制约束。硬限制只是软限制能调到的天花板。所以你会看到 /proc/PID/limits 里 soft 才是真正卡住你的那个值。通常是 soft 和 hard 一起设。
把 LimitNOFILE 设成很大(比如 10485760)有什么后果?
可能超过 fs.nr_open 导致服务启动失败,或被内核压回。而且限制设得过大,等于放弃了保护,某个程序泄漏 FD 时会消耗更多内核资源。按业务实际需要的 1.5-2 倍设置即可,别盲目求大。
为什么 Nginx 改了 systemd 限制还需要改 worker_rlimit_nofile?
因为 Nginx 自己有 worker 进程,worker_rlimit_nofile 是 Nginx 主动向系统申请的 FD 上限,systemd 只是给了"允许的最大值"。如果 Nginx 配置里没有 worker_rlimit_nofile,它会用默认值,可能还是很小。两处要配套改。
总结
- FD 限制是分层的:
fs.file-max(内核总句柄数)、fs.nr_open(单进程天花板)、limits.conf(用户会话)、systemdLimitNOFILE(服务),改哪一层要对症。 - 最关键的一点:systemd 服务不读
limits.conf,必须用systemctl edit 服务名配置LimitNOFILE,改完daemon-reload再restart,这是"改了不生效"的头号原因。 ulimit只管当前会话,临时有效;持久化用户级用/etc/security/limits.conf,服务级用 systemd override。- 验证必须落到
/proc/PID/limits(进程实际限制)和systemctl show 服务 -p LimitNOFILE(systemd 设置)上,别只看配置文件。 LimitNOFILE不要超过fs.nr_open;Nginx 这类服务还要同步调worker_rlimit_nofile和worker_connections,两处配套才真正生效。
企业QQ咨询




