Too many open files 不只是调大 ulimit:从 fd 泄漏到 systemd 限制的排查顺序

举报
yd_217417492 发表于 2026/10/29 07:42:25 2026/10/29
【摘要】 服务突然报 Too many open files,第一反应往往是调大 ulimit,但改完重启又复发。本文按「系统总量、进程限制、fd 去向、泄漏验证」四步排查,并说明 systemd 服务为什么不吃 shell 的 ulimit。

先说结论:报错分两种,处理方式完全不同

Too many open files(错误码 EMFILE)通常是单个进程打开的 fd 达到了自己的 soft limit;而 ENFILE 才是整个系统到达 fs.file-max。前者占绝大多数,后者一般出现在 fd 总量被压得很低或者被大量小进程吃满的机器上。先分清是哪一种,再决定要不要动内核参数,否则很容易出现「调大了还是报错」的情况。

还有一个容易误判的现象:进程已经打不开新文件了,但现有连接仍然工作正常。因为限制只卡新建,不卡已打开的 fd。所以监控上看流量还在,日志里却开始刷 accept 失败。

第一步:看清系统和进程各自的限制

先看系统层面已经用了多少:

cat /proc/sys/fs/file-nr
# 已分配  已释放但可复用  上限
# 2432    0              9223372036854775807

再确认目标进程自己的 soft / hard limit。不要只看当前 shell 的 ulimit -n,它和服务的实际限制没有关系:

cat /proc/<pid>/limits | grep -i files
# Max open files  1024  4096  files

如果这里显示 1024,而业务确实需要更多并发连接,那问题就在限制上;如果显示 65535 却仍然报错,就要怀疑泄漏,而不是继续加数字。

第二步:fd 到底被谁拿走了

先数一下总量,再按类型拆开:

ls /proc/<pid>/fd | wc -l
lsof -p <pid> | awk '{print $5}' | sort | uniq -c | sort -rn | head
# 常见类型:REG 普通文件、IPv4/IPv6 套接字、PIPE、anon_inode

如果 IPv4 套接字占了大头,基本就是连接没关或者连接池没有上限;如果 REG 很多,重点看是不是每次请求都重新打开日志、配置或模板文件;如果出现大量 PIPE,要检查子进程和管道有没有被正确回收。

另外别忘了已删除但仍在被占用的文件。它们不会出现在 du 里,却实实在在占着 fd 和磁盘:

lsof +L1 | head -20

这类文件通常是日志轮转后没有通知进程重新打开,copytruncate 能规避一部分问题,但更稳妥的做法是让进程支持 SIGHUP 重开日志。

第三步:区分「并发高」和「真泄漏」

并发高是正常的,泄漏的特征是连接结束后 fd 数量不回落。最简单的验证方式是每分钟采一次样,观察一段时间:

for i in $(seq 1 30); do
  printf '%s %s\n' "$(date +%H:%M:%S)" "$(ls /proc/<pid>/fd | wc -l)"
  sleep 60
done

如果曲线随请求上涨、空闲后也不下降,就是泄漏。定位到具体代码时,重点看三类地方:HTTP 客户端返回的 response 有没有关闭、异常分支里有没有跳过 close()、连接池的 max_connections 是不是没有设置上限。Python 里用 with 或 try/finally 包住资源,比事后补 close() 可靠得多。

还有一种隐蔽情况:短连接频繁创建,fd 数量看起来不高,但 TIME_WAIT 把本地端口耗尽了。这不是 fd 限制问题,ss -s 能看到大量 timewait,处理方向是启用长连接或连接复用,而不是继续调 ulimit。

第四步:systemd 服务不吃 shell 的 ulimit

这是改完限制仍然不生效的最常见原因。通过 systemd 启动的服务继承的是 unit 里的 LimitNOFILE,在 shell 里执行 ulimit -n 65535 只影响当前 shell 及其子进程。

systemctl show -p LimitNOFILE myservice
# LimitNOFILE=1024

正确的改法是写 override,而不是直接编辑发行版提供的 unit 文件:

systemctl edit myservice
[Service]
LimitNOFILE=65535

保存后执行 systemctl daemon-reload && systemctl restart myservice,再用 /proc/<pid>/limits 复核。注意 soft limit 不能超过 hard limit;如果 unit 里只写了 soft,hard 仍然是旧值,重启后可能被打回去。

内核参数要不要动

fs.file-max 是系统级上限,现代发行版默认值已经很大,普通业务几乎碰不到。真正需要关注的是 fs.nr_open,它限制单个进程能设置的最大 fd 数,LimitNOFILE 不能超过它。调整内核参数属于兜底,不是修复泄漏的手段:泄漏的进程就算把上限调到十万,也只是把故障时间往后推。

收尾:把「当前 fd 数」变成监控项

与其等报错,不如提前告警。采集 /proc/<pid>/fd 的数量,和 /proc/<pid>/limits 里的 soft limit 做比值,超过 70% 告警,比单纯看 CPU 和内存更早发现连接泄漏。修复之后也要观察一整天,确认空闲时 fd 能回落到基线,否则只是把问题藏起来了。

总结一下顺序:先用 file-nr 和 /proc/<pid>/limits 分清是系统限制还是进程限制,再用 lsof 看 fd 去向,用采样判断是并发还是泄漏,最后才考虑调大限制。多数时候,真正要改的是代码里的 close(),而不是 ulimit。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。