Загрузка наборов данных из хранилища данных и объединение в один словарь.Ресурсная проблема - PullRequest
1 голос
/ 11 мая 2010

У меня есть база данных продуктов, которая содержит продукты, детали и этикетки для каждой детали на основе langcodes.

Проблема, с которой я сталкиваюсь и которой не могу обойтись, заключается в огромном количестве ресурсов, используемых для получения различных наборов данных и объединения их в диктовку в соответствии с моими потребностями.

Продукты в базе данных основаны на ряде частей определенного типа (т. Е. Цвет, размер). И каждая часть имеет ярлык для каждого языка. Для этого я создал 4 разные модели. Продукты, ProductParts, ProductPartTypes и ProductPartLabels.

Я сузил его примерно до 10 строк кода, которые используются для генерации проблемы. На данный момент у меня есть 3 продукта, 3 типа, 3 части для каждого типа и 2 языка. И запрос занимает wooping 5500ms для генерации.

for product in productData:
        productDict = {}
        typeDict = {}
        productDict['productName'] = product.name

        cache_key = 'productparts_%s' % (slugify(product.key()))
        partData = memcache.get(cache_key)

        if not partData:
            for type in typeData:
                typeDict[type.typeId] = { 'default' : '', 'optional' : [] }
            ## Start of problem lines ##
            for defaultPart in product.defaultPartsData:
                for label in labelsForLangCode:
                    if label.key() in defaultPart.partLabelList:
                        typeDict[defaultPart.type.typeId]['default'] = label.partLangLabel

            for optionalPart in product.optionalPartsData:
                for label in labelsForLangCode:
                    if label.key() in optionalPart.partLabelList:
                        typeDict[optionalPart.type.typeId]['optional'].append(label.partLangLabel)
            ## end problem lines ##
            memcache.add(cache_key, typeDict, 500)
            partData = memcache.get(cache_key)

        productDict['parts'] = partData    
        productList.append(productDict)

Полагаю, проблема заключается в том, что число циклов for слишком велико, и им приходится повторять одни и те же данные снова и снова. labelForLangCode получает все метки из ProductPartLabels, которые соответствуют текущему langCode.

Все детали для продукта хранятся в db.ListProperty (db.key). То же самое касается всех этикеток для детали.

Причина, по которой мне нужен сложный диктат , заключается в том, что я хочу отобразить все данные для продукта с его частями по умолчанию и показать селектор для дополнительного.

Параметры defaultPartsData и optionaPartsData - это свойства в модели продукта, которые выглядят следующим образом:

@property
def defaultPartsData(self):
    return ProductParts.gql('WHERE __key__ IN :key', key = self.defaultParts)

@property
def optionalPartsData(self):
    return ProductParts.gql('WHERE __key__ IN :key', key = self.optionalParts)

Когда завершенный dict находится в memcache, он работает плавно, но не сбрасывается ли memcache, если приложение переходит в режим гибернации? Также я хотел бы показать страницу для первого пользователя (memcache empty) без огромной задержки.

Также, как я сказал выше, это только небольшое количество деталей / продукта. Каким будет результат, когда будет 30 продуктов из 100 частей.

Является ли одним из решений создание запланированной задачи для кэширования ее в memcache каждый час? Это эффективно?

Я знаю, что это много, но я застрял. Я занимаюсь этим около 12 часов подряд. И не могу найти решение.

.. Фредрик

EDIT:

Снимок экрана AppStats здесь .

Из того, что я могу читать запросы, выглядит нормально в AppStats. только занимает около 200-400 мс. Как разница может быть такой большой?

РЕДАКТИРОВАТЬ 2:

Я реализовал решение dound и добавил abit. Теперь это выглядит так:

