MongoDB - Error: getMore command failed: Cursor not found

MongoDB - Error: getMore command failed: Cursor not found

我需要在包含大约 50 万个文档的集合中的每个文档上创建一个新字段 sid。每个 sid 都是唯一的,并且基于该记录的现有 roundedDatestream 字段。

我正在使用以下代码执行此操作:

var cursor = db.getCollection('snapshots').find();
var iterated = 0;
var updated = 0;

while (cursor.hasNext()) {
    var doc = cursor.next();

    if (doc.stream && doc.roundedDate && !doc.sid) {
        db.getCollection('snapshots').update({ "_id": doc['_id'] }, {
            $set: {
                sid: doc.stream.valueOf() + '-' + doc.roundedDate,
            }
        });

        updated++;
    }

    iterated++;
}; 

print('total ' + cursor.count() + ' iterated through ' + iterated + ' updated ' + updated);

一开始它运行良好,但几个小时后大约 100K 条记录出现错误:

Error: getMore command failed: {
    "ok" : 0,
    "errmsg": "Cursor not found, cursor id: ###",
    "code": 43,
}: ...

编辑 - 查询性能:

正如@NeilLunn 在他的评论中指出的那样,您不应手动过滤文档,而应使用 .find(...) 代替:

db.snapshots.find({
    roundedDate: { $exists: true },
    stream: { $exists: true },
    sid: { $exists: false }
})

此外,使用从 MongoDB 3.2 开始可用的 .bulkWrite() 将比单独更新性能高得多。

这样,您就有可能在游标的 10 分钟生命周期内执行您的查询。如果还需要更多的时间,你的游标就会过期,你仍然会遇到同样的问题,下面解释:

这里发生了什么:

Error: getMore command failed可能是游标超时,与两个游标属性有关:

  • 超时限制,默认为10分钟。 From the docs:

    By default, the server will automatically close the cursor after 10 minutes of inactivity, or if client has exhausted the cursor.

  • 批量大小,第一批为 101 个文档或 16 MB,后续批次为 16 MB,无论文档数量如何(截至 MongoDB 3.4). From the docs:

    find() and aggregate() operations have an initial batch size of 101 documents by default. Subsequent getMore operations issued against the resulting cursor have no default batch size, so they are limited only by the 16 megabyte message size.

可能您正在使用最初的 101 个文档,然后得到 16 MB 的批次,这是最大值,还有更多的文档。由于处理它们需要超过 10 分钟,服务器上的游标超时,当您处理完第二批文档时 and request a new one,游标已经关闭:

As you iterate through the cursor and reach the end of the returned batch, if there are more results, cursor.next() will perform a getMore operation to retrieve the next batch.


可能的解决方案:

我看到了 5 种可能的方法来解决这个问题,3 种好的方法各有利弊,2 种不好的方法:

  1. 减小批量大小以保持游标活动。

  2. 取消游标超时。

  3. 光标过期时重试。

  4. 手动批量查询结果

  5. 获取游标过期前的所有文档

请注意,它们没有按照任何特定标准进行编号。通读它们并决定哪一个最适合您的特定情况。


1。减少批量大小以保持游标活动

解决该问题的一种方法是使用 cursor.bacthSize 设置 find 查询返回的游标的批量大小,以匹配您可以在这 10 分钟内处理的那些:

const cursor = db.collection.find()
    .batchSize(NUMBER_OF_DOCUMENTS_IN_BATCH);

但是,请记住,设置非常保守(小)的批量大小可能会起作用,但也会变慢,因为现在您需要访问服务器更多次。

另一方面,将其设置的值太接近您可以在 10 分钟内处理的文档数意味着如果某些迭代由于任何原因需要更长的时间来处理(其他过程可能消耗更多的资源),游标无论如何都会过期,你会再次得到同样的错误。


2。从游标中删除超时

另一种选择是使用cursor.noCursorTimeout来防止游标超时:

const cursor = db.collection.find().noCursorTimeout();

这被认为是一种不好的做法,因为您需要手动关闭游标或耗尽其所有结果以便自动关闭:

After setting the noCursorTimeout option, you must either close the cursor manually with cursor.close() or by exhausting the cursor’s results.

因为你想处理游标中的所有文档,你不需要手动关闭它,但是你的代码中仍然有可能出现其他问题并且在你完成之前抛出错误,从而使游标保持打开状态。

如果您仍想使用此方法,请使用 try-catch 确保在出现任何问题时关闭游标,然后再使用其所有文档。

