文件句柄数过多真会拖垮服务器?这些迹象你可能天天在碰

上周帮朋友查一台卡得像老牛拉车的测试服务器,top 看 CPU 和内存都挺清闲,偏偏 nginx 经常 502,日志里还反复蹦出 Too many open files。一查才发现,单个 Java 进程居然打开了 6 万多个文件句柄——比系统默认限制(1024)高出整整 58 倍。

文件句柄不是“文件”,是操作系统发的“临时通行证”

很多人一听“文件句柄”,下意识以为是打开了太多 txt 或 log 文件。其实它更像一张张“访问许可证”:每打开一个文件、建立一个 socket 连接、创建一个管道或信号量,内核都会分配一个句柄编号给你。Linux 把它叫 fd(file descriptor),本质是进程级资源槽位。

举个生活例子:就像小区门禁卡,你家有 3 张卡,但物业只给每户配了 10 个卡槽位。如果邻居偷偷复制了 20 张卡插满所有槽位,你再想刷卡进门就直接被拒——服务器也一样,句柄耗尽时,新连接进不来、新日志写不了、连 ls 都可能报错。

句柄爆满的典型症状,别总怪网卡或硬盘

遇到下面这些情况,先别急着重启服务:
- Web 服务突然大量 502/503,但负载不高
- tail -f app.log 报错 Cannot allocate memory
- ps aux 正常,但 lsof -p PID | wc -l 显示句柄数破万
- MySQL 报 Can't create thread,而实际内存充足
- 定时脚本某天起莫名失败,错误提示含 EMFILE

怎么查?三步摸清底细

先看系统全局上限:

cat /proc/sys/fs/file-max
再看当前已用总数:
cat /proc/sys/fs/file-nr
最后一招定位“罪魁祸首”:
lsof -nPi | awk '{print $2}' | sort | uniq -c | sort -nr | head -10
输出第一列是进程 PID,第二列就是它占的句柄数。我见过一个 Python 脚本因没关 requests.Session(),3 天干掉 4 万个 fd。

临时解法和长期对策

紧急时可提升单进程限制:

ulimit -n 65536
但治标不治本。真正要做的:
• Java 应用检查 FileInputStream 是否都套了 try-with-resources
• Nginx 在 http 块加 worker_rlimit_nofile 65535;
• Node.js 项目确认 fs.createReadStream() 后调用了 .close() 或流自然结束
• 定期用 lsof -i :8080 扫描长连接,揪出忘关的 TCP 连接

有次修完一个 Spring Boot 项目,把 @Value 注入的配置文件读取逻辑从构造器移到了懒加载方法里,句柄泄漏直接归零——有时候问题不在多高深,就在那一行漏掉的 close() 上。