langCode = 'en'
    typeData = Products.ProductPartTypes.all()
    productData = Products.Product.all()
    labelsForLangCode = Products.ProductPartLabels.gql('WHERE partLangCode = :langCode', langCode = langCode)
    productList = []

    label_cache_key = 'productpartslabels_%s' % (slugify(langCode))
    labelData = memcache.get(label_cache_key)

    if labelData is None:
        langDict = {}
        for langLabel in labelsForLangCode:
            langDict[str(langLabel.key())] = langLabel.partLangLabel

        memcache.add(label_cache_key, langDict, 500)
        labelData = memcache.get(label_cache_key)

    GQL_PARTS_BY_PRODUCT = Products.ProductParts.gql('WHERE products = :1')
    for product in productData:
        productDict = {}
        typeDict = {}
        productDict['productName'] = product.name

        cache_key = 'productparts_%s' % (slugify(product.key()))
        partData = memcache.get(cache_key)

        if partData is None:
            for type in typeData:
                typeDict[type.typeId] = { 'default' : '', 'optional' : [] }

            GQL_PARTS_BY_PRODUCT.bind(product)
            parts = GQL_PARTS_BY_PRODUCT.fetch(1000)
            for part in parts:
                for lb in part.partLabelList:
                    if str(lb) in labelData:
                        label = labelData[str(lb)]
                        break

                if part.key() in product.defaultParts:
                    typeDict[part.type.typeId]['default'] = label
                elif part.key() in product.optionalParts:
                    typeDict[part.type.typeId]['optional'].append(label)

            memcache.add(cache_key, typeDict, 500)
            partData = memcache.get(cache_key)

        productDict['parts'] = partData    
        productList.append(productDict) 

Результат намного лучше. Теперь у меня около 3000 мс без memcache и около 700 мс с

Я все еще переживаю за 3000 мс, и на локальном сервере app_dev memcache заполняется при каждой перезагрузке. Разве не надо все туда помещать, а потом читать с него?

И последнее, но не менее важное: кто-нибудь знает, почему на производственном сервере запрос занимает в 10 раз больше времени, чем app_dev?

РЕДАКТИРОВАТЬ 3: Я заметил, что не из db.Model проиндексированы, может ли это сделать разницу?

РЕДАКТИРОВАТЬ 4: После консультации с AppStats (и понимания этого потребовалось некоторое время. Похоже, что большие проблемы лежат в part.type.typeId, где part.type - это db.ReferenceProperty. Должен был увидеть это раньше. И, возможно, объяснить это лучше :) Я ' Я переосмыслил эту часть. И вернемся к вам.

.. Фредрик

Ответы [ 3 ]

2 голосов
/ 11 мая 2010

Несколько простых идей:

1) Поскольку вам нужны все результаты, вместо того, чтобы делать цикл for, как у вас, явно вызовите fetch (), чтобы просто продолжить и получить все результаты сразу. В противном случае цикл for может привести к нескольким запросам к хранилищу данных, поскольку он получает только столько элементов одновременно. Например, возможно, вы могли бы попробовать:

return ProductParts.gql('WHERE __key__ IN :key', key = self.defaultParts).fetch(1000)

2) Может быть, загружать только часть данных в первоначальном запросе. Затем используйте методы AJAX для загрузки дополнительных данных по мере необходимости. Например, начните с возврата информации о продукте, а затем сделайте дополнительные запросы AJAX, чтобы получить детали.

3) Как указал Уилл, IN запросов выполняют один запрос на аргумент.

  • Проблема : запрос IN выполняет один запрос на равенство для каждого аргумента, который вы ему даете. Так что <strong>key</strong> IN self.defaultParts на самом деле выполняет len(self.defaultParts) запросов.
  • Возможное улучшение : Попробуйте еще больше денормализовать ваши данные. В частности, храните список продуктов, в которых каждая часть используется в каждой части. Вы можете структурировать свою модель деталей следующим образом:
    class ProductParts(db.Model):
        ...
        products = db.ListProperty(db.Key)  # product keys
        ...
  • Затем вы можете выполнить ОДИН запрос к каждому продукту вместо N запросов к каждому продукту. Например, вы можете сделать это:

