fd耗尽主因是未显式调用close()导致文件描述符泄漏,而非并发过高;python异步框架不自动释放os层fd,只要连接对象被强引用,gc就无法回收,常见于握手失败、异常断连未捕获、广播循环中断等场景。

FD耗尽不是“连接太多”,而是close()漏调了
根本原因不是并发数高,而是每个WebSocket或HTTP连接实例没显式调用close(),导致操作系统文件描述符(FD)持续累积。Python异步框架(如aiohttp、websockets、Uvicorn)本身不自动回收底层socket——GC只管Python对象,不管OS层的FD。只要连接对象还被事件循环、定时器、闭包或全局容器强引用着,它就永远不会释放FD。
常见漏关场景:握手失败、异常中断、广播未兜底
这些地方最容易漏掉close(),且日志里往往不报错:
-
upgrader.CheckOrigin返回False时,HTTP握手已部分完成,但连接对象已创建,必须手动调用conn.close() - 客户端异常断连后,服务端仍在对
websocket对象调用send_text(),抛出ConnectionResetError或WebSocketDisconnect,但没捕获 → 连接卡在list或set里不动 - 广播用
for conn in active_connections:+await conn.send(),一旦某个conn失效,整个循环卡住或中断,后续连接得不到清理机会
Linux层确认和快速验证方法
别猜,直接查系统级信号:
- 运行
lsof -p $(pgrep -f "uvicorn\|aiohttp") | wc -l,看FD数量是否接近ulimit -n设定值 - 检查日志中是否频繁出现
EMFILE(“Too many open files”)或ConnectionResetError: [Errno 104] - 启动Uvicorn时加
--limit-concurrency 100,观察FD增长是否变缓——如果仍涨,说明是泄漏而非负载问题
真正有效的三道防线
光靠try/finally不够,得从设计层堵死泄漏路径:
- 存连接用
set而非list,避免重复添加;每次send前加try/except WebSocketDisconnect,失败立即active_connections.discard(conn) - 心跳必须服务端主动驱动:
while True: try: await websocket.ping(); await asyncio.sleep(20) except Exception: await websocket.close(); break - 所有异步资源(DB连接、HTTP client、WebSocket)统一走
async with,确保__aexit__触发close(),哪怕协程中途return或抛异常
最常被忽略的是:即使你写了await websocket.close(),如果它发生在except块里但没await(比如忘了加await),或者被外层asyncio.shield()包裹导致取消失败,FD照样不释放。异步清理必须可等待、可取消、有超时。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











