与原生pymongo使用相比,Mongoengine在大型文档上的速度非常慢

Bar*_*man 12 python mongodb pymongo mongoengine

我有以下mongoengine型号:

class MyModel(Document):
    date        = DateTimeField(required = True)
    data_dict_1 = DictField(required = False)
    data_dict_2 = DictField(required = True)
Run Code Online (Sandbox Code Playgroud)

在某些情况下,DB中的文档可能非常大(大约5-10MB),而data_dict字段包含复杂的嵌套文档(dicts列表的字典等等).

我遇到过两个(可能是相关的)问题:

  1. 当我运行本机pymongo find_one()查询时,它会在一秒钟内返回.当我运行MyModel.objects.first()时需要5-10秒.
  2. 当我从数据库查询单个大型文档,然后访问其字段时,只需执行以下操作需要10-20秒:

    m = MyModel.objects.first()
    val = m.data_dict_1.get(some_key)
    
    Run Code Online (Sandbox Code Playgroud)

对象中的数据不包含对任何其他对象的任何引用,因此它不是取消引用的对象的问题.
我怀疑它与mongoengine的内部数据表示的某些低效率有关,这会影响文档对象的构造以及字段访问.我能做些什么来改善这个吗?

Ste*_*ter 32

TL; DR:mongoengine正在花费多年时间将所有返回的数组转换为dicts

为了测试这个,我构建了一个DictField带有大嵌套文档的集合dict.该文档大致在5-10MB范围内.

然后我们可以timeit.timeit使用pymongo和mongoengine来确认读数的差异.

然后我们可以使用pycallgraphGraphViz来看看是什么让mongoengine这么长.

这是完整的代码:

import datetime
import itertools
import random
import sys
import timeit
from collections import defaultdict

import mongoengine as db
from pycallgraph.output.graphviz import GraphvizOutput
from pycallgraph.pycallgraph import PyCallGraph

db.connect("test-dicts")


class MyModel(db.Document):
    date = db.DateTimeField(required=True, default=datetime.date.today)
    data_dict_1 = db.DictField(required=False)


MyModel.drop_collection()

data_1 = ['foo', 'bar']
data_2 = ['spam', 'eggs', 'ham']
data_3 = ["subf{}".format(f) for f in range(5)]

m = MyModel()
tree = lambda: defaultdict(tree)  # http://stackoverflow.com/a/19189366/3271558
data = tree()
for _d1, _d2, _d3 in itertools.product(data_1, data_2, data_3):
    data[_d1][_d2][_d3] = list(random.sample(range(50000), 20000))
m.data_dict_1 = data
m.save()


def pymongo_doc():
    return db.connection.get_connection()["test-dicts"]['my_model'].find_one()


def mongoengine_doc():
    return MyModel.objects.first()


if __name__ == '__main__':
    print("pymongo took {:2.2f}s".format(timeit.timeit(pymongo_doc, number=10)))
    print("mongoengine took", timeit.timeit(mongoengine_doc, number=10))
    with PyCallGraph(output=GraphvizOutput()):
        mongoengine_doc()
Run Code Online (Sandbox Code Playgroud)

输出证明与pymongo相比,mongoengine非常慢:

pymongo took 0.87s
mongoengine took 25.81118331072267
Run Code Online (Sandbox Code Playgroud)

生成的调用图清楚地说明了瓶颈的位置:

pycallgraph.png为mongoengine读取大文档 pycallgraph的热点

基本上mongoengine会在DictField从db返回的每一个上调用to_python方法.to_python很慢,在我们的例子中,它被称为疯狂的次数.

Mongoengine用于优雅地将文档结构映射到python对象.如果你有非常大的非结构化文件(mongodb非常适合),那么mongoengine并不是真正的工具,你应该只使用pymongo.

但是,如果您知道结构,则可以使用EmbeddedDocument字段从mongoengine获得稍微更好的性能.我在这个要点中运行了一个类似但不相同的测试代码,输出是:

pymongo with dict took 0.12s
pymongo with embed took 0.12s
mongoengine with dict took 4.3059175412661075
mongoengine with embed took 1.1639373211854682
Run Code Online (Sandbox Code Playgroud)

所以你可以让mongoengine更快,但pymongo仍然更快.

UPDATE

这里pymongo接口的一个很好的捷径就是使用聚合框架:

def mongoengine_agg_doc():
    return list(MyModel.objects.aggregate({"$limit":1}))[0]
Run Code Online (Sandbox Code Playgroud)