确认文件描述符耗尽需先执行ulimit -n和ls /proc/1/fd | wc -l比对使用率是否≥90%,再结合EMFILE等日志报错锁定;须同步检查dockerd自身限制,避免守护进程也卡住。

崩溃不是因为代码写错了,而是系统拒绝分配新文件描述符——Too many open files 是资源耗尽的明确信号,不是异常,是内核在“关门”。临时调高 ulimit -n 只能延缓崩溃,不查泄漏源等于给漏水的船不停止进水,只拼命舀水。
怎么确认真是 fd 耗尽,而不是其他故障伪装?
别一看到服务挂了就重启。先验证是否真卡在 fd 上:
- 进进程所在环境(宿主机或容器)执行
ulimit -n,看软限制值(常见 1024、4096) - 查当前使用量:
ls /proc/1/fd | wc -l(PID 1是容器主进程;宿主机上用lsof -p $PID | wc -l) - 若使用量 ≥ 90% 的
ulimit -n值,且日志里出现EMFILE、accept: too many open files或java.io.IOException: Too many open files,基本锁定 - 顺手检查 Docker daemon 自身限制:
cat /proc/$(pgrep dockerd)/limits | grep "Max open files",避免守护进程自己也卡住
为什么改了 limits.conf 还没生效?systemd 服务常被忽略的关键点
systemd 管理的服务(如 nginx、mysqld、myapp.service)默认不读取 /etc/security/limits.conf,哪怕你写了 * soft nofile 65535 也没用。
- 必须显式在服务单元中配置:
sudo systemctl edit myapp.service,添加:[Service] LimitNOFILE=65535
- 如果服务由用户启动(比如
www-data),需同时配/etc/security/limits.conf和 systemd override,两者缺一不可 - 修改后必须
systemctl daemon-reload && systemctl restart myapp,reload 不等于 restart - 验证是否生效:
cat /proc/$(pgrep -f myapp.jar)/limits | grep "Max open files",看Soft Limit列是否为预期值
泄漏点在哪?lsof 输出怎么看才不被误导
lsof -p $PID 输出几百行,重点不是数总量,而是看类型分布和异常路径:
- 高频
IPv4或IPv6:说明连接未关闭,可能是 HTTP 客户端没设Connection: close、没复用连接池、或 gRPC 长连接未正确 shutdown - 大量
REG类型指向/tmp或日志目录:常见于未关闭的临时文件、日志轮转时旧文件句柄未释放(如 logback 的prudent=true缺失) - 一堆
pipe或eventpoll:Go 程序 goroutine panic 后 defer 未执行,或 Python 异步任务未 await 就 return - 出现
deleted标记的文件:日志被logrotate切走但进程还在写,典型泄漏场景,需发SIGUSR1通知重开句柄
容器环境下必须分层调优,单改一处等于白做
Docker 不是黑盒,fd 限制从下到上共三层,漏一层就会断在中间:
- 宿主机全局上限:
fs.file-max(写入/etc/sysctl.conf并sysctl -p) - Docker daemon 自身限制:在
/etc/systemd/system/docker.service.d/override.conf加LimitNOFILE=1000000,否则它连给容器分配的资格都没有 - 容器启动参数:用
--ulimit nofile=65536:65536或 docker-compose 的ulimits字段,硬限制不能低于软限制 - 容器内应用仍需主动管理资源——调高限制只是买时间,不修复泄漏,迟早再爆
最易被忽略的是泄漏的“延迟显现”:Go 的 runtime GC、Java 的 finalizer、Python 的循环引用,都可能导致 lsof 显示句柄仍在,但实际已无业务用途。这种情况下,光看数量不够,得结合 strace -p $PID -e trace=open,openat,socket,close 抓实时系统调用,才能确认是不是关晚了、漏关了、还是根本没关。

















