"部署的项目耗尽系统句柄"本质上是进程持有的文件描述符(file descriptor, FD)或句柄(handle)数量超过系统或用户限制,导致出现诸如:
Too many open filesEMFILEENFILE- Windows 下
The system cannot open the file
下面从系统层、应用层、典型场景、排查方法四个维度做一个系统性分析。
一、什么是"系统句柄"
在类 Unix 系统中:
-
每个进程都有一个 FD 表
-
文件、socket、pipe、epoll、inotify 都占用 FD
-
默认限制通常是:
- 单进程:1024(ulimit -n)
- 系统级:
/proc/sys/fs/file-max
在 Windows 中:
- 每个进程有 Handle Table
- 文件、线程、注册表键、事件对象都会占用 handle
二、常见耗尽原因(按出现频率排序)
1️⃣ 文件未关闭(最常见)
典型代码问题:
python
f = open("data.txt")
# 没有 f.close()
正确方式:
python
with open("data.txt") as f:
...
特征:
- FD 数持续增长
- 重启服务后恢复正常
2️⃣ 数据库连接泄漏
例如:
python
conn = mysql.connect(...)
cursor = conn.cursor()
# 忘记关闭
或连接池配置错误:
- 没有 max connections 限制
- 未归还连接
- 异常情况下未释放
表现:
- DB 连接数持续增加
- 服务响应变慢
- 最终报错
3️⃣ Socket 泄漏 / HTTP 客户端未关闭
例如:
python
requests.get(url)
如果使用:
python
session = requests.Session()
但未关闭 session,会导致连接不释放。
在 Node.js / Java 中也很常见。
4️⃣ 线程或协程暴增
线程创建后未销毁:
- 每个线程占用句柄
- 每个线程有 stack
- 线程数不断增长
例如:
python
while True:
threading.Thread(target=xxx).start()
5️⃣ inotify / 文件监听泄漏(Linux)
部署系统常见:
- 文件热更新
- Watcher 未释放
例如使用:
- watchdog
- webpack watcher
- nodemon
每个 watch 都会消耗 inotify FD。
6️⃣ 日志系统文件句柄泄漏
典型错误:
- 每次请求创建一个 logger
- 每次写日志新建 FileHandler
错误示例:
python
def log():
logger = logging.getLogger("x")
handler = FileHandler("app.log")
logger.addHandler(handler)
这会不断新增句柄。
7️⃣ 子进程未回收(僵尸进程)
例如:
python
subprocess.Popen(...)
但未:
python
process.wait()
会残留 FD。
三、为什么"部署后"才发生?
部署后:
| 场景 | 原因 |
|---|---|
| 并发提升 | 请求量增加,泄漏被放大 |
| 长时间运行 | 累积性资源泄漏 |
| 多 worker 模式 | gunicorn / uwsgi 复制问题 |
| Docker | 容器 FD 限制较小 |
| Nginx 反向代理 | socket 数暴增 |
四、如何精准排查(非常重要)
1️⃣ 查看当前 FD 数
Linux
查看进程 FD 数:
bash
lsof -p <pid> | wc -l
或:
bash
ls /proc/<pid>/fd | wc -l
实时观察:
bash
watch -n 1 'ls /proc/<pid>/fd | wc -l'
2️⃣ 查看 FD 类型
bash
lsof -p <pid>
你可以看到:
- REG(文件)
- IPv4(socket)
- anon_inode:[eventpoll]
- inotify
从类型判断问题来源。
3️⃣ 查看系统限制
bash
ulimit -n
cat /proc/sys/fs/file-max
4️⃣ 临时提升限制(不是根治)
bash
ulimit -n 65535
或在:
/etc/security/limits.conf
但⚠️ 提升限制只是延缓爆炸,不是修复泄漏。
五、典型真实案例
案例 1:Flask + requests 泄漏
- 每个 API 调用新建 Session
- 未关闭
- FD 10分钟涨到 10万
案例 2:Gunicorn worker 泄漏
- 每个 worker 都泄漏 5 个 FD
- 8 worker
- 每小时几千 FD
解决:
--max-requests 1000
定期重启 worker。
案例 3:日志 handler 泄漏
- 每次请求 addHandler
- 2 小时后 FD 爆满
六、判断是否泄漏的关键
看 FD 是否:
- 持续增长
- 不回落
- 重启恢复
如果是 → 100% 代码泄漏
七、工程级解决策略
- 所有 IO 用 context manager
- 数据库必须使用连接池
- HTTP 使用单例 Session
- 限制线程池大小
- 使用 Gunicorn max-requests
- 监控 FD 数量(Prometheus)