Как заставить модели Django быть освобожденными из памяти - PullRequest
0 голосов
/ 11 января 2019

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

zips = ZipCode.objects.filter(state='MA').order_by('id')
for zip in zips.iterator():
    buildings = Building.objects.filter(boundary__within=zip.boundary)
    important_buildings = []
    for building in buildings.iterator():
        # Some conditionals would go here
        important_buildings.append(building)
    # Several types of analysis would be done on important_buildings, here
    important_buildings = None

Когда я запускаю этот точный код, я обнаруживаю, что использование памяти постоянно увеличивается с каждым внешним циклом итерации (я использую print('mem', process.memory_info().rss) для проверки использования памяти).

Похоже, что список important_buildings увеличивает объем памяти даже после выхода из области видимости. Если я заменю important_buildings.append(building) на _ = building.pk, он больше не будет занимать много памяти, но мне нужен этот список для некоторых анализов.

Итак, мой вопрос: Как заставить Python выпустить список моделей Django, когда он выходит из области видимости?

Edit: я чувствую, что есть некоторая ловушка 22 при переполнении стека - если я напишу слишком много деталей, никто не захочет тратить время на его чтение (и это становится менее подходящей проблемой), но если я пишите слишком мало деталей, я рискую пропустить часть проблемы. В любом случае, я действительно ценю ответы и планирую опробовать некоторые предложения на этих выходных, когда у меня наконец появится возможность вернуться к этому !!

Ответы [ 5 ]

0 голосов
/ 22 января 2019

Чтобы освободить память, вы должны продублировать важные детали каждого из зданий во внутреннем цикле в новый объект, который будет использоваться позже, исключая те, которые не подходят. В коде, не показанном в оригинальном сообщении, существуют ссылки на внутренний цикл. Таким образом, проблемы с памятью. Копируя соответствующие поля в новые объекты, оригиналы можно удалять по назначению.

0 голосов
/ 21 января 2019

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

combined_area = FooModel.objects.filter(...).aggregate(area=Union('geom'))['area']
final = BarModel.objects.filter(coordinates__within=combined_area)

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

Также стоит посмотреть на DjangoDebugToolbar - если вы еще не смотрели его, то это

0 голосов
/ 17 января 2019

Очень быстрый ответ : память освобождается, rss не очень точный инструмент для определения , где используется память , rss дает меру память, в которой процесс использовал , а не память, в которой процесс использует (продолжайте чтение, чтобы увидеть демонстрацию), вы можете использовать пакет memory-profiler для строка за строкой проверяет использование памяти вашей функцией.

Итак, как заставить модели Django быть освобожденными из памяти? Вы не можете сказать, есть такая проблема, просто используя process.memory_info().rss.

Однако я могу предложить вам решение для оптимизации вашего кода. И напишите демо о том, почему process.memory_info().rss не очень точный инструмент для измерения памяти , используемой в каком-то блоке кода.

Предлагаемое решение : как показано ниже в этом же посте, применение del к списку не будет решением, оптимизация с использованием chunk_size для iterator поможет (знайте, * Опция 1028 * для iterator была добавлена ​​в Django 2.0), это точно, но настоящий враг здесь - этот противный список.

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

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

for zip in zips.iterator(): # Using chunk_size here if you're working with Django >= 2.0 might help.
    important_buildings = Building.objects.filter(
        boundary__within=zip.boundary,
        # Some conditions here ... 

        # You could even use annotations with conditional expressions
        # as Case and When.

        # Also Q and F expressions.

        # It is very uncommon the use case you cannot address 
        # with Django's ORM.

        # Ultimately you could use raw SQL. Anything to avoid having
        # a list with the whole object.
    )

    # And then just load into the list the data you need
    # to perform your analysis.

    # Analysis according size.
    data = important_buildings.values_list('size', flat=True)

    # Analysis according height.
    data = important_buildings.values_list('height', flat=True)

    # Perhaps you need more than one attribute ...
    # Analysis according to height and size.
    data = important_buildings.values_list('height', 'size')

    # Etc ...

Очень важно отметить, что если вы используете такое решение, вы будете попадать в базу данных только при заполнении переменной data. И, конечно же, в вашей памяти будет только тот минимум, который необходим для выполнения анализа.

Заранее думая.

Когда вы сталкиваетесь с такими проблемами, вы должны начать думать о параллелизме, кластеризации, больших данных и т. Д. ... Читайте также о ElasticSearch , у него очень хорошие возможности анализа.

Демо

process.memory_info().rss Не буду рассказывать об освобождении памяти.

Я был действительно заинтригован вашим вопросом и тем фактом, который вы описали здесь:

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

Действительно, кажется, но это не так. Посмотрите следующий пример:

from psutil import Process

def memory_test():
    a = []
    for i in range(10000):
        a.append(i)
    del a

print(process.memory_info().rss)  # Prints 29728768
memory_test()
print(process.memory_info().rss)  # Prints 30023680

Так что даже если a память освобождена, последний номер больше. Это связано с тем, что memory_info.rss() - это общая память, используемая процессом , а не память , использующая на данный момент, как указано здесь в документации: memory_info .

На следующем рисунке показан график (память / время) для того же кода, что и раньше, но с range(10000000)

Image against time. Я использую скрипт mprof, который поставляется в memory-profiler для генерации этого графика.

Вы видите, что память полностью освобождена, это не то, что вы видите при профилировании с использованием process.memory_info().rss

Если я заменим Important_buildings.append (сборка) на _ = сборка, используйте меньше памяти

Так будет всегда, список объектов всегда будет использовать больше памяти, чем один объект.

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

