上周三凌晨两点,值班手机响了:一个跑了三个月的订单查询服务开始返回 500。日志里翻到最后,是一句 Too many open files。我第一反应是磁盘满了,去 df 看了看,还有 80% 空着;又去看内存,也正常。再看连接数,netstat 上不过几百条Established。于是我盯着那句报错看了十分钟它说的明明是「文件描述符」,但我脑子里全是「文件」两个字。
后来才反应过来:这个服务是个长连接网关,每个进来的 HTTP 请求都会跟下游的支付服务建一条 socket,用完 close 掉。问题出在一条异常分支上下游返回了非预期状态码,代码里的 return 直接跳过了 conn.close()。正常流量走不到那条路,只有下游抖动时才走到。也就是说,内存没涨、连接数没涨、CPU 也没涨,唯独 fd 一直在涨。
如果你也在排查这类「跑几天就崩」的服务,或者需要一台配置合适的机器先把环境跑起来,可以先看看这几家的报价和技术说明:zgovps(全球云服务器)
很多人第一次接触 fd 会以为它是「打开的文件」。其实在 Linux 内核里,文件描述符就是一个整数进程打开文件、socket、管道、设备时,内核在进程的文件表里占一个坑,把这个坑的编号返回给应用层。0、1、2 是标准输入输出错误,3 从这里开始往后排。

关键在于:这个编号是进程级的、并且是有限的。你的服务能同时打开多少个文件,取决于两个东西的较小值:内核参数 fs.file-max(全系统上限)和进程的 ulimit -n(单个进程上限)。绝大多数情况下,是后者卡住你。默认很多发行版是 1024,也就是说你的进程总共只有 1024 个 fd 可用,扣掉标准输入输出、打开的配置文件、日志文件、已经建立的连接,剩下的能用来开新 socket 的可能只有几百个。
这就是「跑得好好的,怎么突然就 Too many open files」的来源:fd 上限是一个固定的池子,平时用不完看不出问题,一旦某个资源不再被释放,池子就会被慢慢填满,而且不会自动回收。进程不重启,它就一直耗着。
第一种,也是最常见的一种:忘了 close。每次请求建一条连接,异常路径上没关。表现是 fd 数只涨不跌,跟请求量成正比。我那次是每分钟涨十几个,跑满七十二小时刚好把 1024 撑破三个月的服务没人重启过,所以故障点跟上线时间一点关系都没有,这才是最坑的地方。

第二种是 close 了但没 close 干净。比如 close 的是 socket 的文件描述符,但连接被放进了某个连接池的 map 里没删;或者对端已经 FIN 回来了,你这边没读到 EOF 也没去 close,socket 一直挂在 TIME_WAIT 或者 CLOSE_WAIT。这种情况更隐蔽,因为你的代码「明明关了」。
顺着说下 CLOSE_WAIT:它出现得多,基本就等于「应用没去 close」。对端已经主动断开并且完成了四次挥手,你这边收到了 FIN 却没调 close,内核就会把这个连接放在 CLOSE_WAIT 状态等着你。CLOSE_WAIT 一旦开始堆积,不用查代码也知道 fd 在漏。而 TIME_WAIT 堆积是正常的,那是对端没关、留给你回收的,不用紧张。
第一步,看数量。ls /proc/<pid>/fd | wc -l,这是最快的判断方法。如果这个数字稳定在几百,而你的 ulimit -n 是 1024,那它迟早要出事。隔十分钟再看一次,数字在涨,就是泄漏的铁证。

第二步,看类型。lsof -p <pid> 会列出这个进程打开的所有 fd。重点看第四列的 TYPE 字段,如果看到大量 IPv4、IPv6、unix,那就锁定了 socket 没关。用 lsof -p <pid> | grep TCP 可以只看网络连接。
第三步,看状态。ss -tanp | grep <pid> 把连接按状态统计出来:ESTAB 有多少、CLOSE_WAIT 有多少、TIME_WAIT 有多少,一目了然。如果 CLOSE_WAIT 数量在涨,基本可以直接下判断了。
把这三个命令串起来就是完整的排查链:先 ls /proc/<pid>/fd | wc -l 确认泄漏,再 ss 看是不是 CLOSE_WAIT,最后 lsof 定位到具体哪几行代码在漏。如果你的服务需要长期稳定跑,同时承接不少长连接和大并发,那机器本身的资源余量和网络质量也得跟上,liteserver(低延迟高性能云服务器) 这类主打网络质量的商家可以放进对比名单里,fd 泄漏解决了,别让带宽和延迟再拖后腿。
第一,把 fd 上限提上去,但别当成解决方案。ulimit -n 65535 或者在 systemd 的 LimitNOFILE= 里写上,确实能撑更久,但它是把池子变大,不是让池子不漏。漏的东西只会让崩溃来得晚一点。如果确实有大量长连接(比如网关、代理、推送服务),提高上限是合理的配套动作,但前提是泄漏本身已经修了。

第二,给 fd 使用率加监控。在监控里加一条 ls /proc/<pid>/fd | wc -l 的曲线,接近上限的 70% 就告警。等到真的报 Too many open files 才处理,通常已经是线上事故了。我现在的做法是每分钟采一次,配一条阈值告警,半年下来再没被这个问题半夜叫醒过。
第三,用语言自带的机制兜底。Java 的 try-with-resources、Go 的 defer close、Python 的 with context manager,都能保证异常路径上也会关闭。写代码的时候把「关闭」和「创建」绑在一起,比事后排查便宜太多了。我们那次改完之后,还顺手把下游的异常状态码补全了那条分支本来就不该被跳过。
回头看那三个小时,其实卡住我的不是技术,是惯性思维:一看到 open files 就去找磁盘和内存。fd 这个词骗了我很久。它的含义其实非常直白:你的进程想开的东西,开不动了。至于是谁没关,/proc/<pid>/fd 里那个数字会告诉你答案。