requests触发“too many open files”错误的根本原因是未复用socket连接,每次请求新建tcp连接并占用文件描述符(fd),叠加线程并发和未关闭资源,迅速耗尽ulimit -n默认限制(如1024);修复须用带连接池的session并配对释放所有fd。
![为什么python在并发请求时会报oserror: [errno 24] too many open files?](https://img.php.cn/upload/article/000/969/633/178148468620236.png?x-oss-process=image/resize,p_40)
这不是 Python 本身的问题,而是进程级文件描述符耗尽——HTTP 连接、socket、临时文件、子进程管道,全算“文件”。requests 每次发请求默认新建 socket,没复用就等于每请求占一个 fd;线程数 × 并发请求数 × 未关闭连接,很快撞上 ulimit -n 的默认值(Linux 多为 1024)。
为什么 requests 会触发这个错误?
根本原因是底层 socket 复用没打开,每个请求都新建 TCP 连接并持有 fd,直到连接被内核回收(TIME_WAIT 状态仍占 fd)。常见于:
- 直接调用
requests.get()/requests.post()循环中未复用 session - 使用了
urllib3或http.client但没配置连接池 - 错误地在多线程里为每个线程创建独立
requests.Session()却没设pool_connections和pool_maxsize
示例问题代码:
for url in urls:
requests.get(url) # ❌ 每次都新建 socket,fd 不释放
修复方式(必须):
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=10,
pool_maxsize=20,
max_retries=3
)
session.mount('http://', adapter)
session.mount('https://', adapter)
<p>for url in urls:
session.get(url) # ✅ 复用连接池中的 socket</p>
为什么 with tempfile.NamedTemporaryFile() 还会泄露 fd?
因为 NamedTemporaryFile 默认 delete=False 时不会自动删文件,而即使 delete=True,若你在 with 块内把文件对象传给子进程(如 subprocess.Popen(..., stdin=tmpfile)),Python 主进程退出前子进程可能仍在读,导致 fd 无法释放。
更隐蔽的坑:
-
tempfile.mkstemp()返回的 fd 必须手动os.close(fd),它不支持上下文管理 - 用
shutil.copyfileobj()流式拷贝时,如果源或目标是未关闭的NamedTemporaryFile,fd 会持续占用 - 多进程场景下,父进程 fork 后子进程继承所有 fd,若未显式
close(),父子都持有一份引用
如何确认是 fd 耗尽而非代码逻辑错误?
别猜,直接查进程实时状态:
- 拿到 Python 进程 PID:
pgrep -f "python.*your_script" - 看它开了多少 fd:
lsof -p <pid> | wc -l</pid>(注意:结果包含 socket、pipe、eventpoll 等) - 对比系统限制:
cat /proc/<pid>/limits | grep "Max open files"</pid> - 快速分类 fd 类型:
lsof -p <pid> -a -d "0-65535" | awk '{print $5}' | sort | uniq -c | sort -nr</pid>—— 如果大量出现IPv4、unix、pipe,基本锁定是网络或 IPC 泄露
特别注意:/proc/<pid>/fd/</pid> 目录下的链接数就是当前 fd 总量,比 lsof 更轻量、更准确。
ulimit 调大就能一劳永逸?
不能。调高只是兜底,不是解药。比如把 nofile 设到 65535,但代码里每轮循环开 100 个未关闭的 socket,10 轮就占满;或者 subprocess.Popen 启了 500 个子进程却没 .wait(),它们的 stdin/stdout fd 全挂在父进程上。
真正要做的两件事:
- 代码层:所有资源获取(
open、tempfile、socket.socket、subprocess.Popen)必须配对释放,优先走with;网络请求强制用带连接池的Session - 部署层:在服务启动前确保环境生效,例如 Supervisor 配置里加
ulimit -n 65535,或 systemd service 文件中写LimitNOFILE=65535—— 单纯改/etc/security/limits.conf对 daemon 进程常无效
最易被忽略的一点:Python 的 gc.collect() 不会立即回收文件对象,fd 释放依赖引用计数归零 + __del__ 触发,而后者时机不确定。所以别依赖“等 GC”,必须显式控制生命周期。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











