Linux 上线前夕负载一高就报“too many open files”,要么就是进程明明没超,链接却打不开、线程起不来。这种问题在我排查过的系统里出现过太多次,而且大部分排查链都指向两个基础又容易被忽视的底子:文件描述符和进程数限制。理解它们为什么存在、怎样限制、如何修改,基本能把这类故障时间从“折腾一晚上”压缩到“十分钟定位”。
这篇文章从文件描述符的底层机制讲起,把进程数限制的两套规则说清楚,再用排查思路和实际调参命令走一遍完整流程。内容适用于从中级运维到偏应用开发的读者,也适合刚接手高并发服务、对系统资源配额没有完整把握的新手对照操作。
1. 文件描述符:进程手里那张“资源小票”
1.1 从 open 到 fd:一次回调背后的全部过程
文件描述符听起来很高深,实际上就是进程和内核之间的一个数字凭证。在 Linux 里,进程要访问文件、网络连接、管道、设备节点,并不是直接拿着路径去找硬件,而是先告诉内核“我想打开这个资源”,内核把资源登记到一张内部表里,然后返回一个非负整数给进程。这个整数就是文件描述符(file descriptor,后文简称 fd)。
以后进程再读写、关闭这块资源,只需要告诉内核“帮我操作 fd 3”,内核就会通过这个编号在自己的文件表中找到对应的资源。可以把它理解成去图书馆借书:你不可能把整座书库搬回家,图书馆给你一张借阅证,上面写了个编号,之后你凭编号就能借书、还书。进程的“借书”动作就是 open、socket、accept 这些系统调用,“还书”动作就是 close。
这里有个所有刚接触底层的人都会问的问题:为什么进程不能直接记住资源地址,非要内核转一手数字。原因在于安全隔离和资源统一管理。进程运行在用户态,如果每个进程都能拿到真实的内存地址或磁盘逻辑位置,那用户程序只要写错指针就可能直接搞坏别的进程甚至内核数据。让内核来分配 fd、维护引用关系,进程只需要关心“这个编号能不能用”,底层资源如何分配、回收全靠内核裁决。
从这个角度看,fd 值本身没有实际含义,只是进程私有的编号。它从 0 开始递增使用,常规情况下 0 是标准输入、1 是标准输出、2 是标准错误,从 3 往后的数字才是一般程序自己打开的 fd。所以排查时看到某个进程 fd 占用几千个,通常意味着它打开了很多文件或连接。
1.2 限制不是故意找麻烦:内核资源的真实成本
每个进程的 fd 表都有自己的大小上限,与此同时系统层面的总 fd 数也有上限,这两道限制分别由用户态配置和内核参数控制。
为什么要限制,而不是让程序无限打开资源?最直接的原因是:每打开一个文件描述符,内核就要分配一块内存来维护打开文件表项、文件偏移、状态标志等信息。操作系统内存不可能是无限大,如果不设限,一个漏洞百出的用户程序就能通过无限循环 open 把系统内存吃满,把同一台机器上的其他服务全部拖死。
内核里的双层结构在这里表现得非常清楚:
- 第一层是进程级文件描述符表,记录进程打开了哪些 fd;
- 第二层是系统级打开文件表(open file table),记录所有进程共享的打开文件信息;
- 第三层还有 inode 或 socket 等底层对象,关联真实磁盘文件或网络协议栈。
每次 open 一个新资源,至少要在上述数据结构里做内存分配和引用计数操作。fd 数量越多,内核在对文件表做遍历、查找、复制等操作时消耗的 CPU 也越多。
这就像是写字楼里的消防通道:平时没人在意,真遇到火警时大家都往那里挤,如果设计时不考虑总疏散人数上限,后果就是通道堵塞。限制 fd 和进程数不是给正常业务添堵,是为了避免极端情况下整个系统被拖垮。
1.3 哪些服务最容易卡死在文件描述符上
实际处理过的故障类型里,以下几类服务最容易和 fd 限制正面撞上:
- 高并发接入层服务,典型如 Nginx、HAProxy。每个客户端 TCP 连接至少占用一个 fd,连接数上万以后,很容易触碰到默认的 1024 用户级限制。
- 数据库服务,比如 MySQL、PostgreSQL、Redis。MySQL 的每个会话、每张表的缓存文件都可能占用 fd,遇到慢查询堆积、表数量巨大时 fd 占用会显著上升。
- 应用容器中的 Java 或 Node 服务。Java 的 NIO 模型会为每个 Socket 创建 fd,一旦连接没有正常关闭,就会出现 fd 泄漏,最终表现为“java.net.SocketException: Too many open files”。
- 依赖大量本地日志文件的业务进程。很多老项目喜欢用 FileAppender 按天切日志,长期不重启的进程会持续累积对历史日志文件的 fd,直到文件系统里的文件删了,fd 还被进程攥着不放。
这类问题的特点很明显:刚上线时一切正常,运行几小时后突然报错,重启应用又能恢复一段时间。表象是“进程不稳定”,根源是“资源配额不够”,只要摸清这条链路,排查方向基本不会跑偏。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 进程数限制:不止是“最多能起多少个进程”
2.1 进程、线程与内核任务之间的换算
进程数限制的表现形式比 fd 更隐蔽,因为它不只是简单地限制“ps 出来有几个进程”。Linux 里线程也被视为一个可调度的“任务”(task),所以内核的进程上限实际上限制的是线程总数,而不是传统意义上的进程数。
用户态调用 pthread_create 创建一个线程时,内核会分配一个新的 task_struct,占用一个进程描述符位置,PID 也照常分配。也就是说,一个 Java 应用运行后开了 200 个线程,在这些限制里它就是 200 个“任务”,不是 1 个任务。
用命令就能直观验证:ps -eLf 会列出所有线程,如果只统计 comm 相同的 PID,并不能反映内核真实压力。日常排查中看到“java”这个进程只占一行,但 /proc/sys/kernel/threads-max 和 pid_max 却能很快被打满,就是因为线程数没有引起足够重视。
所以后面配置“单用户最多能开多少进程”时,脑子里要想清楚:这个 UID 下运行的所有业务服务,所有线程池、JVM GC 线程、后台定时任务线程,统统要计入这个数字。
2.2 用户级和系统级两道关卡
进程数限制同样分两个维度控制。
第一个维度是用户级配额,通过 ulimit -u 或 /etc/security/limits.conf 里的 nproc 配置控制。它限制的是单个用户身份能够创建的最大任务数。在 Ubuntu、CentOS 等主流发行版里,默认值通常是 1024、4096 或者 65536,具体得看系统版本和 systemd 是否启用了 user limits。
第二个维度是内核级上限。内核参数 /proc/sys/kernel/pid_max 决定系统最多能分配多少个 PID,默认值在 32 位系统上通常是 32768,在 64 位系统上可能是 4194304。还有一个 /proc/sys/kernel/threads-max 表示内核全局线程/进程总数上限,计算时会结合内存大小给出一个默认值。
这两个维度的关系很像停车场的两级闸机:PID max 是整个停车场能停的总车位数,用户级 nproc 是持有月卡用户能同时停进场的车辆数。任何一级满了,后面要进来的车都只能排队等着或者直接被拒。
之前遇到过一次故障,top 里看到 CPU 和内存都还有富余,系统就是不愿意再创建新线程,报错内容是 pthread_create 返回 EAGAIN。用 ulimit -u 查用户限制,发现根本还没到;但查 /proc/sys/kernel/pid_max 时发现 PID 已经走到接近顶部的值,说明系统主要 pid 空间已经耗尽。简单调大 pid_max,同时清理了部分临时脚本发起的僵尸进程后,服务才恢复。
2.3 先算账再调参:给项目预留多少合适
调参前最好的做法是先手动估算一下项目峰值时需要的“任务总数”和“fd 总量”。不需要特别精确,但能帮你判断该改内核参数还是用户级配置。
举一个线上的实例。某个 Java 网关服务使用 4C8G 的容器规格,运行参数里设置了 200 个核心业务线程,加上 Netty 的 boss/worker 线程、JVM 后台线程、GC 线程,实测单实例线程数大约在 340 个左右。如果这台机器上同时跑 5 个这样的实例,需要的用户态任务数就是 340×5=1700 个。nproc 默认 1024 显然不够,一次扩容可能直接把进程打进 “Resource temporarily unavailable”。
fd 的估算逻辑类似:一个 HTTP 长连接进程,理论上每个连接吃一个 fd,JVM 本身还会打开一些配置文件、jar 包、日志文件。假设每实例峰值连接 2000 个,加上杂项 200 个,单实例 fd 需求 2200 个,5 个实例就是 11000 个。部署机通过 systemd 设置 LimitNOFILE=65535 完全够用,没必要盲目调到百万级。
很多团队习惯把所有服务全设成一个很大的统一值,用户限制直接写 1048576,内核参数也随便拉高。这样做不是不行,但会造成两个隐患:一是某进程 fd 泄漏时系统根本不会在早期暴露,最终把磁盘 inode、内存全部吃光才发现;二是内核维护超大 fd 表需要更多内存,低配机器上反而影响性能。正确的做法是“够用、留有余量、报警监控齐上”。
3. 排查实录:不到10分钟定位“too many open files”
3.1 第一步:确认当前限制和真实用量
故障发生时任你猜一万个原因都不如先查三层数据:当前 ulimit、当前已用 fd 数、当前内核总配额。
切换成出问题的用户身份执行 ulimit -n 能看到进程能开的最大 fd 数。默认值如果是 1024,而应用峰值连接是 3000,那问题大概率已经锁定。
bash复制# 查看当前登录环境的 soft/hard 限制
ulimit -n
ulimit -Hn
# 查看指定进程实际打开的 fd 数量
ls /proc/<PID>/fd | wc -l
# 更精确、更直观:用 lsof 按进程统计
lsof -p <PID> 2>/dev/null | wc -l
# 也可以按用户名聚合,直接找是谁在大量占 fd
lsof -u <USERNAME> 2>/dev/null | awk '{print $1, $2}' | sort | uniq -c | sort -rn | head
系统全局的 fd 情况要看 /proc/sys/fs/file-nr。这个文件包含三个数字:已分配 fd 数量、未使用但已分配的 fd 数量(通常是 0)、系统最大 fd 数量。
bash复制cat /proc/sys/fs/file-nr
# 3008 0 1048576
第一个数字如果已经非常接近第三个数字,说明系统级 fd 池也接近耗尽。这种情况即使把单个进程的 ulimit 调大也无济于事,因为全球配额堵死了。
另外可以顺手看下内存里的打开文件对象数量:
bash复制cat /proc/sys/fs/file-max
sysctl fs.file-nr
3.2 第二步:用 fd 追踪锁住“拿文件不还”的进程
确认系统确实出现大量 fd 占用后,下一步就是把“泄漏源”找出来。lsof 是排查 fd 泄漏最顺手的工具,结合进程号能直接列出这个进程打开的每一个文件、socket、管道。
bash复制# 只查看进程里打开的 TCP 连接数量
lsof -p <PID> 2>/dev/null | grep TCP | wc -l
# 只列出普通文件 fd,找日志文件占用异常
lsof -p <PID> 2>/dev/null | grep REG | head -30
# 查看某个 fd 对应的具体路径,确认是不是异常保持打开的状态
ls -l /proc/<PID>/fd/ | head -50
使用 /proc/
曾经遇到过一个 Java 定时任务服务,每天凌晨跑完批量任务后 fd 数不降反而继续涨。用 ls -l /proc 检查它的 fd 目录,发现大量指向 /tmp 目录的临时文件符号链接,但文件实际已经不存在,全部标记 deleted。原因就是代码里用临时文件做 Buffer 后只 close 了 FileOutputStream,没有把 FileInputStream 一起 close,导致底层的 fd 一直处于打开状态。把泄漏点修掉后,fd 总占用从四万降到两千左右。
3.3 第三步:从进程数超限到无法创建线程
如果故障现象不是连接打不开,而是程序启动时报 Resource temporarily unavailable,或者后台日志出现 pthread_create failed,就需要按进程数限制的链路排查。
先看用户级限制是否到了:
bash复制# 当前用户能创建的最大进程/线程数
ulimit -u
# 查看某个进程所属用户已经创建了多少 task
ps -eLf | grep <USERNAME> | wc -l
# 按实际用户统计 task 数量
ps -eLf | awk '{print $1}' | sort | uniq -c | sort -rn | head
再看内核级 PID 空间是否快被用尽:
bash复制# 当前系统分配的 PID 最大值
cat /proc/sys/kernel/pid_max
# 当前正在运行的进程/线程总数(不含内核线程则加 --no-header 过滤)
ps -eLf --no-header | wc -l
# 内核线程总数也可以从 /proc/loadavg 最后一个字段看,但这个数包含内核线程
cat /proc/loadavg
如果 ps 统计的总 task 数接近 pid_max 默认值 32768,而系统又开了大量线程,那 PID 耗尽就是主要原因。这时候除了调大 pid_max,更重要的是排查为什么会有这么多线程:是线程池没有设置最大上限,还是代码里每次请求都 new Thread、没有复用。
顺手补充一个排查技巧:查看进程内部线程时,不要只看 ps -ef 输出,因为普通 ps 默认只显示进程级视角。要用 ps -eLf 或 top 里按 H 键切换线程视图,才能准确判断单进程真正占用了多少 task。
bash复制# 查看一个进程开了多少线程
ls /proc/<PID>/task | wc -l
# 或使用 ps -T -p
ps -T -p <PID>
3.4 线上故障检查清单
结合上面的诊断过程,整理一套现场可以照着执行的检查顺序,能显著减少排查弯路。
| 序号 | 操作命令 | 作用 |
|---|---|---|
| 1 | ulimit -n、ulimit -u | 确认当前运行环境用户级配额 |
| 2 | cat /proc/sys/fs/file-nr | 确认系统级 fd 总池余量 |
| 3 | ls /proc/ |
确认单进程 fd 占用峰值 |
| 4 | lsof -p |
排查连接型 fd 占用 |
| 5 | ps -eLf | grep |
统计用户级线程/进程总量 |
| 6 | cat /proc/sys/kernel/pid_max | 确认系统 PID 总空间是否耗尽 |
| 7 | ls /proc/ |
确认单进程线程数是否异常 |
这套检查做得足够快,能在五分钟内判断故障属于“单个进程 fd 泄漏”“用户配额不足”还是“整个系统 PID 耗尽”。后面再做针对性调优,就不会盲改参数。
4. 调优实操:从登录会话到内核参数一次调到位
4.1 修改用户级限制:limits.conf 的 soft 与 hard
在传统 SysV init 系统或直接 SSH 登录的场景下,针对单个用户的资源限制主要通过 /etc/security/limits.conf 配置,它依赖 PAM 模块在登录时加载。
格式包含四个字段:域名、类型、资源项、值。域可以用用户名、组名(前面加 @)或通配符 *。类型分 soft 和 hard,soft 是当前会话可直接生效的软上限,hard 是最高可调硬上限。普通用户可以把 soft 调高但不能超过 hard,root 可以随意调整。
例如要对 app 用户放开文件和进程限制:
ini复制app soft nofile 65535
app hard nofile 65535
app soft nproc 65535
app hard nproc 65535
配置生效后,切换用户重登即可通过 ulimit -n 验证。常用参数里还需要注意 file size 限制,有些服务器默认 core file size 是 0,程序需要生成 core dump 时也要在 limits.conf 里配置,但和高并发无关,这里不展开。
这里有一个隐藏的重点:如果某个服务是由 systemd 管理的,修改 limits.conf 往往不会对它生效。因为 systemd 直接管理进程时,PAM 登录流程没有参与,systemd 有自己的 service 级限制语法。很多运维改了 /etc/security/limits.conf 然后重启服务发现依旧报错,就是这个原因。
4.2 systemd 服务别忘了单独配置
systemd 管理的服务要在 unit 文件的 [Service] 段落里设置 LimitNOFILE、LimitNPROC,语法比 limits.conf 更直观:
ini复制[Service]
LimitNOFILE=65535
LimitNPROC=65535
修改后需要执行 systemctl daemon-reload,然后重启对应服务。注意 LimitNPROC 在 systemd 里的含义是限制“服务进程能创建的进程数量”,在某些版本中,它可以同时限制进程 fork 出的线程数量,所以要测量实际需求后再写。
为了统一管理,很多团队会在 /etc/systemd/system.conf 或 /etc/systemd/user.conf 里设置全局默认值:
ini复制[Manager]
DefaultLimitNOFILE=65535
DefaultLimitNPROC=65535
设置完同样要 daemon-reload,并且新启动的服务才会继承。之前踩过一个坑:改了 system.conf 后直接 systemctl restart 服务,服务进程的 LimitNOFILE 还是 1024,后来发现是服务 unit 文件里没有显式继承,必须用 systemctl show
4.3 内核级参数修改和持久化
单个用户限制调整后,系统级资源池如果也偏小,还需要修改内核参数。常见需要调整的包括 fs.file-max、fs.file-nr(只读)、kernel.pid_max、kernel.threads-max、vm.max_map_count 等。
临时修改可以直接用 sysctl -w:
bash复制sysctl -w fs.file-max=2097152
sysctl -w kernel.pid_max=4194304
sysctl -w vm.max_map_count=262144
持久化修改需要写入 /etc/sysctl.conf 或 /etc/sysctl.d/ 下的独立配置文件:
bash复制# /etc/sysctl.d/99-custom-limits.conf
fs.file-max = 2097152
kernel.pid_max = 4194304
kernel.threads-max = 515396
vm.max_map_count = 262144
写完执行 sysctl --system 让配置生效。对于 vm.max_map_count 这个参数,很多做 Elasticsearch、ClickHouse 或 Redis 混合部署的团队会遇到“max number of threads”之外的问题:进程启动 mmap 区域数量不足导致启动失败,这个值在 ELK 官方文档里一般建议至少 262144。
为什么这些参数要分开管?fs.file-max 控制着整个内核能打开的文件总量,vm.max_map_count 控制每个进程能创建的内存映射区域数量,threads-max 控制内核全局的线程总数,pid_max 则直接影响 PID 分配上限。四者之间没有强耦合,故障时要针对实际指标选择调整对象,别一上来就全改。
4.4 不同服务的配置示例与验证方法
不同中间件对资源配额的诉求不太一样,下面给几个可以直接参考的配置经验,均为实际部署验证过的组合。
Nginx 作为反向代理,常见调法是同时提高 worker 进程的 fd 上限,确保每个 worker 能承接大量连接。systemd 管理时:
ini复制[Service]
LimitNOFILE=1024000
LimitNPROC=102400
在 nginx.conf 里还需要显式写上 worker_rlimit_nofile 1024000,让 worker 进程自己把这个值设置上去。如果只在 systemd 里调大而 nginx 配置没写,部分版本下 worker 仍然不会继承过高的 fd 限制。
MySQL 服务一般通过 systemd 或 mysqld_safe 启动。systemd 下建议至少设置:
ini复制[Service]
LimitNOFILE=65535
LimitNPROC=65535
配合 MySQL 参数 open_files_limit = 65535,table_open_cache = 4000。只调系统侧不调 MySQL 内部参数,实例文件 cache 还是会被 MySQL 自身的限制卡住。
Java 服务如果用 systemd 托管,常见的推荐值:
ini复制[Service]
LimitNOFILE=65535
LimitNPROC=65535
TasksMax=infinity
TasksMax 是 systemd 特有的 cgroup 任务数限制,如果不设置 infinty(或者一个大值),即使系统 nproc 够用,systemd 的 cgroup pids 控制器也可能在任务数超过默认值后阻止新线程启动。这是很多 Java 容器“线程数怎么都上不去”的隐藏元凶之一。
配置完成后验证最直接的方法是找到实际进程:
bash复制# 找到服务进程 PID
systemctl status nginx
# 或者
pgrep -f nginx
# 查看进程实际生效的 fd 和进程数限制
cat /proc/<PID>/limits
# 重点看 Max open files 与 Max processes 两行
/proc/
5. 那些年踩过的坑:改完不生效的七个原因
5.1 limits.conf 和 systemd 的“双轨制”坑
前面已经提过 systemd 不走 /etc/security/limits.conf 的问题,这里再强调一次,因为实际踩过太多次。两种管理方式本质上是两套体系:传统登录会话由 PAM 读取 limits.conf,而 systemd service 在启动进程时根据 unit 的 Limit 指令设置 rlimit。
解决方法的优先级也很明确:
- 先确认服务由谁启动:ps -p
-o comm= 看不出什么,但 systemctl status 能告诉你管理归属。 - systemd 管理的服务只改 unit 文件,SSH 登录的用户才改 limits.conf。
- 登录会话和 systemd 都改,保持配置一致性,避免下次排查时产生误导。
修改 /etc/security/limits.conf 后,不需要重启机器,新登录会话会加载。但个别发行版(特别是较新的 Ubuntu 版本)即使在同一用户下执行 su - 也可能不会重新读取,需要完全断开 SSH 再连接一次,或者在 /etc/pam.d/common-session 里确认有 pam_limits.so 模块。
5.2 容器环境下的特殊姿势
容器化部署下,资源限制又多了一层:容器 runtime(Docker、containerd)会根据镜像配置或运行时参数设置 limit。在宿主机上调大 ulimit,容器内如果不继承,业务照样会被卡住。
Docker 启动容器时可以用 --ulimit 参数:
bash复制docker run --ulimit nofile=65535:65535 --ulimit nproc=65535:65535 ...
如果使用 docker-compose,对应写法是:
yaml复制services:
app:
ulimits:
nofile:
soft: 65535
hard: 65535
nproc:
soft: 65535
hard: 65535
Kubernetes 场景更复杂,容器 runtime 的默认 ulimit 可能被 kubelet 或 CRI 的配置影响,排查时要进入容器内部执行 cat /proc/
另外 Kubernetes 的 Pod 如果设置了资源 requests/limits,CPU 和内存限制是按 cgroup 实现的,和 ulimit 是两个维度。线程数限制很多时候会通过 cgroup pids.max 产生作用,这时报错可能表现为 “cannot create thread: Resource temporarily unavailable”,需要查看容器对应的 cgroup pids 文件而不是改内核 pid_max:
bash复制cat /sys/fs/cgroup/pids/pids.max
注意新版本 cgroup v2 的路径是 /sys/fs/cgroup/pids.max,需要根据系统实际挂载路径适配。
5.3 改得过大也会带来新问题
资源限制不是越大越好。我见过有团队把所有服务的 nproc 都设成 unlimited,文件描述符上限设成几百万,最后系统内存被内核结构体逐渐吃满,反而加剧了故障。
内核维护 fd 表需要内存,维护 task_struct 也需要内存。每个线程默认栈大小通常 8MB,但这个并不直接占用物理内存,而是虚拟地址空间。大量线程如果都真正使用了栈空间,不仅内存压力上升,频繁的上下文切换也会让 CPU 负载飙升。
从性价比角度出发,给服务设置一个“合理上限+监控阈值”比“无限上限”更稳妥。例如 Java 服务线程数合理上限是 1000,nproc 可以设 4096,留出一定余量,但不要设成 unlimited。fd 上限依据连接数估算,普通服务开 65535 已经覆盖绝大多数场景,不需要盲目设置千万级。
5.4 排查“不能创建线程”时的隐藏逻辑
如果系统真正进入“无法创建线程”状态,除了 pid_max、RLIMIT_NPROC、cgroup pids.max,还有另一个容易忽略的数据:内核内存不足以分配新的 task_struct。
创建线程时内核需要为每个新的 task_struct 分配内存,如果内核内存不足,即使 PID 空间还有余量,pthread_create 依然会失败。这种问题在排查时经常被误判为资源限制,实际是 cgroup memory 限制或内核 slab 占用过高导致。
遇到这类故障建议按以下顺序排查:
bash复制# 查看 cgroup 内存限制(容器环境)
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
cat /sys/fs/cgroup/memory/memory.usage_in_bytes
# 查看系统剩余内存
free -h
# 查看内核 slab 占用是否过高
cat /proc/meminfo | grep Slab
5.5 几个提升排障效率的小习惯
把经常用到的命令封装成脚本,能大幅减少排障时敲键盘的负担。我自己常用的一个小脚本思路:
bash复制#!/bin/bash
# 用法: ./fdstat.sh <PID>
PID=$1
echo "=== 进程限制 ==="
cat /proc/$PID/limits | grep -E "Max open files|Max processes"
echo "=== 文件描述符 ==="
echo "fd 总数: $(ls /proc/$PID/fd 2>/dev/null | wc -l)"
echo "socket 数: $(ls -l /proc/$PID/fd 2>/dev/null | grep socket | wc -l)"
echo "管道数: $(ls -l /proc/$PID/fd 2>/dev/null | grep pipe | wc -l)"
echo "=== 线程数 ==="
echo "task 总数: $(ls /proc/$PID/task 2>/dev/null | wc -l)"
顺手还可以把当前系统所有进程按 fd 数量从高到低排序:
bash复制for pid in $(ls /proc | grep -E '^[0-9]+$'); do
count=$(ls /proc/$pid/fd 2>/dev/null | wc -l)
if [ "$count" -gt 1000 ]; then
echo "$pid $count $(cat /proc/$pid/comm 2>/dev/null)"
fi
done | sort -k2 -rn | head -20
配合 Prometheus 的 node_exporter 采集 process_fds_open_bytes、process_max_fds 等指标,还能在故障发生前提前发现 fd 膨胀趋势。node_exporter 默认采集的 process 指标有限,如果想按进程监控,可以在 textfile collector 里加入自定义脚本输出。
最后再说一点个人体会。文件描述符和进程数限制本身不是特别高深的知识,但它牵涉的系统面很广,从用户态配置到内核参数、从传统登录到 systemd 再到容器,每一层都可能成为拦截点。处理这类问题时,别急着去改参数——先问三个问题:当前真实用量是多少?限制卡在哪一层?按业务量算需要多少?把这三个问题回答清楚,调整只是顺手的事。我见过太多人把 all 服务的 fd 调到无限大,最后故障没解决,反而把内存吃满,这比不过分调优的代价要大得多。
