Mongoengine очень медленно на больших документах по сравнению с родным использованием pymongo

у меня есть следующая модель mongoengine:

class MyModel(Document):
    date        = DateTimeField(required = True)
    data_dict_1 = DictField(required = False)
    data_dict_2 = DictField(required = True)

в некоторых случаях документ в БД может быть очень большим (около 5-10 МБ), а поля data_dict содержат сложные вложенные документы (dict списков dicts и т. д...).

я столкнулся с двумя (возможно, связанными) проблемами:

  1. когда я запускаю собственный запрос pymongo find_one (), он возвращается в течение секунды. Когда я запускаю MyModel.объекты.во-первых (), это занимает 5-10 секунд.
  2. Когда Я запрос одного большого документа из БД, а затем доступ к его полю, занимает 10-20 секунд, чтобы сделать следующее:

    m = MyModel.objects.first()
    val = m.data_dict_1.get(some_key)
    

данные в объекте не содержат ссылок на какие-либо другие объекты, поэтому это не проблема разыменования объектов.
Я подозреваю, что это связано с некоторой неэффективностью внутреннего представления данных mongoengine, что влияет на построение объекта документа, а также доступ к полям. Я могу что-нибудь сделать? улучшить это ?

1 ответов


TL; DR: mongoengine тратит века на преобразование всех возвращенных массивов в dicts

чтобы проверить это, я создал коллекцию с документом с DictField С большим вложенным dict. Док находится примерно в вашем диапазоне 5-10MB.

мы можем использовать timeit.timeit чтобы подтвердить разницу в чтениях с помощью pymongo и mongoengine.

мы можем использовать pycallgraph и GraphViz чтобы увидеть, что 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()

и выход доказывает, что mongoengine очень медленно по сравнению с pymongo:

pymongo took 0.87s
mongoengine took 25.81118331072267

результирующий график вызовов довольно ясно показывает, где находится горлышко бутылки:

pycallgraph.png for mongoengine read of large doc hot spot in pycallgraph

по существу mongoengine вызовет метод to_python на каждом DictField что он вернется из БД. to_python довольно медленно, и в нашем примере это называется безумным количеством раз.

Mongoengine используется для элегантного сопоставления структуры документа с объектами python. Если у вас очень большие неструктурированные документы (для которых mongodb отлично подходит), то mongoengine не является правильным инструментом, и вы должны просто использовать pymongo.

однако, если вы знаете структуру, вы можете использовать EmbeddedDocument поля, чтобы получить немного лучшую производительность от mongoengine. Я бежал аналогичный, но не эквивалентный тест код в этом gist и выход:

pymongo with dict took 0.12s
pymongo with embed took 0.12s
mongoengine with dict took 4.3059175412661075
mongoengine with embed took 1.1639373211854682

таким образом, вы можете сделать mongoengine быстрее, но pymongo намного быстрее.

обновление

хороший ярлык для интерфейса pymongo здесь, чтобы использовать структуру агрегации:

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