编辑-查询性能:
正如@NeilLunn在他的 comments 中指出的,你不应该手动过滤文档,而应该使用.find(...)
:
db.snapshots.find({
roundedDate: { $exists: true },
stream: { $exists: true },
sid: { $exists: false }
})
此外,从MongoDB 3.2
开始使用.bulkWrite()
将远比单独进行更新更有效.
这样,您就有可能在光标的10分钟生命周期内执行查询.如果仍然需要更多时间,光标将过期,并且无论如何都会出现相同的问题,如下所述:
这里发生了什么:
Error: getMore command failed
可能是由于光标超时,这与两个光标属性有关:
很可能您正在使用最初的101个文档,然后得到一个16MB的批处理,这是最大的批处理,其中包含更多的文档.由于处理这些文档需要10分钟以上,服务器上的光标会超时,当您处理完第二批and request a new one中的文档时,光标已经关闭:
在遍历游标并到达返回批处理的末尾时,如果有更多结果,请单击游标.next()将执行getMore操作以检索下一批.
可能的解决方案:
我认为有5种可能的方法可以解决这个问题,3种是好的,各有利弊,还有2种是坏的:
? 减少批处理大小以保持光标处于活动状态.
? 从光标中删除超时.
? 光标过期后重试.
? 手动批量查询结果.
? 在光标过期之前获取所有文档.
注意:它们没有按照任何特定标准进行编号.仔细阅读并决定哪一个最适合你的具体情况.
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
确保在使用光标的所有文档之前,如果出现任何问题,请关闭光标.
注意,我不认为这是一个坏的解决办法(因此)?), 甚至认为这是一种不好的做法
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"模式.这可以确保查询不会多次返回文档,即使中间的写入操作由于文档大小的增长而导致文档移动.
但是,请记住它的局限性:
注意:与其他解决方案相比,解决方案5移动可能导致重复文档检索的文档的时间窗口较窄,因此您可能不需要snapshot()
.
在您的特定情况下,因为集合名为snapshot
,所以可能不太可能更改,所以您可能不需要snapshot()
.此外,您正在根据文档的数据对其进行更新,更新完成后,即使多次检索该文档,也不会再次更新该文档,因为if
条件将跳过它.
关于开放游标的注意事项
要查看打开的游标数,请使用db.serverStatus().metrics.cursor
.