MongoDB 在批量读取数据时偶尔会抛出 CursorNotFound 异常,完整信息通常类似 CursorNotFound: cursor id 123456789 not found,错误码是 43,错误名也是 CursorNotFound。这个异常的含义并不是查询语句写错,也不是网络闪断直接报错,而是客户端拿着一个已经失效的游标 id 继续向服务端请求下一批数据,MongoDB 服务端已经把这个游标清理掉了。

这类问题在数据同步、报表生成、批量导出等长任务中非常常见。很多任务一开始查询很快,前几批数据都能正常返回,但处理到中间某个批次时突然就中断,根因通常和 MongoDB 的游标生命周期管理有关。
一、MongoDB 游标为什么会自动过期
MongoDB 查询并不是一次性把所有结果返回给客户端。例如 find 默认只返回第一批数据,通常是 101 条或根据 batchSize 设置的值。客户端遍历完当前批次后,驱动会自动向服务端发送 getMore 命令,继续获取下一批结果。服务端必须记住这个查询的执行状态、游标位置以及可能占用的排序内存,这样才能在后续 getMore 时接着返回。
为了释放不再使用的资源,MongoDB 服务端为游标设置了一个空闲超时时间,默认是 10 分钟。只要客户端在 10 分钟内没有任何 getMore 请求,服务端就会销毁游标。这里的关键是“空闲”,不是查询的运行时间。如果客户端每一批之间只间隔几秒,游标不会超时;但如果处理某批数据时调用了外部接口、写文件、执行复杂计算,导致超过 10 分钟没有读取下一批,服务端就会清理掉游标。之后客户端再次 getMore,服务端找不到对应 id,于是抛出 CursorNotFound。
下面是一个典型的慢处理示例:
from pymongo import MongoClient
import time
client = MongoClient("mongodb://localhost:27017")
db = client.test
cursor = db.orders.find({}).batch_size(100)
for doc in cursor:
# 假如 process_slow_api 每批耗时超过10分钟,游标会在服务端被回收
process_slow_api(doc)
在这段代码里,驱动会先获取前 100 条文档,然后进入 for 循环逐条处理。如果这 100 条处理时间超过 10 分钟,服务端已经认为游标空闲太久了,进程下一次取下一批 100 条时就会失败。
二、CursorNotFound 的常见触发场景
除了处理慢导致超时,还有几种典型场景会触发 CursorNotFound。第一种是应用重启或连接断开。游标是绑定在服务端连接上的,如果客户端进程被重启、部署新版本,或者网络导致连接被释放,原有的游标 id 自然失效。第二种是分页策略错误。有些开发者一次性打开游标,然后在循环里调用异步接口,且没有及时取下一批。第三种是聚合查询。聚合管道如果输出到游标,同样有 10 分钟空闲超时,尤其是 aggregate 中包含 $sort、$group 等耗时阶段时更明显。
需要特别区分两个参数:maxTimeMS 和游标空闲超时。前者限制服务端执行查询的累计时间,后者限制两次 getMore 之间的空闲时间。即使设置了 maxTimeMS,游标仍然会受默认 10 分钟空闲回收机制影响。真正控制空闲超时的是服务端参数 cursorTimeoutMillis,默认值为 600000 毫秒。
如果希望游标不被自动回收,可以在创建查询时设置 noCursorTimeout。这个选项会告诉服务端不要对该游标进行空闲超时清理。它的副作用也很明显:如果客户端崩溃或者忘记关闭游标,服务端游标会一直存在,持续占用内存和快照资源。因此只在确实需要长时间处理且能保证显式关闭游标的情况下使用。
三、解决方法与代码示例
最直接的解决方式是开启 noCursorTimeout,并且把游标关闭放进 finally 块,避免异常退出时泄漏。Python 驱动示例如下:
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017")
db = client.test
cursor = db.orders.find({}, no_cursor_timeout=True).batch_size(200)
try:
for doc in cursor:
handle_long_task(doc)
finally:
cursor.close()
Node.js 驱动也可以使用同样的选项,但需要特别注意游标类型。MongoDB Node.js 驱动 4.x 以后,find 返回的是 FindCursor,可以使用 close() 手动关闭。
const { MongoClient } = require('mongodb');
async function processOrders() {
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const collection = client.db('test').collection('orders');
const cursor = collection.find({}, { noCursorTimeout: true }).batchSize(200);
try {
while (await cursor.hasNext()) {
const doc = await cursor.next();
await handleLongTask(doc);
}
} finally {
await cursor.close();
await client.close();
}
}
不过,无脑开启 noCursorTimeout 并不适合所有项目。如果批量任务可以中断后重跑,更推荐采用分页方式。分页查询不会在服务端长期保留游标,每一页都是独立查询,处理多久都不会因为游标空闲而失败。常见做法是基于主键 _id 或业务时间字段进行范围分页。
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017")
db = client.test
last_id = None
while True:
query = {"_id": {"$gt": last_id}} if last_id else {}
batch = list(db.orders.find(query).sort("_id", 1).limit(200))
if not batch:
break
for doc in batch:
handle_long_task(doc)
last_id = batch[-1]["_id"]
这段代码每次只查询 200 条,处理完后再根据最后一条的 _id 继续查下一批。由于每一轮查询都是新的游标,处理时间不会受 10 分钟限制。需要注意 _id 或分页字段必须有索引,并且数据在任务期间不能被删除或修改,否则可能出现重复或漏读。如果数据不断变化,可以结合快照时间或使用 createdAt 字段和稳定排序条件。
除了以上两种思路,还可以通过减小 batchSize 或提高单批处理速度来降低触发概率,但这只是缓解,不是根治。对于确实需要长时间持有游标的场景,例如数据库遍历同时执行远程调用,建议在业务上做断点续传,把已处理的主键或偏移量记录下来,即使中途异常也能从断点继续。
四、排查步骤与生产环境建议
当生产环境出现 CursorNotFound 时,首先要确认异常错误码是否为 43,并记录发生时间。然后排查从那一次 getMore 到前一次 getMore 之间是否超过 10 分钟。若不确认,可以开启驱动命令监控,打印所有 getMore 的触发时间。
const { MongoClient } = require('mongodb');
const client = new MongoClient('mongodb://localhost:27017', { monitorCommands: true });
client.on('commandStarted', (event) => {
if (event.commandName === 'getMore') {
console.log(new Date(), 'getMore start', event.command.getMore);
}
});
client.on('commandSucceeded', (event) => {
if (event.commandName === 'getMore') {
console.log(new Date(), 'getMore success');
}
});
通过日志可以很明显地看到两个时间戳之间的差距。如果间隔正好超过 10 分钟,就可以确认是游标空闲超时。如果间隔很短但仍然报 CursorNotFound,则需要检查客户端是否发生过重连、是否使用了不兼容的连接池配置,或者多线程共享了同一个游标对象。
生产环境中的建议是:优先采用基于索引字段的分页查询,避免长游标;如果业务必须使用长游标,必须同时配置 noCursorTimeout 与 close 保护;为所有批量任务增加重试和断点续传逻辑;监控 MongoDB 实例的 metrics.cursor.open.total 等指标,避免游标泄漏;不要把全局 cursorTimeoutMillis 调得过大,因为在低版本或高并发下,过多长时间未清理的游标会占用大量内存。
总之,CursorNotFound 的本质是服务端资源回收策略,而不是查询错误。理解游标的生命周期,选择合适的遍历方式,才能让长任务更稳定。如果任务可以拆页,就不要依赖长游标;如果必须长游标,就显式关闭并接受资源占用成本。
MongoDB游标超时CursorNotFoundnoCursorTimeout修改时间:2026-09-28 05:34:16