【问题标题】:MongoDB - Error: getMore command failed: Cursor not foundMongoDB - 错误:getMore 命令失败:找不到光标
【发布时间】:2017-10-30 02:15:59
【问题描述】:

我需要在大约 500K 个文档的集合中的每个文档上创建一个新字段 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,
}: ...

【问题讨论】:

  • 我原以为你最大的问题是doc.stream && doc.roundedDate && !doc.sid 这一行。您只是传递 .find() 而根本没有查询表达式,而是在代码中过滤 all 文档。通过移动到查询.find({ "roundedDate": { "$exists": true }, "stream": { "$exists": true }, "sid": { "$exists": false } })数据库 完成工作。您还应该每 500 个左右的项目使用.bulkWrite(),而不是实际执行服务器上的每个语句。
  • 这两件简单的事情 1. 不要迭代不必要的项目,2. 使用批量提交来避免服务器确认开销。应该从您当前的流程中节省“几个小时”。您可以进行其他更改以使光标保持活动状态,但最好的办法基本上是减少处理时间。甚至可以通过_id 上的“范围”来分解文档的选择,以进一步减少可能的选择。
  • 很棒的信息@NeilLunn。我正在查看 bulkWrite() - 在我使用 bulkWrite() 和 updateMany() 的特殊情况下的问题是我无法访问每条记录,并且无法创建 sid 字段。不过,我一直在寻找这样的方法。我可以使用 mongoose 在数组中本地组装/更新所有记录,但我找不到在数据库中对它们进行 batcg 插入/更新/覆盖的方法。
  • 我想你误解了.bulkWrite()的用法。以this answer 为例。弄乱光标超时只会带你到目前为止。您真正需要做的是减少开销。
  • @NeilLunn 感谢您指出这一点,我完全错过了这一点,实际上查询可以在游标过期之前执行,特别是如果有可用于匹配.find(...) 查询的索引。

标签: node.js mongodb mongodb-query database-cursor


【解决方案1】:

编辑 - 查询性能:

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

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

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

这样,您就有可能在游标的 10 分钟生命周期内执行查询。如果它仍然需要更多时间,您的光标将过期,无论如何您都会遇到同样的问题,如下所述:

这里发生了什么:

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

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

    默认情况下,服务器会在 10 分钟不活动或客户端耗尽光标后自动关闭光标。

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

    find()aggregate() 操作的初始批量大小默认为 101 个文档。对结果游标发出的后续getMore 操作没有默认批处理大小,因此它们仅受 16 兆字节消息大小的限制。

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

当您遍历游标并到达返回批次的末尾时,如果有更多结果,则 cursor.next() 将执行 getMore 操作以检索下一批。


可能的解决方案:

我看到了 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();

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

设置noCursorTimeout 选项后,您必须使用cursor.close() 手动关闭游标或通过耗尽游标的结果来关闭游标。

由于您要处理光标中的所有文档,因此您不需要手动关闭它,但是您的代码中仍然可能出现其他问题并且在您完成之前抛出错误,从而离开光标打开了。

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

请注意,我不认为这是一个糟糕的解决方案(因此是 ?),甚至认为这被认为是一种糟糕的做法......:

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

  • 有一些方法可以安全使用它,只是要格外小心。

  • 我假设您没有定期运行此类查询,因此您开始在各处留下打开的游标的可能性很小。如果不是这种情况,并且您确实需要一直处理这些情况,那么不使用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()sort() 对您认为可以在 10 分钟内处理的大量文档进行多次查询。

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

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


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

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

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

这将一个接一个地检索所有批次并关闭光标。然后,你就可以遍历results里面的所有文档,做你需要做的事情。

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


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

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

将 snapshot() 方法附加到光标以切换“快照”模式。这样可以确保查询不会多次返回一个文档,即使由于文档大小的增长,干预写入操作导致文档移动也是如此。

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

  • 它不适用于分片集合。

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

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

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

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


关于打开游标的注意事项

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

【讨论】:

  • Cursor not found 错误是否来自超时以外的情况?
  • 我猜很少,但是如果您快速搜索一下,您会发现由于驱动程序中的错误而发生了几次:github.com/meteor/meteor/issues/7763github.com/go-mgo/mgo/pull/295
  • 只是为了超级清楚,光标超时是否在下一次批量检索时重置(即下次客户端获取一批 101 个文档时)?谢谢
  • @mils 是的,如果您请求新批次,并且自上次请求以来光标尚未过期(您仍在 10 分钟窗口内),它将被重置,所以从那时起开,您有 10 分钟的时间在关闭之前发出另一个请求(并再次重置光标)。
  • 哇,多好的答案啊!非常感谢!
【解决方案2】:

这是 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) 帮助我稍微降低了性能。

【讨论】:

  • 请不要对多个问题添加相同的答案。回答最好的一个并将其余的标记为重复。见Is it acceptable to add a duplicate answer to several questions?
  • 另外值得注意的是,如果您使用包含大量数据的大型集合,这将对性能产生重大影响,因为减少批量大小显然意味着您将不得不为了获得下一批并处理相同数量的数据,请多次访问数据库并返回。
【解决方案3】:

我也遇到了这个问题,但对我来说是由 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 在这里给出的好的建议。

【讨论】:

    【解决方案4】:

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

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

    【讨论】:

      【解决方案5】:

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

      【讨论】:

      • 你拯救了我的一天!不错的收获
      【解决方案6】:

      noCursorTimeout 将不起作用

      现在是 2021 年,对于

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

      official

      考虑一个使用 cursor.noCursorTimeout() 发出 db.collection.find() 的应用程序。服务器返回一个游标以及由 find() 的 cursor.batchSize() 定义的一批文档。每次应用程序从服务器请求一批新文档时,会话都会刷新。但是,如果应用程序处理当前批次文档的时间超过 30 分钟,则会话将被标记为过期并关闭。当服务器关闭会话时,它也会杀死游标,尽管游标配置了 noCursorTimeout()。当应用程序请求下一批文档时,服务器返回错误。

      这意味着:即使你已经设置:

      • noCursorTimeout=真
      • 更小batchSize

      在默认30 minutes之后仍将cursor id not found

      如何修复/避免cursor 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()
      

      【讨论】:

        猜你喜欢
        • 2019-02-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-04-05
        • 2021-03-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多