我们如何改进 MongoDB 的 MapReduce 函数,该函数检索数据的时间过长并给出内存不足错误?
How do we improve a MongoDB MapReduce function that takes too long to retrieve data and gives out of memory errors?
从 mongo 检索数据花费的时间太长,即使对于小型数据集也是如此。对于更大的数据集,我们得到 javascript 引擎的内存不足错误。我们已经尝试了几种模式设计和几种检索数据的方法。我们如何优化 mongoDB/mapReduce function/MongoWire 以更快地检索更多数据?
我们对 MongoDB 还不是很有经验,因此不确定我们是否遗漏了优化步骤,或者我们是否只是使用了错误的工具。
1.背景
出于绘图和回放的目的,我们希望存储多个对象随时间的变化。目前我们每个项目有数十个对象,但我们预计需要存储数千个对象。对象可能每秒都在变化,也可能长时间不变化。 Delphi 后端通过 MongoWire 和 SuperObjects 写入和读取 MongoDB,数据显示在 Web 前端。
2。架构设计
我们以每小时一条记录的形式将对象更改存储在分-秒-毫秒对象中。架构设计与描述的一样 here。示例:
o: object1,
dt: $date,
v: {0: {0:{0: {speed: 8, rate: 0.8}}}, 1: {0:{0: {speed: 9}}}, …}
我们在 {dt: -1, o: 1}
和 {o:1}
上建立了索引。
3。检索数据
我们使用 mapReduce 根据分-秒-毫秒对象构造一个新日期并将该对象放回 v:
o: object1,
dt: $date,
v: {speed: 8, rate:0.8}
在 mapReduce 函数之前,平均文档大约为 525 kB,并且有大约 29000 次更新。这样的文档经过mapReduce后,结果大概是746kB。
3.1 使用 mapReduce
从 mongo shell 检索数据
我们正在使用以下地图功能:
function mapF(){
for (var i = 0; i < 3600; i++){
var imin = Math.floor(i / 60);
var isec = (i % 60);
var min = ''+imin;
var sec = ''+isec;
if (this.v.hasOwnProperty(min) && this.v[min].hasOwnProperty(sec)) {
for (var ms in this.v[min][sec]) {
if (imin !== 0 && isec !== 0 && ms !== '0' && this.v[min][sec].hasOwnProperty(ms)) {// is our keyframe
var currentV = this.v[min][sec][ms];
//newT is new date computed by the min, sec, ms above
if (toDate > newT && newT > fromDate) {
if (fields && fields.length > 0) {
for (var p = 0, length = fields.length; p < length; p++){
//check if field is present and put it in newV
}
if (newV) {
emit(this.o, {vs: [{o: this.o, dt: newT, v: newV}]});
}
} else {
emit(this.o, {vs: [{o: this.o, dt: newT, v: currentV}]});
}
}
}
}
}
}
};
reduce 函数基本上只是传递数据。对 mapReduce 的调用:
db.collection.mapReduce( mapF,reduceF,
{out: {inline: 1},
query: {o: {$in: objectNames]}, dt: {$gte: keyframeFromDate, $lt: keyframeToDate}},
sort: {dt: 1},
scope: {toDate: toDateWithinKeyframe, fromDate: fromDateWithinKeyframe, fields: []},
jsMode: true});
在 1 小时内检索 2 个对象:2.4 秒。
在 5 小时内检索 2 个对象:8.3 秒。
对于这种方法,我们必须在运行时编写 js 和 bat 文件并读回 json 数据。我们还没有测量他的时间,因为坦率地说,我们不太喜欢这个想法。
此方法的另一个问题是,当我们尝试检索更长时间 and/or 更多对象的数据时,我们会遇到 v8 javascript 引擎的内存不足错误。使用具有更多 RAM 的 pc 在一定程度上可以防止内存不足,但它不会使检索数据更快。
这个article提到了splitVector,我们可能会用它来划分工作负载。但是我们不确定如何使用 keyPattern 和 maxChunkSizeBytes 选项。我们可以为 o
和 dt
使用 keyPattern 吗?
我们可能会使用多个集合,但目前我们的数据集并没有那么大,所以我们担心我们需要多少集合。
3.2 通过 mongoWire with mapReduce 检索数据
为了通过 mongoWire with mapReduce 检索数据,我们使用与上述相同的 mapReduce 函数。我们使用以下 Delphi 代码开始查询:
FMongoWire.Get('$cmd',BSON([
'mapreduce', ‘collection’,
'map', bsonJavaScriptCodePrefix + FMapVCRFunction.Text,
'reduce', bsonJavaScriptCodePrefix + FReduceVCRFunction.Text,
'out', BSON(['inline', 1]),
'query', mapquery,
'sort', BSON(['dt', -1]),
'scope', scope
]));
使用此方法检索数据大约慢 3-4 倍 (!)。然后必须将数据从 BSON(IBSONDocument 转换为 JSON(SuperObject),这是此方法中主要耗时的部分。为了检索原始数据,我们使用 TMongoWireQuery 将 BSONdocument 翻译成几部分,而此 mapReduce函数直接使用 TMongoWire 并尝试转换完整的结果。这可以解释为什么这需要这么长时间,而通常情况下它很快。如果我们可以将 mapReduce 所需的时间减少到 return 结果,这可能是一个我们要关注的下一步。
3.3 在Delphi
中检索原始数据并解析
将原始数据检索到 Delphi 比以前的方法需要更长的时间,但可能是因为使用了 TMongoWireQuery,从 BSON 到 JSON 的转换要快得多。
4.问题
- 我们可以进一步优化我们的架构设计吗?
- 我们怎样才能使 mapReduce 函数更快?
- 我们怎样才能防止外出
v8引擎的内存错误?有人可以提供更多信息吗
splitVector 函数?
- 我们如何才能最好地利用来自 Delphi 的 mapReduce?我们可以使用
MongoWireQuery 代替 MongoWire?
5.规格
MongoDB3.0.3
2015 年的 MongoWire(最近更新)
Delphi 2010(也有 XE5)
4GB RAM(也试过8GB RAM,内存不足,但读取时间大致相同)
哇,真是个问题!首先:我不是 MongoDB 方面的专家。我写 TMongoWire 是为了稍微了解一下 MongoDB。此外,我真的(真的)不喜欢包装器有过多的重载来做同样的事情,但对于各种特定类型。很久以前程序员没有泛型,但我们有 Variant。所以我构建了一个基于变体的 MongoDB 包装器(和 IBSONDocument)。也就是说,我显然做了一些人们喜欢使用的东西,并且通过保持简单来表现得很好。 (我最近没有花太多时间在上面,但排在首位的是自版本 3 以来的新身份验证方案。)
现在,关于您的具体设置。你说你使用 mapreduce 从 500KB 到 700KB?我认为有迹象表明您使用了错误的工具来完成这项工作。我不确定默认的 mongo shell 与你在 TMongoWire.Get
上做同样的事情有什么不同,但如果我假设 mapReduce 在通过网络发送之前先组装响应,那就是性能丢失的地方。
所以这是我的建议:您考虑使用 TMongoWireQuery 是正确的。它提供了一种更快地处理数据的方法,因为服务器会将数据流式传输进来,但还有更多。
我强烈建议使用数组来存储秒列表。即使不是所有的秒都有数据,在没有数据的秒上存储 null
所以每个分钟数组有 60 个项目。这就是为什么:
在设计 TMongoWireQuery 时出现的一个巧妙之处在于,假设您将一次处理一个 (BSON) 文档,并且文档的内容将大致相似,至少在值名称方面是这样.因此,通过在枚举响应时使用相同的 IBSONDocument 实例,您实际上可以节省大量时间,因为您不必取消分配和重新分配所有这些变体。
这适用于简单的文档,但实际上也适用于数组。这就是我创建 IBSONDocumentEnumerator 的原因。您需要在您期望文档数组的地方预加载一个带有 IBSONDocumentEnumerator 的 IBSONDocument 实例,并且您需要以与 TMongoWireQuery 大致相同的方式处理该数组:使用相同的 IBSONDocument 实例枚举它,所以当后续文档具有相同的键时,无需重新分配它们就可以节省时间。
但在您的情况下,您仍然需要通过网络将一整小时的数据拉到您需要的 select 秒。正如我之前所说,我不是 MongoDB 专家,但我怀疑可能有更好的方法来存储这样的数据。要么每秒一个单独的文档(我想这会让索引做更多的工作,并且 MongoDB 可以采用该插入率),要么使用特定的查询结构以便 MongoDB 知道将秒数数组缩短为您请求的数据($splice 是这样做的吗?)
这是一个如何在 {name:"fruit",items:[{name:"apple"},{name:"pear"}]}
等文档上使用 IBSONDocumentEnumerator 的示例
q:=TMongoWireQuery.Create(db);
try
q.Query('test',BSON([]));
e:=BSONEnum;
d:=BSON(['items',e]);
d1:=BSON;
while q.Next(d) do
begin
i:=0;
while e.Next(d1) do
begin
Memo1.Lines.Add(d['name']+'#'+IntToStr(i)+d1['name']);
inc(i);
end;
end;
finally
q.Free;
end;
从 mongo 检索数据花费的时间太长,即使对于小型数据集也是如此。对于更大的数据集,我们得到 javascript 引擎的内存不足错误。我们已经尝试了几种模式设计和几种检索数据的方法。我们如何优化 mongoDB/mapReduce function/MongoWire 以更快地检索更多数据? 我们对 MongoDB 还不是很有经验,因此不确定我们是否遗漏了优化步骤,或者我们是否只是使用了错误的工具。
1.背景
出于绘图和回放的目的,我们希望存储多个对象随时间的变化。目前我们每个项目有数十个对象,但我们预计需要存储数千个对象。对象可能每秒都在变化,也可能长时间不变化。 Delphi 后端通过 MongoWire 和 SuperObjects 写入和读取 MongoDB,数据显示在 Web 前端。
2。架构设计
我们以每小时一条记录的形式将对象更改存储在分-秒-毫秒对象中。架构设计与描述的一样 here。示例:
o: object1,
dt: $date,
v: {0: {0:{0: {speed: 8, rate: 0.8}}}, 1: {0:{0: {speed: 9}}}, …}
我们在 {dt: -1, o: 1}
和 {o:1}
上建立了索引。
3。检索数据
我们使用 mapReduce 根据分-秒-毫秒对象构造一个新日期并将该对象放回 v:
o: object1,
dt: $date,
v: {speed: 8, rate:0.8}
在 mapReduce 函数之前,平均文档大约为 525 kB,并且有大约 29000 次更新。这样的文档经过mapReduce后,结果大概是746kB。
3.1 使用 mapReduce
从 mongo shell 检索数据
我们正在使用以下地图功能:
function mapF(){
for (var i = 0; i < 3600; i++){
var imin = Math.floor(i / 60);
var isec = (i % 60);
var min = ''+imin;
var sec = ''+isec;
if (this.v.hasOwnProperty(min) && this.v[min].hasOwnProperty(sec)) {
for (var ms in this.v[min][sec]) {
if (imin !== 0 && isec !== 0 && ms !== '0' && this.v[min][sec].hasOwnProperty(ms)) {// is our keyframe
var currentV = this.v[min][sec][ms];
//newT is new date computed by the min, sec, ms above
if (toDate > newT && newT > fromDate) {
if (fields && fields.length > 0) {
for (var p = 0, length = fields.length; p < length; p++){
//check if field is present and put it in newV
}
if (newV) {
emit(this.o, {vs: [{o: this.o, dt: newT, v: newV}]});
}
} else {
emit(this.o, {vs: [{o: this.o, dt: newT, v: currentV}]});
}
}
}
}
}
}
};
reduce 函数基本上只是传递数据。对 mapReduce 的调用:
db.collection.mapReduce( mapF,reduceF,
{out: {inline: 1},
query: {o: {$in: objectNames]}, dt: {$gte: keyframeFromDate, $lt: keyframeToDate}},
sort: {dt: 1},
scope: {toDate: toDateWithinKeyframe, fromDate: fromDateWithinKeyframe, fields: []},
jsMode: true});
在 1 小时内检索 2 个对象:2.4 秒。
在 5 小时内检索 2 个对象:8.3 秒。
对于这种方法,我们必须在运行时编写 js 和 bat 文件并读回 json 数据。我们还没有测量他的时间,因为坦率地说,我们不太喜欢这个想法。
此方法的另一个问题是,当我们尝试检索更长时间 and/or 更多对象的数据时,我们会遇到 v8 javascript 引擎的内存不足错误。使用具有更多 RAM 的 pc 在一定程度上可以防止内存不足,但它不会使检索数据更快。
这个article提到了splitVector,我们可能会用它来划分工作负载。但是我们不确定如何使用 keyPattern 和 maxChunkSizeBytes 选项。我们可以为 o
和 dt
使用 keyPattern 吗?
我们可能会使用多个集合,但目前我们的数据集并没有那么大,所以我们担心我们需要多少集合。
3.2 通过 mongoWire with mapReduce 检索数据
为了通过 mongoWire with mapReduce 检索数据,我们使用与上述相同的 mapReduce 函数。我们使用以下 Delphi 代码开始查询:
FMongoWire.Get('$cmd',BSON([
'mapreduce', ‘collection’,
'map', bsonJavaScriptCodePrefix + FMapVCRFunction.Text,
'reduce', bsonJavaScriptCodePrefix + FReduceVCRFunction.Text,
'out', BSON(['inline', 1]),
'query', mapquery,
'sort', BSON(['dt', -1]),
'scope', scope
]));
使用此方法检索数据大约慢 3-4 倍 (!)。然后必须将数据从 BSON(IBSONDocument 转换为 JSON(SuperObject),这是此方法中主要耗时的部分。为了检索原始数据,我们使用 TMongoWireQuery 将 BSONdocument 翻译成几部分,而此 mapReduce函数直接使用 TMongoWire 并尝试转换完整的结果。这可以解释为什么这需要这么长时间,而通常情况下它很快。如果我们可以将 mapReduce 所需的时间减少到 return 结果,这可能是一个我们要关注的下一步。
3.3 在Delphi
中检索原始数据并解析
将原始数据检索到 Delphi 比以前的方法需要更长的时间,但可能是因为使用了 TMongoWireQuery,从 BSON 到 JSON 的转换要快得多。
4.问题
- 我们可以进一步优化我们的架构设计吗?
- 我们怎样才能使 mapReduce 函数更快?
- 我们怎样才能防止外出 v8引擎的内存错误?有人可以提供更多信息吗 splitVector 函数?
- 我们如何才能最好地利用来自 Delphi 的 mapReduce?我们可以使用 MongoWireQuery 代替 MongoWire?
5.规格
MongoDB3.0.3
2015 年的 MongoWire(最近更新)
Delphi 2010(也有 XE5)
4GB RAM(也试过8GB RAM,内存不足,但读取时间大致相同)
哇,真是个问题!首先:我不是 MongoDB 方面的专家。我写 TMongoWire 是为了稍微了解一下 MongoDB。此外,我真的(真的)不喜欢包装器有过多的重载来做同样的事情,但对于各种特定类型。很久以前程序员没有泛型,但我们有 Variant。所以我构建了一个基于变体的 MongoDB 包装器(和 IBSONDocument)。也就是说,我显然做了一些人们喜欢使用的东西,并且通过保持简单来表现得很好。 (我最近没有花太多时间在上面,但排在首位的是自版本 3 以来的新身份验证方案。)
现在,关于您的具体设置。你说你使用 mapreduce 从 500KB 到 700KB?我认为有迹象表明您使用了错误的工具来完成这项工作。我不确定默认的 mongo shell 与你在 TMongoWire.Get
上做同样的事情有什么不同,但如果我假设 mapReduce 在通过网络发送之前先组装响应,那就是性能丢失的地方。
所以这是我的建议:您考虑使用 TMongoWireQuery 是正确的。它提供了一种更快地处理数据的方法,因为服务器会将数据流式传输进来,但还有更多。
我强烈建议使用数组来存储秒列表。即使不是所有的秒都有数据,在没有数据的秒上存储 null
所以每个分钟数组有 60 个项目。这就是为什么:
在设计 TMongoWireQuery 时出现的一个巧妙之处在于,假设您将一次处理一个 (BSON) 文档,并且文档的内容将大致相似,至少在值名称方面是这样.因此,通过在枚举响应时使用相同的 IBSONDocument 实例,您实际上可以节省大量时间,因为您不必取消分配和重新分配所有这些变体。
这适用于简单的文档,但实际上也适用于数组。这就是我创建 IBSONDocumentEnumerator 的原因。您需要在您期望文档数组的地方预加载一个带有 IBSONDocumentEnumerator 的 IBSONDocument 实例,并且您需要以与 TMongoWireQuery 大致相同的方式处理该数组:使用相同的 IBSONDocument 实例枚举它,所以当后续文档具有相同的键时,无需重新分配它们就可以节省时间。
但在您的情况下,您仍然需要通过网络将一整小时的数据拉到您需要的 select 秒。正如我之前所说,我不是 MongoDB 专家,但我怀疑可能有更好的方法来存储这样的数据。要么每秒一个单独的文档(我想这会让索引做更多的工作,并且 MongoDB 可以采用该插入率),要么使用特定的查询结构以便 MongoDB 知道将秒数数组缩短为您请求的数据($splice 是这样做的吗?)
这是一个如何在 {name:"fruit",items:[{name:"apple"},{name:"pear"}]}
q:=TMongoWireQuery.Create(db);
try
q.Query('test',BSON([]));
e:=BSONEnum;
d:=BSON(['items',e]);
d1:=BSON;
while q.Next(d) do
begin
i:=0;
while e.Next(d1) do
begin
Memo1.Lines.Add(d['name']+'#'+IntToStr(i)+d1['name']);
inc(i);
end;
end;
finally
q.Free;
end;