根本原因是PyMongo的Cursor对象未显式关闭,且不支持自动清理:它不实现__del__、无上下文管理器、GC无法保证及时回收,导致游标在服务端长期空闲直至超时。

Python脚本退出后 MongoDB 仍有未关闭的空闲游标,根本原因不是“脚本没退干净”,而是 Cursor 对象未被显式关闭,且 Python 的垃圾回收(GC)无法及时触发 cursor.close() —— MongoDB 服务端不会因客户端进程结束就自动清理游标,它只认游标 ID 和空闲时间。
PyMongo 游标不自动关闭是设计使然
PyMongo 的 find() 返回的是一个惰性 Cursor 对象,它不持有数据,只持有一个服务端分配的游标 ID 和连接上下文。这个对象本身不实现 __del__ 或自动注册 GC 回调来调用 close();即使脚本正常退出、变量超出作用域,只要 GC 没运行或没轮到它,cursor 就一直挂着。
- PyMongo 4.0+ 彻底移除了
no_cursor_timeout=True直接传给find()的用法,说明官方在推动更明确的生命周期管理 -
Cursor没有上下文管理器协议(不支持with cursor:),不能像open()那样靠__exit__保底 - 若脚本因 SIGKILL、OOM 或强制 kill 结束,连 GC 都没机会跑,游标直接“孤儿化”遗留在 mongod 上
常见误以为“已安全退出”的写法
这些代码看似无害,实则埋下游标泄漏隐患:
-
for doc in collection.find().batch_size(1000): process(doc)—— 循环结束后Cursor仍存活,只要没手动.close(),10 分钟后服务端删它,但期间它占着内存和连接句柄 -
list(collection.find())—— 表面看“一次取完”,但底层仍是先建游标、再遍历耗尽、最后丢弃对象;若中间抛异常或提前 break,游标可能没被完全消费,也不会自动关 - 把
cursor存在全局变量、类属性或缓存里,又没配清理逻辑 —— 生命周期脱离函数作用域,GC 更难回收
真正可靠的关闭方式只有两种
必须由你主动控制,不能依赖 Python 自动行为:
- 显式调用
cursor.close(),并确保它一定被执行:cursor = collection.find(filter) try: for doc in cursor: process(doc) finally: cursor.close() # 即使 process 抛异常也执行 - 改用
find()+limit()+_id范围分页,每次查询都生成新游标、自然消亡:last_id = None while True: query = {"_id": {"$gt": last_id}} if last_id else {} batch = list(collection.find(query).limit(500)) if not batch: break for doc in batch: process(doc) last_id = batch[-1]["_id"]这种方式不维持长生命周期游标,规避了超时与泄漏双重风险
最易被忽略的一点:PyMongo 的 Cursor 不是“用完即焚”的轻量对象,它是有状态的服务端资源代理;你不关它,它就在 mongod 的 currentOp() 里持续显示为 "secs_running": N,直到超时或被 kill —— 这不是 bug,是协议层的设计事实。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











