我需要在大约 500K 个文档的集合中的每个文档上创建一个新字段 sid
。每个 sid
都是唯一的,并且基于该记录的现有 roundedDate
和 stream
字段。
我正在使用以下代码执行此操作:
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 许可协议
编辑 - 查询性能:
正如@NeilLunn 在他的评论中指出的那样,您不应该手动过滤文档,而是使用
.find(...)
来代替:此外,使用
.bulkWrite()
(从MongoDB 3.2
开始提供)将比进行单独更新的性能要高得多。这样,您就有可能在游标的 10 分钟生命周期内执行查询。如果它仍然需要更多时间,您的光标将过期并且无论如何您都会遇到同样的问题,如下所述:
这里发生了什么:
Error: getMore command failed
可能是游标超时,和两个游标属性有关:3.4
)。 从文档:可能您正在使用最初的 101 个文档,然后获得 16 MB 的批次,这是最大值,还有更多的文档。由于处理它们需要 10 多分钟,因此服务器上的光标超时,当您处理完第二批中的文档 并请求新 的文档时,光标已经关闭:
可能的解决方案:
我看到了 5 种可能的方法来解决这个问题,3 种好的方法,各有利弊,2 种不好的方法:
👍 减少批量大小以保持光标存活。
👍从光标中删除超时。
👍 当光标过期时重试。
👎 手动批量查询结果。
👎 在光标过期之前获取所有文档。
请注意,它们没有按照任何特定标准编号。通读它们并决定哪一个最适合您的特定情况。
1. 👍 减少批量大小以保持光标存活
解决此问题的一种方法是使用
cursor.bacthSize
设置find
查询返回的游标上的批处理大小,以匹配您可以在这 10 分钟内处理的那些:但是,请记住,设置一个非常保守(小)的批量大小可能会起作用,但也会变慢,因为现在您需要更多次访问服务器。
另一方面,将其设置为与您可以在 10 分钟内处理的文档数量太接近的值意味着如果某些迭代由于任何原因需要更长的时间来处理(其他进程可能会消耗更多资源) ,光标无论如何都会过期,你会再次得到同样的错误。
2. 👍从光标中删除超时
另一种选择是使用 cursor.noCursorTimeout 来防止光标超时:
这被认为是一种不好的做法,因为您需要手动关闭游标或耗尽其所有结果以使其自动关闭:
由于您要处理光标中的所有文档,因此您不需要手动关闭它,但是您的代码中仍然可能出现其他问题并且在您完成之前抛出错误,从而使光标保持打开状态.
如果您仍想使用此方法,请使用
try-catch
确保在使用所有文档之前在出现任何问题时关闭光标。注意我不认为这是一个不好的解决方案(因此是👍),甚至认为它被认为是一种不好的做法……:
这是驱动程序支持的功能。如果它是如此糟糕,因为有其他方法可以解决超时问题,如其他解决方案中所述,这将不受支持。
有很多方法可以安全地使用它,只是要格外小心。
我假设您没有定期运行此类查询,因此您开始在各处留下打开的游标的可能性很低。如果不是这种情况,并且您确实需要一直处理这些情况,那么不使用
noCursorTimeout
确实有意义。3. 👍 光标过期重试
基本上,您将代码放在
try-catch
中,当您收到错误消息时,您会看到一个新光标跳过您已经处理的文档:请注意,您需要对结果进行排序才能使此解决方案起作用。
使用这种方法,您可以通过使用 16 MB 的最大可能批处理大小来最小化对服务器的请求数量,而无需提前 10 分钟猜测您将能够处理多少文档。因此,它也比以前的方法更健壮。
4.👎手动批量查询结果
基本上,您使用 skip() 、 limit() 和 sort() 对您认为可以在 10 分钟内处理的多个文档进行多个查询。
我认为这是一个糟糕的解决方案,因为驱动程序已经可以选择设置批量大小,因此没有理由手动执行此操作,只需使用解决方案 1 并且不要重新发明轮子。
此外,值得一提的是,它与解决方案 1 具有相同的缺点,
5. 👎 获取光标过期前的所有文档
由于结果处理,您的代码可能需要一些时间来执行,因此您可以先检索所有文档,然后再处理它们:
这将一个接一个地检索所有批次并关闭游标。然后,你可以循环遍历
results
里面的所有文档,做你需要做的事情。但是,如果您遇到超时问题,则很可能您的结果集非常大,因此将所有内容都拉到内存中可能不是最明智的做法。
关于快照模式和重复文档的注意事项
如果由于文档大小的增长而干预写入操作移动了某些文档,则可能会多次返回某些文档。要解决此问题,请使用
cursor.snapshot()
。 从文档:但是,请记住它的局限性:
它不适用于分片集合。
它不适用于
sort()
或hint()
,因此它不适用于解决方案 3 和 4。它不保证与插入或删除隔离。
请注意,解决方案 5 移动可能导致重复文档检索的文档的时间窗口比其他解决方案更窄,因此您可能不需要
snapshot()
。在您的特定情况下,由于集合被称为
snapshot
,它可能不太可能改变,所以您可能不需要snapshot()
。此外,您正在根据文档的数据对其进行更新,一旦更新完成,即使多次检索同一文档也不会再次更新,因为if
条件将跳过它。关于打开游标的注意事项
要查看打开游标的计数,请使用
db.serverStatus().metrics.cursor
。