上周帮朋友查一台卡得像老牛拉车的测试服务器,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() 上。