С этого превосходного сайта мы можем прочитать:

Метод добавления «амортизируется» O (1). В большинстве случаев память, необходимая для добавления нового значения, уже выделена, что строго равно O (1). После того, как массив C, лежащий в основе списка, был исчерпан, он должен быть расширен, чтобы вместить дальнейшие добавления. Этот периодический процесс расширения является линейным по отношению к размеру нового массива, что, кажется, противоречит нашему утверждению, что добавление - это O (1).

Однако, скорость расширения выбирается умно, чтобы она была в три раза больше предыдущего размера массива ; Когда мы распределяем стоимость расширения по каждому дополнительному приложению, предоставленному этим дополнительным пространством, стоимость за добавление составляет O (1) на амортизированной основе.

Это быстро, но имеет стоимость памяти.

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

Золотое правило для оптимизации Django: замените использование списка для квестов, где только можете.

0 голосов
/ 17 января 2019

Ответ Лорана S вполне уместен (+1 и хорошо с моей стороны: D).

Есть несколько моментов, которые следует учитывать, чтобы сократить использование памяти:

  1. Использование iterator:

    Вы можете установить для параметра chunk_size итератора что-то настолько маленькое, насколько это возможно (например, 500 элементов на блок).
    Это замедлит ваш запрос (так как каждый шаг итератора будет переоценивать запрос), но это сократит потребление памяти.

  2. Опции only и defer:

    defer(): в некоторых сложных ситуациях моделирования данных ваши модели могут содержать много полей, некоторые из которых могут содержать много данных (например, текстовые поля) , или требуется дорогостоящая обработка для преобразования их в объекты Python. Если вы используете результаты набора запросов в какой-то ситуации, когда вы не знаете, нужны ли вам эти конкретные поля при первоначальном извлечении данных, вы можете указать Django не извлекать их из базы данных.

    only(): Более или менее противоположно defer(). Вы называете это с полями, которые не должны быть отложены при получении модели. Если у вас есть модель, в которой почти все поля необходимо отложить, использование only () для указания дополнительного набора полей может привести к упрощению кода.

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

  3. Если ваш запрос по-прежнему занимает слишком много памяти, вы можете оставить только building_id в вашем списке important_buildings, а затем использовать этот список для выполнения запросов, необходимых для модели Building. , для каждой из ваших операций (это замедлит ваши операции, но сократит использование памяти).

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

Теперь давайте попробуем объединить все вышеперечисленные пункты в вашем примере кода:

# You don't use more than the "boundary" field, so why bring more?
# You can even use "values_list('boundary', flat=True)"
# except if you are using more than that (I cannot tell from your sample)
zips = ZipCode.objects.filter(state='MA').order_by('id').only('boundary')
for zip in zips.iterator():
    # I would use "set()" instead of list to avoid dublicates
    important_buildings = set()

    # Keep only the essential fields for your operations using "only" (or "defer")
    for building in Building.objects.filter(boundary__within=zip.boundary)\
                    .only('essential_field_1', 'essential_field_2', ...)\
                    .iterator(chunk_size=500):
        # Some conditionals would go here
        important_buildings.add(building)

Если это все еще требует слишком много памяти для вашего вкуса, вы можете использовать третий пункт, описанный выше:

zips = ZipCode.objects.filter(state='MA').order_by('id').only('boundary')
for zip in zips.iterator():
    important_buildings = set()
    for building in Building.objects.filter(boundary__within=zip.boundary)\
                    .only('pk', 'essential_field_1', 'essential_field_2', ...)\
                    .iterator(chunk_size=500):
        # Some conditionals would go here

        # Create a set containing only the important buildings' ids
        important_buildings.add(building.pk)

, а затем используйте этот набор для запроса ваших зданий для остальных операций:

# Converting set to list may not be needed but I don't remember for sure :)
Building.objects.filter(pk__in=list(important_buildings))...

PS: Если вы можете обновить свой ответ с более подробной информацией, такой как структура ваших моделей и некоторые операции анализа, которые вы пытаетесь выполнить, мы можем предоставить более конкретные ответы, чтобы помочь вы!

0 голосов
/ 15 января 2019

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

По умолчанию QuerySet.iterator() будет загружать 2000 элементов в память (при условии, что вы используете django> = 2.0). Если ваша модель Building содержит много информации, это может привести к увеличению объема памяти. Вы можете попробовать изменить параметр chunk_size на более низкий.

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

Или короткое замыкание вышеупомянутой идеи, возможно, просто вызовите del(important_buildings) и del(buildings) с последующим gc.collect() в конце каждого цикла для принудительного сбора мусора?

Область ваших переменных - это функция, а не просто цикл for, поэтому может помочь разбиение кода на более мелкие функции. Хотя обратите внимание, что сборщик мусора python не всегда возвращает память операционной системе, поэтому, как объяснено в в этом ответе , вам может потребоваться принять более жестокие меры, чтобы увидеть, как rss выключится.

Надеюсь, это поможет!

EDIT:

Чтобы помочь вам понять, какой код использует вашу память и сколько вы можете использовать модуль tracemalloc , например, используя предложенный код:

import linecache
import os
import tracemalloc

def display_top(snapshot, key_type='lineno', limit=10):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        # replace "/path/to/module/file.py" with "module/file.py"
        filename = os.sep.join(frame.filename.split(os.sep)[-2:])
        print("#%s: %s:%s: %.1f KiB"
              % (index, filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))

tracemalloc.start()

# ... run your code ...

snapshot = tracemalloc.take_snapshot()
display_top(snapshot)
...