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

时间:2017-05-29 17:52:33

标签: node.js mongodb mongodb-query cursor

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

mongo error

5 个答案:

答案 0 :(得分:57)

编辑 - 查询性能:

正如@NeilLunn在他的评论中指出的那样,你不应该手动过滤文档,而是使用.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. 在光标到期之前获取所有文档。

  6. 请注意,它们没有按照任何特定标准编号。仔细阅读并确定哪一种最适合您的特定情况。

    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并且不要重新发明轮子。< / p>

    此外,值得一提的是,它与解决方案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

答案 1 :(得分: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)帮助我提高了性能。

答案 2 :(得分:1)

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

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

将npm软件包更新为3.1.x对我来说已经解决了,因为我已经考虑了@Danziger在此提供的良好建议。

答案 3 :(得分:0)

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

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

答案 4 :(得分:0)

在我的案例中,

这是一个负载平衡问题,在Node.js服务和Mongos上作为Kubernetes上的Pod运行时存在相同的问题。 客户端使用具有默认负载平衡的mongos服务。 更改kubernetes服务以使用const filteredUsers = users.filter(user => user.isLost) (粘性)对我来说解决了这个问题。