parts = ProductParts.all().filter("products =", product).fetch(1000)

  • Компромисс? Вам необходимо хранить больше данных в каждой сущности ProductParts. Кроме того, когда вы пишете сущность ProductParts, она будет немного медленнее, потому что это приведет к записи 1 строки в индексе для каждого элемента в вашем свойстве списка. Тем не менее, вы заявили, что у вас есть только 100 продуктов, поэтому даже если бы деталь использовалась в каждом продукте, список все равно не был бы слишком большим (Ник Джонсон отмечает здесь , что у вас не будет проблем, пока попробуйте проиндексировать свойство списка с помощью ~ 5000 элементов).

Менее критическая идея улучшения:

4) Вы можете создать объект GqlQuery ОДИН РАЗ, а затем повторно использовать его. Это не ваша основная проблема с производительностью, но это немного поможет. Пример:

GQL_PROD_PART_BY_KEYS = ProductParts.gql('WHERE __key__ IN :1')
@property
def defaultPartsData(self):
    return GQL_PROD_PART_BY_KEYS.bind(self.defaultParts)

Вам также следует использовать AppStats , чтобы вы могли точно понять, почему ваш запрос занимает так много времени. Возможно, вы даже захотите опубликовать снимок экрана с информацией о вашем запросе вместе со своим сообщением.


Вот как может выглядеть код, если вы переписали его и извлекли данные с меньшим количеством обращений к хранилищу данных (эти изменения основаны на идеях # 1, # 3 и # 4 выше).

GQL_PARTS_BY_PRODUCT = ProductParts.gql('WHERE products = :1')
for product in productData:
    productDict = {}
    typeDict = {}
    productDict['productName'] = product.name

    cache_key = 'productparts_%s' % (slugify(product.key()))
    partData = memcache.get(cache_key)

    if not partData:
        for type in typeData:
            typeDict[type.typeId] = { 'default' : '', 'optional' : [] }

        # here's a new approach that does just ONE datastore query (for each product)
        GQL_PARTS_BY_PRODUCT.bind(product)
        parts = GQL_PARTS_BY_PRODUCT.fetch(1000)
        for part in parts:
            if part.key() in self.defaultParts:
                part_type = 'default'
            else:
                part_type = 'optional'

            for label in labelsForLangCode:
                if label.key() in defaultPart.partLabelList:
                    typeDict[defaultPart.type.typeId][part_type] = label.partLangLabel
        # (end new code)
        memcache.add(cache_key, typeDict, 500)
        partData = memcache.get(cache_key)

    productDict['parts'] = partData    
    productList.append(productDict)
1 голос
/ 11 мая 2010

Важно помнить, что IN запросы (вместе с != запросами) приводят к тому, что несколько подзапросов порождаются за кулисами, и существует ограничение в 30 подзапросов.

Таким образом, ваш запрос ProductParts.gql('WHERE __key__ IN :key', key = self.defaultParts) на самом деле вызовет len(self.defaultParts) подзапросов за кулисами, и он потерпит неудачу, если len(self.defaultParts) больше 30.

Вот соответствующий раздел из GQL Reference :

Примечание: Операторы IN и != используют несколько запросов за сценой. Например, оператор IN выполняет отдельный базовый запрос хранилища данных для каждого элемента в списке. Возвращенные объекты являются результатом перекрестного продукта всех базовых запросов к хранилищу данных и дедуплицируются. Для каждого отдельного запроса GQL допускается не более 30 запросов к хранилищу данных.

Вы можете попробовать установить AppStats для своего приложения, чтобы увидеть, где еще оно может замедляться.

0 голосов
/ 11 мая 2010

Я думаю, что проблема заключается в дизайне: желание создать реляционную таблицу соединений в memcache, когда фреймворк специально этого не терпит.

GAE бросит вашу работу, потому что это занимает слишком много времени, но вы не должны делать это в первую очередь. Я сам GAE Tyro, поэтому, к сожалению, я не могу указать, как это сделать.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...