请注意,我不认为这是一个糟糕的解决方案(因此 ),因为我什至认为它被认为是一种糟糕的做法...:[=​​53=]

  • 这是驱动支持的功能。如果它是如此糟糕,因为有其他方法可以解决超时问题,如其他解决方案中所述,这将不被支持。

  • 有安全使用方法,只是要格外小心。

  • 我假设您不会 运行 经常进行此类查询,因此您开始到处留下打开的游标的可能性很小。如果不是这种情况,并且您确实需要一直处理这些情况,那么不使用 noCursorTimeout.

  • 确实有意义

3。光标过期时重试

基本上,您将代码放在 try-catch 中,当出现错误时,您会得到一个新的光标,跳过您已经处理过的文档:

let processed = 0;
let updated = 0;

while(true) {
    const cursor = db.snapshots.find().sort({ _id: 1 }).skip(processed);

    try {
        while (cursor.hasNext()) {
            const doc = cursor.next();

            ++processed;

            if (doc.stream && doc.roundedDate && !doc.sid) {
                db.snapshots.update({
                    _id: doc._id
                }, { $set: {
                    sid: `${ doc.stream.valueOf() }-${ doc.roundedDate }`
                }});

                ++updated;
            } 
        }

        break; // Done processing all, exit outer loop
    } catch (err) {
        if (err.code !== 43) {
            // Something else than a timeout went wrong. Abort loop.

            throw err;
        }
    }
}

请注意,您需要对结果进行排序才能使此解决方案生效。

通过这种方法,您可以使用 16 MB 的最大可能批处理大小来最大程度地减少对服务器的请求数,而不必事先猜测您将能够在 10 分钟内处理多少文档。因此,它也比以前的方法更健壮。


4。手动批量查询结果

基本上,您使用 skip(), limit() and sort() 对您认为可以在 10 分钟内处理的大量文档进行多次查询。

我认为这是一个糟糕的解决方案,因为驱动程序已经可以选择设置批量大小,因此没有理由手动执行此操作,只需使用解决方案 1,不要重新发明轮子。

此外,值得一提的是,它与解决方案 1 具有相同的缺点,


5。获取游标过期前的所有文档

可能由于结果处理,您的代码执行需要一些时间,因此您可以先检索所有文档,然后再处理它们:

const results = new Array(db.snapshots.find());

这将依次检索所有批次并关闭游标。然后,您可以遍历 results 中的所有文档并执行您需要执行的操作。

但是,如果您遇到超时问题,可能是您的结果集非常大,因此将所有内容都拉入内存可能不是最明智的做法。


关于快照模式和重复文档的注意事项

如果由于文档大小的增加,中间的写入操作移动了某些文档,则可能会多次返回这些文档。要解决此问题,请使用 cursor.snapshot()From the docs:

Append the snapshot() method to a cursor to toggle the “snapshot” mode. This ensures that the query will not return a document multiple times, even if intervening write operations result in a move of the document due to the growth in document size.

但是,请记住它的局限性:

  • 它不适用于分片集合。

  • 不适用于sort() or hint(),因此不适用于解决方案 3 和 4。

  • 它不保证与插入或删除隔离。

注意解决方案 5 的时间 window 可能导致重复文档检索的文档移动时间比其他解决方案要短,因此您可能不需要 snapshot()

在您的特定情况下,由于集合名为 snapshot,它可能不太可能更改,因此您可能不需要 snapshot()。此外,您正在根据文档的数据对文档进行更新,一旦更新完成,即使多次检索同一个文档也不会再次更新,因为 if 条件将跳过它。


关于打开游标的注意事项

要查看打开游标的计数,请使用 db.serverStatus().metrics.cursor

这是 mongodb 服务器会话管理中的错误。目前正在修复,应该在 4.0+

中修复

SERVER-34810: Session cache refresh can erroneously kill cursors that are still in use

(转载于MongoDB 3.6.5)

添加 collection.find().batchSize(20) 帮助我稍微降低了性能。

我也 运行 遇到了这个问题,但对我来说这是由 MongDB 驱动程序中的错误引起的。

它发生在 npm 包 mongodb 的版本 3.0.x 中,例如在 Meteor 1.7.0.x 中使用,我也记录了这个问题。在此评论中对其进行了进一步描述,并且该线程包含一个示例项目来确认该错误:https://github.com/meteor/meteor/issues/9944#issuecomment-420542042

将 npm 包更新到 3.1.x 为我修复了它,因为我已经考虑了@Danziger 在这里给出的好的建议。

当使用Java v3 驱动程序时,应在 FindOptions 中设置 noCursorTimeout。

