Too many open files 不只是调大 ulimit:从 fd 泄漏到 systemd 限制的排查顺序
先说结论:报错分两种,处理方式完全不同
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。
- 点赞
- 收藏
- 关注作者
评论(0)