Как заставить модели Django быть освобожденными из памяти

Я хочу использовать команду управления для одноразового анализа зданий в штате Массачусетс. Я сократил код ошибки до 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, когда он выходит из области видимости?

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

Ответ 1

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

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

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

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

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

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

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

Чтобы помочь вам понять, какой код использует вашу память и сколько вы можете использовать модуль 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)

Ответ 2

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

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

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

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

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

Попробуйте получить необходимые атрибуты на ходу и выберите целевые здания, используя Django 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 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 переменной 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 который поставляется в профилировщике памяти для генерации этого графа.

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

Если я заменю важный_билдинг .append (сборка) на _ = сборка, используйте меньше памяти

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

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

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

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

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

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

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

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

Ответ 3

Ответ Лорана С вполне уместен (+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: Если вы сможете обновить свой ответ с более подробной информацией, такой как структура ваших моделей и некоторые операции анализа, которые вы пытаетесь выполнить, мы можем предоставить более конкретные ответы, чтобы помочь вам!

Ответ 4

Я не знаком с вашей структурой модели. Но вы могли бы избежать петель

zip_boundary = ZipCode.objects.filter(state='MA').values_list('boundary', flat=True).order_by('id').distinct() # select distinct boundary from ZipCode where ...



output = Building.objects.filter(boundary__within__in=zip_boundary)

output будет иметь Building Queryset который является вашим требуемым выводом (что я понял из вашего вопроса)


Что я здесь сделал?

  1. По вашему фрагменту ZipCode QuerySet используется только для доступа к boundary атрибуту. Мы можем получить значение того же самого выражения SQL DISTINCT. Версия того же Django может быть найдена здесь, "SELECT DISTINCT field_name from table" Django, используя raw sql
  2. Теперь переменная zip_boundary содержит данные boundary релевантности из шага 1 в соответствующем формате DB (целое число, символ и т.д.). Мы используем оператор in для их фильтрации.

Заметка

  1. Операция IN займет немного больше времени в зависимости от вашего размера zip_boundary

  2. Атрибут boundary в вашей модели должен быть полем БД (не может быть property)

Ответ 5

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

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

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

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

Ответ 6

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