文章深入分析了Linux系统中线上服务因文件句柄泄露导致的崩溃问题,揭示了通过`limits.conf`调优`ulimit`时的常见错误和解决方案。作者详细描述了系统限制不适用于systemd管理服务的原因,并提供了通过修改unit文件和全局配置文件解决此问题的方法,避免了因误操作而引发的其他问题。
周五下午刚过四点,业务监控突然报警,网关层开始大面积返回 502。登上服务器看后端日志,控制台密密麻麻全是 java.io.IOException: Too many open files,伴随着下游 Go 服务报出来的 accept tcp [::]:8080: accept4: too many open files in system。
这时候所有的 TCP 新建连接全部被操作系统直接拒绝,curl 本地端口也握不上手。
遇到这个报错,很多人第一反应往往是在网上搜个命令:打开 /etc/security/limits.conf,加上 * soft nofile 65535,然后觉得完事了。
但真正在生产环境排查过几次就会发现,事情远没这么简单。改完配置服务依然起不来、重启后依然显示限制 1024、盲目把数值改到几百万导致机器无法登录、或者是代码里连接没关把 65535 活活耗光……这些坑我都踩过一遍。
把排查过程里的关键节点和容易混淆的地方理清楚,线上再遇到就能直接定位。
坑一:改了 limits.conf,systemd 服务依然限制 1024
这是排查 Too many open files 最常见的翻车现场。
很多人跑到 /etc/security/limits.conf 里加上两行:
* soft nofile 65535
* hard nofile 65535
重开 SSH 终端,敲一下命令验证:
ulimit -n
# 输出:65535
看着很正常,结果重启应用服务,业务跑了一阵子又挂了,日志依然报句柄不足。
为什么会这样?
因为 /etc/security/limits.conf 是 PAM 认证机制(pam_limits.so)的配置文件。只有通过 PAM 登录的会话(比如 SSH 登录、控制台登录、su - username 切换用户),才会加载这套限制。
而现在的 Linux 发行版(CentOS 7/8/9、Ubuntu 18.04+、Debian 等),后台常驻服务几乎都是交给 systemd 统一管理的。systemd 启动的守护进程是直接由 PID 1 fork 出来的,压根不走 PAM 认证链,自然完全无视 limits.conf 里的设置。
验证某个正在运行的进程实际句柄上限,绝不能只看当前终端的 ulimit -n,而是要直接查内核 proc 文件系统:
# 找到出问题的服务进程 PID
pidof my-service
# 查看该进程真实生效的资源限制
cat /proc/<PID>/limits | grep "Max open files"
输出大概长这样:
Max open files 1024 524288 files
第一列是软限制(Soft Limit),第二列是硬限制(Hard Limit)。你会发现软限制死死卡在 1024,只要进程打开的 socket 和文件总数超过 1024,立马抛错。
正确解法
给 systemd 管理的服务增加句柄上限,有两个位置:
1. 针对单个服务(推荐)
修改服务的 unit 文件,或者通过 systemctl edit <service_name> 增加覆盖配置:
[Unit]
Description=My Backend Service
[Service]
Type=simple
ExecStart=/opt/app/bin/server
# 显式指定该进程的文件句柄限制
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
保存后执行:
systemctl daemon-reload
systemctl restart <service_name>
再去查 /proc/<PID>/limits,这时候 Max open files 就会变成 65535。
2. 全局 systemd 默认值
如果这台服务器上跑了大量容器或自建服务,想让 systemd 默认给所有 unit 比较大的限制,可以修改 /etc/systemd/system.conf 和 /etc/systemd/user.conf:
[Manager]
DefaultLimitNOFILE=65535:524288
改完后执行 systemctl daemon-reexec 让 systemd 重新加载主配置,后续新启动的服务就会继承这个默认值。
坑二:搞混三个层级的限制,修改数值直接超纲
Linux 对文件句柄的限制其实分了三层,从上到下互相约束。很多运维同学只知道 ulimit -n,遇到报错就瞎填一个 1000000,结果直接被内核拦住或者导致异常。
这三层关系如下:
1. 系统全局限制:fs.file-max
这是整个操作系统内核允许所有进程打开的文件句柄总数上限。就算单个进程只占了几千个,如果整台机器所有服务加起来把 fs.file-max 吃完了,系统也会报 Too many open files in system。
查看当前系统上限:
cat /proc/sys/fs/file-max
# 或者
sysctl fs.file-max
查看全系统当前使用了多少句柄:
cat /proc/sys/fs/file-nr
# 输出三个数字,例如:
# 3200 0 2097152
这三个数字分别代表:
- 第 1 个:系统当前已分配并使用的文件句柄数。
- 第 2 个:已分配但目前处于空闲的句柄数(Linux 2.6 以后通常保持为 0)。
- 第 3 个:系统允许的最大文件句柄数(对应
fs.file-max)。
2. 单进程上限天花板:fs.nr_open
这是内核针对单个进程允许设置的 nofile 最大上限,默认通常是 1048576(1024 * 1024)。
你在 /etc/security/limits.conf 或 systemd 的 LimitNOFILE 里填的值,绝对不能超过 fs.nr_open。如果强行把 nofile 设成 2000000,用户再次通过 SSH 登录时,PAM 模块解析配置出错,会直接把你拒之门外,SSH 连都连不上。
3. 用户及进程级限制:nofile(ulimit -n)
这是特定用户或特定进程能打开的句柄数。软限制可以由普通用户在运行时调大(只要不超过硬限制),硬限制只能由 root 用户调整。
生产推荐配置组合
在高并发或者网关服务器上,建议在 /etc/sysctl.conf 里统一调优内核参数:
# 全局句柄总数上限(按机器内存来,16G/32G 机器给 200 万足够)
fs.file-max = 2097152
# 单进程能够设置的最大打开句柄数
fs.nr_open = 1048576
修改后执行 sysctl -p 生效,然后再去调整应用的 LimitNOFILE=65535 或者 131072,层层递进才不会冲突。
坑三:盲目调大限制,掩盖真正的句柄泄露
很多线上事故里,服务原本只需要几百个长连接,某天突然把 65535 甚至更高限制撑爆。
这时候如果只是机械地把 LimitNOFILE 改成 200000 重新拉起,往往跑两三天又爆了。因为这不是真实的业务量增长,而是代码发生了文件句柄泄露。
在 Linux 世界里,一切皆文件。打开一个本地日志文件是 fd,建立一个 TCP 连接是 fd,一个 UNIX Domain Socket 是 fd,管道 pipe 也是 fd。
当句柄数异常暴涨时,第一步不是调参数,而是抓出现场证据。
1. 确认当前进程实时占用了多少个 fd
ls -l /proc/<PID>/fd | wc -l
如果这个数字逼近了 cat /proc/<PID>/limits 里的限制值,说明这个进程离崩溃只有一步之遥。
2. 分类统计到底是什么资源占满了句柄
用 lsof 按照文件类型快速分组统计:
lsof -p <PID> | awk '{print $5}' | sort | uniq -c | sort -rn
看输出里的主导类型是哪一个:
- 如果排在前面的是
IPv4/IPv6/sock:说明大量 TCP 连接没有释放。 - 如果排在前面的是
REG:说明普通磁盘文件被打开后没有调用 close。 - 如果排在前面的是
FIFO或pipe:说明进程间通信的管道未回收。
3. 常见泄露根因排查
场景 A:网络连接死在 CLOSE_WAIT 状态
用 ss 或 netstat 统计该进程的网络状态:
ss -antp | grep "pid=<PID>," | awk '{print $1}' | sort | uniq -c
如果看到成千上万个 CLOSE_WAIT,问题基本就坐实了。
CLOSE_WAIT 表示远端客户端或下游服务已经主动发起断开(发了 FIN 包),Linux 内核回复了 ACK,正在等待本地应用程序调用 socket.close()。
如果应用代码拿到了连接断开的通知,却因为异常处理逻辑疏漏(比如解析 JSON 抛了 RuntimeException,导致后续的 client.close() 或 response.close() 被跳过),这个连接就会永久卡在 CLOSE_WAIT。每个卡住的 socket 都会长期占着一个文件句柄,直到堆满。
场景 B:读写临时文件忘记关闭流
在做 Excel 导出、图片转码、日志写入或者临时文件解压的业务里,经常有人写出这样的代码:
// 错误示范:抛异常时无法保证流被关闭
FileInputStream fis = new FileInputStream(file);
processData(fis); // 如果这里抛错,下面的 close 永远走不到
fis.close();
即使把磁盘上的文件 rm 删掉了,如果打开文件的进程还没有执行 close,Linux 也只是把文件从目录树里 unlink,inode 和数据块依然挂在进程的 fd 表里。
这时候运行 lsof -p <PID> | grep deleted,就能抓到一长串标记着 (deleted) 的僵尸文件,每一个都在白白消耗句柄配额。
排查出泄露原因后,在代码里老老实实用 try-with-resources 或保证 finally 块显式关闭,这才是治本的做法。
线上应急排查命令速查
遇到线上报警 Too many open files 时,按顺序执行这组命令定位问题:
# 1. 确认哪个进程的句柄耗尽
for pid in $(pgrep -d ' ' -f "java|nginx|node|python|go"); do
echo "PID: $pid, FDs: $(ls -1 /proc/$pid/fd 2>/dev/null | wc -l)"
done | sort -k4 -rn | head -n 5
# 2. 查看目标进程实际生效的句柄限制
cat /proc/<PID>/limits | grep "Max open files"
# 3. 检查系统整体句柄使用水位
cat /proc/sys/fs/file-nr
# 4. 分析目标进程句柄泄露的具体类型
lsof -p <PID> | awk '{print $5}' | sort | uniq -c | sort -rn | head -n 10
# 5. 如果是网络连接泄露,查看具体处于什么状态、连向哪个 IP
ss -antp | grep "pid=<PID>," | head -n 20
先确认生效限制是否偏小,再确认是否存在未释放的连接与已删除文件句柄。分清 systemd 和 limits.conf 的边界,把系统级参数和代码泄露理顺,以后再碰到类似的句柄打满报警,五分钟就能完成定界。
文章标题:线上服务突发 Too many open files 崩溃?排查 Linux 文件句柄泄露与 ulimit 调优我踩过的几个坑
文章链接:https://llbbs.cn/jishujaocheng/204.html
本站文章均为原创,未经授权请勿用于任何商业用途
评论一下?