上周五下午,客户报障说他们的一套订单系统突然开始报错,接口大面积超时。我们登录服务器一看,业务日志里密密麻麻全是同一行:

java.io.IOException: Too many open files

第一反应是"又是这个"。这毛病在 Linux 上太经典了,经典到很多老运维闭着眼都知道怎么改——ulimit 调大、/etc/security/limits.conf 加配置、重启服务。但我那天多留了个心眼,因为客户的系统是刚迁移到新机器上的,迁移前好好的,迁移后才开始报。

如果只是默认的 1024 句柄限制,为什么迁移前没事?

我查了迁移前的配置记录,发现老机器上 limits.conf 里明明写了 nofile 65535,但新机器上这个文件居然是空的。再一看,新机器用的不是 systemd 默认的 limits 加载路径——应用是通过 docker 起的,容器里的 ulimit 是 1024,根本没继承宿主机的 limits.conf。

这就是很多"Too many open files"排查里最坑的地方:你改了宿主机配置,但容器里的进程根本不读它。

正确的改法有两层:

第一层,宿主机。/etc/security/limits.conf 里加:

  • soft nofile 65535
  • hard nofile 65535

注意 systemd 管理的服务还要看 /etc/systemd/system.conf 和 user.conf 里的 DefaultLimitNOFILE,光改 limits.conf 对 systemd 服务不一定生效。

第二层,容器。docker run 的时候加 --ulimit nofile=65535:65535,或者 docker-compose 里写:

ulimits:
nofile:

soft: 65535
hard: 65535

如果是 k8s,就在 pod 的 resources 里配,或者改 kubelet 的 --system-reserved 相关参数。

改完还要验证,别改完就以为完事了。最直接的验证方式:

cat /proc//limits

看进程实际生效的 nofile 是多少。这个文件显示的是进程真正拿到的值,比任何配置文件都靠谱。我曾经遇到过改了配置忘了重启服务,结果进程还是 1024,排查了半天才发现是重启遗漏。

顺便说一句,除了文件句柄,还有两个兄弟坑经常一起出现:线程数限制(ulimit -u)和 TCP 连接数限制。特别是高并发场景,tomcat 或 nginx 的连接数上去了,文件句柄和线程数一起爆,报错里会混着 OutOfMemoryError 和 Unable to create new native thread,那时候别光盯着内存,先看看是不是进程的线程/句柄配额被默认值卡死了。

最后那个客户,我们给他宿主机和容器两层都改了,重启应用,观察了一个下午,报错再没出现过。他说你们排查挺快啊,我说不快,这个坑我踩过四五回了,第一次也是折腾到半夜。

Linux 里很多默认值,设计的时候是按"够用"来的,但生产环境从来不是"够用"就行。1024 这个数字,看着不起眼,对高并发的业务来说就是一道随时会炸的闸门。改之前先想清楚:这个进程到底要多少句柄?日志里有没有文件泄漏?如果应用本身就 leak fd,你把 ulimit 调到一百万也白搭,该修的代码还是得修。