Linux服务器如何调整文件描述符限制?系统级与用户级配置详解

"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(用户会话)、systemd LimitNOFILE(服务),改哪一层要对症。
  • 最关键的一点: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,两处配套才真正生效。