MongoDB - 错误:getMore 命令失败:找不到光标

新手上路,请多包涵

我需要在大约 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,
}: ...

蒙戈错误

原文由 Chava Sobreyra 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 683
2 个回答

编辑 - 查询性能:

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

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

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

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

这里发生了什么:

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

  • 超时限制,默认为 10 分钟。 从文档

默认情况下,服务器将在 10 分钟不活动后自动关闭游标,或者如果客户端已耗尽游标。

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

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

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

当您遍历游标并到达返回批次的末尾时,如果有更多结果,则 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()从文档

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

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

  • 它不适用于分片集合。

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

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

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

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


关于打开游标的注意事项

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

原文由 Danziger 发布,翻译遵循 CC BY-SA 3.0 许可协议

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

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

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

原文由 SimonSimCity 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
logo
Stack Overflow 翻译
子站问答
访问
宣传栏