我们如何改进 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 选项。我们可以为 odt 使用 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.问题

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;