1. 文件描述符与进程数限制到底是什么
1.1 文件描述符不是“文件”那么简单
先聊一个我经常在面试里问的问题:你见过 too many open files 这个报错吗?十个人里有八个会说见过,但追问一句“这个 open files 到底指的是什么”,能答清楚的人就少一大半了。
文件描述符(File Descriptor),简称 fd,是操作系统给每个被打开的资源分配的一个非负整数编号。注意,这里说的“资源”不只是普通文件,还包括网络 socket、管道、设备节点、共享内存对象等。也就是说,你在 Linux 上每建立一个 TCP 连接,每打开一个日志文件,每启动一个子进程的管道通信,背后都会消耗一个文件描述符。
fd 的编号规则很简单:从 0 开始递增,0 是标准输入,1 是标准输出,2 是标准错误输出,这三个是程序启动时默认就有的。之后每打开一个新资源,系统就分配一个当前可用的最小编号。进程退出时,所有 fd 自动关闭;fd 被关闭后,编号可以复用。
很多人把 fd 理解成“文件句柄”,这个概念本身没问题,但容易让人忽略一个关键点:网络连接也走 fd。在高并发场景下,fd 不够用了,表现往往不是“文件打不开”,而是“连不上”、“请求超时”,甚至进程直接崩掉。这也是为什么 fd 限制会成为运维排查的盲区——你盯着网络配置折腾半天,结果问题出在进程的资源上限上。
1.2 进程数限制背后的资源模型
进程数限制这块,误解更多。Linux 里的“进程数限制”通常指的是单个用户能创建的进程/线程总数,由 RLIMIT_NPROC 控制。但是很多人不知道,这个限制把线程也算进去了。在现代 Linux 内核中,线程和进程在内部统称为 task,创建线程同样要占用 PID 资源和进程表项。
我曾经把一个服务的线程数从 200 调到 2000,结果发现进程数上限被击穿了,服务直接报 Resource temporarily unavailable。当时第一反应是内存不够,查了一圈才发现是 nproc 限制的问题。
这个坑特别值得强调:计算资源上限时,不能只看进程数,要把线程数也加进去。比如你设置了 nproc=1024,但 Java 应用启动后默认就有 300 个线程,那这个服务实际上只能再开 700 个左右的进程,很容易撞到天花板。
还有个很容易混淆的点:进程数限制分用户态和内核态两层。用户态的限制通过 ulimit -u 或 /etc/security/limits.conf 配置,限制的是某个用户能创建的进程总数;内核态的限制则包括 kernel.pid_max(全局 PID 编号上限,默认通常是 32768 或更大)和 kernel.threads-max(内核线程上限)。这两者不是一回事,调了用户态限制但内核态不够,同样会出问题。
打个比方:用户态限制像是你家小区能住的户数,内核态限制像整座城市能容纳的人口。小区再大,城市总人口满了,你也住不进去。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三层限制体系的来龙去脉
2.1 从 ulimit 到 limits.conf 再到 systemd,三层限制的关系
Linux 的资源限制体系,说白了就两层:会话级别的和进程级别的。但实际配置起来,很多人会被绕晕,因为有三套东西在起作用。
第一层是 shell 内置的 ulimit 命令。它只对当前 shell 会话及其子进程生效,shell 一关就失效,属于临时调整的手段。比如你在命令行里执行 ulimit -n 65535,只对当前终端窗口有效,开个新窗口又变回默认值。
第二层是 /etc/security/limits.conf 或 /etc/security/limits.d/ 目录下的配置文件。这层是通过 PAM(可插拔认证模块)在用户登录时加载的,能对指定用户或用户组设置持久化的限制。配置格式长这样:
bash复制# 用户 类型 项目 值
* soft nofile 65535
* hard nofile 65535
* soft nproc 4096
* hard nproc 4096
注意这里有个“软限制”和“硬限制”的区别。软限制是内核实际执行的限制值,硬限制是用户能调高的最大值。普通用户可以把自己进程的软限制调高,但最高不能超过硬限制;只有 root 能直接改硬限制。所以你在配置时,最好把硬限制和软限制都设成同一个值,否则应用自己把限制调高时,可能会被卡在硬限制上。
第三层是 systemd 的限制,通过 service 文件里的 LimitNOFILE 和 LimitNPROC 指令配置。前面两层对 systemd 托管的服务可能根本不生效,因为 systemd 在启动进程时,有自己的系统调用直接设置限制,不走 PAM 那条路。这是很多人在 CentOS 7 之后的系统上踩过的最大的坑:明明 limits.conf 配了,重启服务一看 cat /proc/<PID>/limits,限制还是默认的 1024。
2.2 全局限制与动态加载机制
除了上面说的进程级限制,Linux 还有一套全局限制。最核心的是 fs.file-max,它决定了整个系统范围内所有进程加起来能打开的文件描述符总数。
这个值的默认设置在不同系统上不一样,有的内核会自动按内存大小估算,有的则是固定的。查看方式:
bash复制cat /proc/sys/fs/file-max
如果这个全局值太小,即使单个进程的限制放得再大也没用,因为整个系统的 fd 总数就那么多。调整方式:
bash复制# 临时调整,重启失效
sysctl -w fs.file-max=2097152
# 永久调整
echo "fs.file-max = 2097152" >> /etc/sysctl.conf
sysctl -p
全局限制还有一个兄弟参数叫 fs.nr_open,它规定了单个进程能设置的 fd 数上限,默认是 1048576。也就是说,你就算在 limits.conf 里写了 nofile 2097152,实际生效时也会被卡在 nr_open 这个值上。真需要调更大的话,需要先调 fs.nr_open:
bash复制sysctl -w fs.nr_open=2097152
注意:
fs.nr_open调整后立即生效,无需重启。但fs.file-max调大后,建议确认一下内存是否足够,因为每个 fd 在内核里都有对应的数据结构占用内存。fd 总数开得太大,内存不够的话系统会不稳定。
还有一个常见误解是:调整了 fs.file-max 就等于给所有进程都加了大限制。其实不是,它是“上限的上限”,真正决定单个进程并发能力的还是 limits.conf 或 systemd 里配置的 nofile。
3. 实操:为高并发服务设计一套合理的限制方案
3.1 按业务场景推算需要多少 fd 和进程数
配置限制之前,首先要算清楚你到底需要多少。这里我以一套典型的请求链路为例:Nginx 做反向代理,后面挂一个 Java 应用(比如 Spring Boot),再连一个 MySQL。
先说结论:一个 HTTP 请求进来,Nginx 到 Java 会建立一个上游连接,Java 到 MySQL 会建立一个数据库连接。JVM 本身还有 GC 线程、内部定时任务线程。所以单请求的 fd 消耗量至少是 2(Nginx 侧)+ 1(Java 到 MySQL),这只是最基础的情况。
假设你期望的峰值 QPS 是 5000,平均每个请求占用连接的时间约 200ms,那么用 Little's Law 粗略估算:并发连接数 = 5000 × 0.2 = 1000。这只是业务连接数,再加上 Nginx 本身要维护和客户端之间的连接、Java 应用线程池额外预留的缓冲,建议直接按 4 到 5 倍来预留 fd 数。也就是说 nofile 至少应该设置到 5000 到 6000 左右,考虑到突发流量,我一般建议直接上 10000 到 20000 的级别,反正现在服务器的内存都够扛。
进程数方面,Java 应用如果是默认线程池配置,线程数通常在 200 到 1000 之间。加上 Nginx 的 worker 进程(通常按 CPU 核数 × 2 配置),以及 MySQL 的连接线程,单个用户下同时存在的进程/线程总数到几千是很正常的事。所以 nproc 不要拍脑袋设一个 1024,要按实际业务评估。特别是某些监控 agent 也会占进程数,比如 node_exporter、filebeat 这类常驻进程,虽然单个占得不多,但架不住数量多。
3.2 三层配置的具体操作和验证
有了估算值,接下来就是落地配置。我以一台 CentOS 7.9 服务器为例,目标是让 Java 应用(以 appuser 用户运行)的 nofile 达到 65535,nproc 达到 65535,并且通过 systemd 启动也能生效。
第一步,修改 /etc/security/limits.conf:
bash复制cat >> /etc/security/limits.conf << 'EOF'
appuser soft nofile 65535
appuser hard nofile 65535
appuser soft nproc 65535
appuser hard nproc 65535
EOF
第二步,处理登录会话的 PAM 配置。确认 /etc/pam.d/login 和 /etc/pam.d/sshd 里有下面这行,没有就加上:
bash复制session required pam_limits.so
这一步很多人会漏掉,导致 limits.conf 配置不生效。CentOS 默认是带了的,但某些精简安装的系统会没有,需要手动加。
第三步,调整全局限制:
bash复制cat >> /etc/sysctl.conf << 'EOF'
fs.file-max = 2097152
fs.nr_open = 2097152
EOF
sysctl -p
第四步,如果是 systemd 托管的服务,要单独配置。编辑 service 文件,在 [Service] 段加:
ini复制[Service]
LimitNOFILE=65535
LimitNPROC=65535
然后重载配置并重启服务:
bash复制systemctl daemon-reload
systemctl restart my-java-service
第五步,也是最重要的一步——验证。别配完就完事,一定查看文章里的配置到底生效没有:
bash复制# 查看进程的实际限制
cat /proc/<PID>/limits
# 切换到目标用户身份检查
su - appuser
ulimit -n
ulimit -u
如果 /proc/<PID>/limits 里显示的还是 1024,那就说明 systemd 的限制配置没生效,检查一下 service 文件的语法,或者确认你用的是不是 systemctl --user 级别的服务。如果 ulimit -n 显示 65535 但 /proc/<PID>/limits 只有 1024,说明进程不是通过 systemd 启动的,是通过别的 supervisor 启动的,比如旧的 nohup 脚本或者 Docker,那就要单独处理。
3.3 容器环境下的特殊注意事项
容器里的资源限制是个完全不同的世界。很多人在宿主机上配好了 limits.conf,进容器一看还是默认值,就怀疑配置没生效——其实不是没生效,而是容器根本不会读宿主机上那套配置。
Docker 容器的 fd 和 nproc 限制,由 Docker daemon 在启动容器时指定,默认继承自 Docker daemon 进程的限制。想要单独设置,在 docker run 时加参数:
bash复制docker run --ulimit nofile=65535:65535 --ulimit nproc=65535:65535 my-image
如果用 docker-compose,对应配置段:
yaml复制services:
my-service:
image: my-image
ulimits:
nofile:
soft: 65535
hard: 65535
nproc:
soft: 65535
hard: 65535
判断一个进程是否在容器里受限,最直接的办法还是看 /proc/<PID>/limits。容器里跑应用,我建议在 Dockerfile 或者启动脚本里显式设置 ulimit,不要依赖宿主机配置,不然换一台机器部署就失效。
另外,Kubernetes 环境里还有个隐形坑:Pod 的 spec.containers.resources.limits 只限制 CPU 和内存,不会自动设置 fd 和 nproc。你需要通过 securityContext 里的 ulimits 字段显式配置,或者用准入控制器统一注入。
4. 常见问题与排查技巧实录
4.1 排查“too many open files”的完整思路
服务器出现 too many open files 时,通常有几种表现:应用日志里直接打出这个报错,或者连接被拒绝,或者进程异常退出。我处理过的最离奇的一个案例是,Nginx 连续报这个错,但 lsof 一看 fd 数才用了两三千,离上限还有几万。查了半天才发现是 Nginx 的 worker 进程分别计数,每个进程都有自己独立的 fd 上限,不是共享的。所以排查时要注意:限制是按进程算的,不是按服务算的。
完整的排查链路,我一般是这么走的:
首先看系统级的使用情况:
bash复制cat /proc/sys/fs/file-nr
这个文件有三个数字:已分配 fd 数、未使用 fd 数、系统最大 fd 数。如果第一个数字接近第三个,说明系统级 fd 资源快耗尽,优先调 fs.file-max。
然后定位哪个进程消耗最大:
bash复制for pid in $(ls /proc | grep -E '^[0-9]+$'); do
count=$(ls /proc/$pid/fd 2>/dev/null | wc -l)
if [ "$count" -gt 500 ]; then
echo "PID $pid: $count fds"
fi
done | sort -t: -k2 -rn | head -20
这个脚本会把 fd 数超过 500 的进程按数量排序列出来,瞬间定位“大户”。想更精细的话,用 lsof -p <PID> 查看具体打开的 fd 对应什么文件或连接。
最后确认进程的限制值:
bash复制cat /proc/<PID>/limits | grep -E 'open files|process'
如果发现限制值已经很大但实际使用也很大,就看看是不是代码里有 fd 泄漏。fd 泄漏有个特征:fd 数随时间持续增长,GC 也压不下来。Java 应用可以用 lsof -p <PID> | grep deleted 查已经被删除但仍被占用的文件,那是泄漏的典型信号。
4.2 limits.conf 不生效的 7 个原因
这是运维面试里最爱考的点,也是实际踩坑重灾区。我总结一下,limits.conf 配了但不生效,基本逃不出这几个原因:
-
PAM 没有加载 pam_limits.so。检查
/etc/pam.d/login、/etc/pam.d/sshd和/etc/pam.d/su,确保有session required pam_limits.so这一行。 -
用户切换方式不对。如果你用
su切换到目标用户,不带-参数时,不会重新加载 PAM 会话配置,ulimit看到的还是旧值。记住:用su - username。 -
SSH 连接方式特殊。某些 SSH 客户端在非交互式会话下不触发 PAM session 模块,解决办法是检查
/etc/ssh/sshd_config里UsePAM yes。 -
改错文件。
/etc/security/limits.conf和/etc/security/limits.d/目录下的文件是同一套配置,但limits.d下的文件优先级更高。如果你两个地方都配置了且值不一样,以limits.d为准。我就见过有人改了主配置,但被95-nofile.conf里的旧值覆盖了。 -
进程不是通过登录会话启动的。systemd 服务、cron 任务、Docker 容器内部的进程,都不会走 PAM 登录流程,
limits.conf对它们不生效。 -
语法写错。
limits.conf的列对应关系是:domain、type、item、value。很多人把type和item的顺序搞反,或者 value 写成了字符串。写完之后可以用ulimit -a快速验证。 -
nproc 的硬限制对 root 无效。注意
limits.conf里对 root 用户的 nproc 配置有特殊规则:内核强制 root 的硬限制不能设置得太低。如果你写root hard nproc 10,实际生效可能不是 10,而是系统的一个最小值(通常是 127)。
4.3 nproc 限制击穿的几种典型场景
进程数限制的问题,比 fd 更难排查,因为报错信息很有迷惑性。最常见的报错是 Resource temporarily unavailable,很多人第一反应是内存不足或者线程池满了,走了很多弯路。
我在实践中总结出几个典型场景,大家可以对号入座:
第一种是 Java 应用启动时疯狂创建线程,但 init 脚本里设置了 ulimit -u 1024。Java 光 GC 线程就有好几个,加上连接池和业务线程,轻松超过 1024。这种情况建议初始化脚本里直接 ulimit -u 65535,或者干脆在 service 文件里配 LimitNPROC。
第二种是单用户多实例部署。比如同一个用户下跑着 5 个应用进程,每个占 300 线程,加起来 1500。如果 limits.conf 里写 * soft nproc 1024,所有应用就会互相打架。注意 * 通配符会匹配所有非 root 用户,包括 nobody、mysql 这些系统账户,新加用户容易莫名其妙起不来,也是这个原因。处理方式是改成按用户配置,别用通配符一刀切。
第三种和 fork 炸弹有关。虽然不至于真的有人恶意搞,但代码里的 bug 也可能导致进程无限循环 fork。我遇到过脚本里递归调用自己但退出条件写错了,瞬间炸出几千个进程,直接把系统 OOM。这种场景下,nproc 限制反而是保命符,防止一个失控的进程拖垮整个系统。
4.4 配置完重启失效或者反复被重置
很多人的配置当时生效了,但重启服务器之后回到原点。这通常是因为修改没有持久化到正确的配置层。注意几个坑:
第一,ulimit -n 65535 这种命令只对当前 session 有效,写进 /etc/profile 也只是对所有登录用户生效,对 systemd 服务无效。持久化配置还是要写到 limits.conf 和 sysctl.conf 两处。
第二,如果系统里跑着调优脚本或者监控 agent,有些工具会在启动时主动修改资源限制。比如某些 Java 应用自带的 set-env 脚本会执行 ulimit -n 65535,这本身没问题,但它可能是在 systemd 设置完限制之后又调低了,就会造成配置“不生效”的错觉。
第三,容器平台自动注入的限制。Kubernetes 如果配了 pidsLimit,它会直接限制 Pod 里所有进程的 PID 总数,这个限制来自 cgroup,和 Linux 传统的 nproc 限制是两套机制。遇到容器内 Resource temporarily unavailable,要优先查 cgroup 的限制值,别在宿主机上折腾 limits.conf。
查看 cgroup 限制:
bash复制cat /sys/fs/cgroup/pids/pids.max
cat /sys/fs/cgroup/pids/pids.current
这两个文件分别是 Pod 的最大 PID 数和当前已用 PID 数。如果 pids.current 接近 pids.max,说明要调大这个值,或者减少实例数量。
5. 几个值得收藏的实用命令组合
排查资源限制问题,命令的组合使用很关键,单一命令很难定位到根因。我把自己常用的一套组合分享出来,都是实际项目里反复验证过的。
快速查看所有用户登录会话的资源限制:
bash复制for user in $(getent passwd | awk -F: '$3>=1000 {print $1}'); do
echo "=== $user ==="
su - $user -c "ulimit -n; ulimit -u" 2>/dev/null
done
这条命令能一次性列出所有普通用户的 nofile 和 nproc 限制,适合做批量巡检。注意有些用户没有 shell,会报错,2>/dev/null 把错误信息吞掉就行。
定位占用 fd 最多的进程,更简洁的写法:
bash复制find /proc -maxdepth 2 -name fd -type d -exec sh -c 'echo "$(ls {} | wc -l) {}"' \; | sort -rn | head -10
这条命令不依赖 lsof,在某些没有安装 lsof 的极简系统上也能用。
动态监控某个进程的 fd 数变化:
bash复制watch -n 2 'ls /proc/<PID>/fd | wc -l'
这个适合在压测时观察 fd 泄漏。如果数字持续增长不回落,那基本可以断定有泄漏。
查看系统当前 PID 分配情况:
bash复制cat /proc/sys/kernel/pid_max
cat /proc/sys/kernel/threads-max
这两个值决定全局进程和线程的容量上限,如果太小,即使 nproc 配置合理,也会出现无法创建新进程的情况。
最后还有一个技巧:lsof -p <PID> | grep -vE '^(COMMAND|$)' | awk '{print $5}' | sort | uniq -c | sort -rn | head,这个能按 fd 类型统计一个进程打开的资源分布。比如看到大量 TCP 类型的 fd,说明网络连接占用是主因,就要从连接池和 Keep-Alive 上优化;如果大量 REG 类型,说明文件打开过多,要查日志文件句柄是否泄漏。
我处理过最多的情况是日志框架配置不当,每天上百个日志文件句柄没有释放,几个月下来 fd 数就顶到上限了。这种问题调大 limit 只是治标,治本还是定位到具体的 fd 占用源头。
6. 分享一个真实的调优案例
去年帮一个客户处理过一次线上事故,场景相当典型,写出来供大家参考。
客户的环境是一台 8C16G 的服务器,跑着一个 Spring Boot 网关服务和两个微服务实例,前面是 Nginx 做入口。某天业务高峰期,服务突然大面积超时,应用日志大量刷 Too many open files。登录服务器一看,/proc/sys/fs/file-nr 显示系统总 fd 数才用了 128000,但 file-max 配置的是 2097152,说明系统级资源完全够用。
然后我用前面的脚本跑了一遍,发现进程 PID 3421(网关服务)的 fd 数高达 95000,而它的 nofile 限制是 102400,几乎是顶满的状态。再看 lsof -p 3421 | grep TCP | head,发现大量来自同一组客户端的连接处于 CLOSE_WAIT 状态。然后问题就浮出水面了:上游服务响应变慢,网关的 HTTP 连接池在等响应时没有设置超时,导致连接无法回收,fd 被大量占用,最终拖垮了网关。
当时做了三步处理:第一步,临时把网关服务的 LimitNOFILE 从 102400 调到 655350,让服务先恢复;第二步,排查上游服务为什么响应变慢,发现是一个数据库慢查询把连接池打满了;第三步,给 HTTP 客户端加上连接和读取超时,避免连接无限期挂起。
这个案例很好地说明了 fd 限制的三个特性:第一,限制是单进程维度的,一个进程耗尽,其他进程再空闲也没用;第二,fd 不只是文件,网络连接是最大的隐形消耗者;第三,调大 limit 只是止血,真正的病根在应用层的资源管理逻辑上。
配置完这些之后,我还让客户把监控加上,用 node_exporter 的 process_fds_open 指标持续跟踪每个进程的 fd 使用曲线,设置超过 80% 的告警阈值。从那次之后,这个系统再没出现过类似的故障。
用我自己的话说,文件描述符和进程数限制这套东西,属于“平时不起眼,出事要人命”的典型配置。花一个小时把原理摸清楚,把配置做对,能省下不知道多少加班的夜晚。以后再遇到 too many open files,不用慌,按着排查链路一步步走,基本都能快速定位到根因。
