我需要在大约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);

它一开始运行良好,但几小时后,大约10万条记录出错:

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

mongo error

推荐答案

编辑-查询性能:

正如@NeilLunn在他的 comments 中指出的,你不应该手动过滤文档,而应该使用.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:

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

  • 批量大小,第一批为3.4个文档或16MB,后续批(从MongoDB 3.4开始)为16MB,与文档数量无关.From the docs:

    默认情况下,find()aggregate()操作的初始批处理大小为find()个文档.针对结果游标发出的后续getMore个操作没有默认的批处理大小,因此它们仅受16MB消息大小的限制.

很可能您正在使用最初的101个文档,然后得到一个16MB的批处理,这是最大的批处理,其中包含更多的文档.由于处理这些文档需要10分钟以上,服务器上的光标会超时,当您处理完第二批and request a new one中的文档时,光标已经关闭:

在遍历游标并到达返回批处理的末尾时,如果有更多结果,请单击游标.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();

This is considered a bad practice as you would need to close the cursor manually or exhaust all its results so that it is automatically closed:

设置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()方法附加到光标以切换"snapshot"模式.这可以确保查询不会多次返回文档,即使中间的写入操作由于文档大小的增长而导致文档移动.

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

  • 它不适用于碎片Collection .

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

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

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

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


关于开放游标的注意事项

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

Mongodb相关问答推荐

最有效的Mongo聚合来展开和过滤:匹配、展开、匹配与投影和过滤

为什么数组长度0被认为是Mongoose中排序中最大的数字

在mongo聚合管道的组阶段排除字段,但在最后将其包含在内

MongoDB shell:如何删除列表以外的所有集合

MongoDB:使用数组过滤器进行更新插入

MongoDB 支持的最 Big Data 库数

MongoDB:插入重复键更新

为什么 MongoDB 配置服务器必须只有一个或三个?

如何在 Mongo 聚合中合并文档中的数组字段

声明多个模式后无法从数据库中获取数据(mongoose + express + mongodb

子文档上的mongoose唯一索引

为什么 local.oplog.rs 上每隔几分钟的活动就会锁定 mongo 客户端

从 MongoDB find() 结果集中识别最后一个文档

Spring Mongodb @DBREF

如何将 mongoDB 数据导出为 CSV 格式?

mongo - 如何查询嵌套的 json

MongoDB - 我如何找到另一个集合中的文档未引用的所有文档

指定在 mongodb .js 脚本中使用哪个数据库

RoboMongo:不显示所有文档

单个语句中的多个 mongo 更新运算符?