DBCollectionFindOptions options =
                    new DBCollectionFindOptions()
                        .maxTime(90, TimeUnit.MINUTES)
                        .noCursorTimeout(true)
                        .batchSize(batchSize)
                        .projection(projectionQuery);        
cursor = collection.find(filterQuery, options);

在我的例子中,这是一个负载平衡问题,有同样的问题 运行 Node.js 服务和 Mongos 作为 Kubernetes 上的 pod。 客户端正在使用具有默认负载平衡的 mongos 服务。 将 kubernetes 服务更改为使用 sessionAffinity: ClientIP(粘性)为我解决了这个问题。

noCursorTimeout 将不起作用

现在是 2021 年,

cursor id xxx not found, full error: {'ok': 0.0, 'errmsg': 'cursor id xxx not found', 'code': 43, 'codeName': 'CursorNotFound'}

official

Consider an application that issues a db.collection.find() with cursor.noCursorTimeout(). The server returns a cursor along with a batch of documents defined by the cursor.batchSize() of the find(). The session refreshes each time the application requests a new batch of documents from the server. However, if the application takes longer than 30 minutes to process the current batch of documents, the session is marked as expired and closed. When the server closes the session, it also kills the cursor despite the cursor being configured with noCursorTimeout(). When the application requests the next batch of documents, the server returns an error.

这意味着:即使你设置了:

  • noCursorTimeout=真
  • 更小batchSize

在默认 30 minutes

后仍将 cursor id not found

如何fix/avoidcursor id not found?

确定两点

  • (明确地)创建新会话,从该会话中获取 dbcollection
  • 定期刷新会话

代码:

  • (官方)js
var session = db.getMongo().startSession()
var sessionId = session.getSessionId().id
var cursor = session.getDatabase("examples").getCollection("data").find().noCursorTimeout()
var refreshTimestamp = new Date() // take note of time at operation start
while (cursor.hasNext()) {
  // Check if more than 5 minutes have passed since the last refresh
  if ( (new Date()-refreshTimestamp)/1000 > 300 ) {
    print("refreshing session")
    db.adminCommand({"refreshSessions" : [sessionId]})
    refreshTimestamp = new Date()
  }
  // process cursor normally
}
  • (我的)python
import logging
from datetime import datetime
import pymongo

mongoClient = pymongo.MongoClient('mongodb://127.0.0.1:27017/your_db_name')

# every 10 minutes to update session once
#   Note: should less than 30 minutes = Mongo session defaul timeout time
#       https://docs.mongodb.com/v5.0/reference/method/cursor.noCursorTimeout/
# RefreshSessionPerSeconds = 10 * 60
RefreshSessionPerSeconds = 8 * 60

def mergeHistorResultToNewCollection():

    mongoSession = mongoClient.start_session() # <pymongo.client_session.ClientSession object at 0x1081c5c70>
    mongoSessionId = mongoSession.session_id # {'id': Binary(b'\xbf\xd8\xd...1\xbb', 4)}

    mongoDb = mongoSession.client["your_db_name"] # Database(MongoClient(host=['127.0.0.1:27017'], document_class=dict, tz_aware=False, connect=True), 'your_db_name')
    mongoCollectionOld = mongoDb["collecion_old"]
    mongoCollectionNew = mongoDb['collecion_new']

    # historyAllResultCursor = mongoCollectionOld.find(session=mongoSession)
    historyAllResultCursor = mongoCollectionOld.find(no_cursor_timeout=True, session=mongoSession)

    lastUpdateTime = datetime.now() # datetime.datetime(2021, 8, 30, 10, 57, 14, 579328)
    for curIdx, oldHistoryResult in enumerate(historyAllResultCursor):
        curTime = datetime.now() # datetime.datetime(2021, 8, 30, 10, 57, 25, 110374)
        elapsedTime = curTime - lastUpdateTime # datetime.timedelta(seconds=10, microseconds=531046)
        elapsedTimeSeconds = elapsedTime.total_seconds() # 2.65892
        isShouldUpdateSession = elapsedTimeSeconds > RefreshSessionPerSeconds
        # if (curIdx % RefreshSessionPerNum) == 0:
        if isShouldUpdateSession:
            lastUpdateTime = curTime
            cmdResp = mongoDb.command("refreshSessions", [mongoSessionId], session=mongoSession)
            logging.info("Called refreshSessions command, resp=%s", cmdResp)
        
        # do what you want

        existedNewResult = mongoCollectionNew.find_one({"shortLink": "http://xxx"}, session=mongoSession)

    # mongoSession.close()
    mongoSession.end_session()