Почему память не выделяется в систему после больших запросов (или серии запросов) в django?

Прежде всего, DEBUG = False в settings.py, поэтому no, connections['default'].queries не растет и не растет, пока не использует всю память.

Давайте начнем с того, что я загрузил таблицу User из django.contrib.auth.models.User с 10000 пользователями (каждый из них назвал 'test #', где # - число от 1 до 10000).

Вот представление:

from django.contrib.auth.models import User
from django.http import HttpResponse

import time

def leak(request):
    print "loading users"

    users = []
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())

    print "sleeping"
    time.sleep(10)

    return HttpResponse('')

Я привязал представление выше к URL-адресу /leak/ и запустил сервер разработки (с DEBUG = False, и я протестировал его, и он не имеет никакого отношения к запуску сервера разработки против других экземпляров).

После запуска:

% curl http://localhost:8000/leak/

Память процесса сервера-хранилища растет примерно до размера, видимого из ps aux вывода ниже, а затем остается на этом уровне.

USER       PID %CPU %MEM    VSZ    RSS TTY      STAT START   TIME COMMAND
dlamotte 25694 11.5 34.8 861384 705668 pts/3    Sl+  19:11   2:52 /home/dlamotte/tmp/django-mem-leak/env/bin/python ./manage.py runserver

Тогда выполнение вышеприведенной команды curl, похоже, не увеличивает использование памяти экземпляра (что я ожидал от истинной утечки памяти?), он должен повторно использовать память? Тем не менее, я чувствую, что здесь что-то не так, что память не получает доступ к системе (однако я понимаю, что это может быть лучшая производительность, что python НЕ выпускает память).

После этого я наивно пытался увидеть, выпустит ли python большие куски памяти, которые он выделил. Поэтому я пытаюсь выполнить следующее из сеанса python:

>>> a = ''
>>> a += 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' * 10000000
>>> del a

Память выделяется в строке a += ..., как ожидалось, но когда del a происходит, память освобождается. Почему поведение django отличается от поведения? Это что-то, что джанго намеревается сделать? Есть ли способ изменить это поведение?

Я буквально провел 2 дня, отлаживая это поведение, не представляя, куда идти дальше (я научился использовать guppy AND objgraph, который, похоже, не указывает на что-либо интересное, что я могу понять).

ОБНОВЛЕНИЕ: Это может быть просто управление памятью python на работе и не имеет ничего общего с Django (предлагается в списке рассылки django-users), но я бы хотел подтвердить, как-то реплицируя это в python вне Django.

ОБНОВЛЕНИЕ: Использование python версии 2.6.5

Ответы

Ответ 1

Я решил переместить свои комментарии в ответ, чтобы сделать все более ясным.

Начиная с Python 2.5, распределение памяти CPython отслеживает использование внутренней памяти небольшим распределителем объектов и пытается вернуть полностью свободные арены в базовую ОС. Это работает большую часть времени, но тот факт, что объекты не могут перемещаться в памяти, означает, что фрагментация может быть серьезной проблемой.

Попробуйте следующий эксперимент (я использовал 3.2, но 2.5+ должен быть похож, если вы используете xrange):

# Create the big lists in advance to avoid skewing the memory counts
seq1 = [None] * 10**6 # Big list of references to None
seq2 = seq1[::10]

# Create and reference a lot of smaller lists
seq1[:] = [[] for x in range(10**6)] # References all the new lists
seq2[:] = seq1[::10] # Grab a second reference to 10% of the new lists

# Memory fragmentation in action
seq1[:] = [None] * 10**6 # 90% of the lists are no longer referenced here
seq2[:] = seq1[::10] # But memory freed only after last 10% are dropped

Обратите внимание, что даже если вы отбрасываете ссылки на seq1 и seq2, приведенная выше последовательность, скорее всего, оставит ваш процесс Python большим количеством дополнительной памяти.

Когда люди говорят о PyPy, используя меньше памяти, чем CPython, это большая часть того, о чем они говорят. Поскольку PyPy не использует ссылки прямого указателя под капотом, он может использовать уплотняющий GC, тем самым избегая большей части проблемы фрагментации и более надежно возвращая память в ОС.

Ответ 2

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

Это может быть даже основная реализация malloc, выполняющая это, или ваша операционная система, сохраняющая определенный объем памяти, выделенный для вашего процесса, хотя процесс явно не использует его. Не цитируйте меня на этом, хотя это было какое-то время, так как я рассматривал такие вещи.

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