Python: поведение сборщика мусора - PullRequest
9 голосов
/ 16 ноября 2009

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

Я провел последние два дня, исследуя это и изучая сборку мусора в Python, и я думаю, что я понимаю, что происходит сейчас - по большей части. При использовании

gc.set_debug(gc.DEBUG_STATS)

Затем для одного запроса я вижу следующий вывод:

>>> c = django.test.Client()
>>> c.get('/the/view/')
gc: collecting generation 0...
gc: objects in each generation: 724 5748 147341
gc: done.
gc: collecting generation 0...
gc: objects in each generation: 731 6460 147341
gc: done.
[...more of the same...]    
gc: collecting generation 1...
gc: objects in each generation: 718 8577 147341
gc: done.
gc: collecting generation 0...
gc: objects in each generation: 714 0 156614
gc: done.
[...more of the same...]
gc: collecting generation 0...
gc: objects in each generation: 715 5578 156612
gc: done.

Таким образом, по существу, огромное количество объектов выделено, но первоначально перемещено в поколение 1, а когда генерация 1 сканируется во время того же запроса, они перемещаются в поколение 2. Если я выполняю gc.collect вручную (2 ) после этого они удаляются. И, как я уже упоминал, там также удаляется, когда происходит следующая автоматическая развертка поколения 2, которая, если я правильно понимаю, будет в этом случае примерно как каждые 10 запросов (на данный момент приложению требуется около 150 МБ).

Хорошо, поэтому сначала я подумал, что в процессе обработки одного запроса могут происходить циклические ссылки, которые не позволяют собирать какие-либо из этих объектов при обработке этого запроса. Тем не менее, я потратил часы, пытаясь найти один из них с использованием pympler.muppy и objgraph, как после, так и путем отладки внутри обработки запроса, и, похоже, их нет. Скорее, кажется, что 14.000 объектов или около того, которые создаются во время запроса, все находятся в цепочке ссылок на некоторый глобальный объект запроса, то есть, как только запрос уходит, их можно освободить.

В любом случае, это была моя попытка объяснить это. Однако, если это так, и циклических зависимостей действительно нет, разве не должно быть освобождено все дерево объектов, если какой-либо объект запроса, который их удерживает, исчезнет, ​​без участия сборщика мусора, чисто на основании количества ссылок падает до нуля?

С этой настройкой, вот мои вопросы:

  • Имеет ли вышесказанное смысл, или мне нужно искать проблему в другом месте? Является ли просто несчастным случаем, что важные данные хранятся так долго в этом конкретном случае использования?

  • Могу ли я что-нибудь сделать, чтобы избежать этой проблемы. Я уже вижу некоторый потенциал для оптимизации представления, но, похоже, это решение с ограниченной областью действия - хотя я не уверен, какой я бы был универсальный; Насколько целесообразно, например, вызвать gc.collect () или gc.set_threshold () вручную?

С точки зрения работы самого сборщика мусора:

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

  • Что произойдет, если gc выполняет, скажем, развертку 1-го поколения и находит объект, на который ссылается объект в поколении 2; следует ли это отношение внутри поколения 2, или оно ожидает, что произойдет развертка поколения 2, прежде чем анализировать ситуацию?

  • При использовании gc.DEBUG_STATS я забочусь прежде всего об информации "объекты в каждом поколении"; тем не менее, я продолжаю получать сотни «gc: 0.0740s истек», «gc: 1258233035.9370s истек». Сообщения; они совершенно неудобны - им требуется много времени для распечатки, и они затрудняют поиск интересных вещей. Есть ли способ избавиться от них?

  • Я не думаю, что есть способ сделать gc.get_objects () по поколениям, то есть получить только объекты из поколения 2, например?

Ответы [ 2 ]

3 голосов
/ 16 ноября 2009

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

Да, это имеет смысл. И да, есть другие вопросы, которые стоит рассмотреть. Django использует threading.local в качестве базы для DatabaseWrapper (и некоторые разработчики используют его, чтобы сделать объект запроса доступным из мест, где он явно не передан). Эти глобальные объекты выдерживают запросы и могут сохранять ссылки на объекты до тех пор, пока в потоке не будет обработано какое-либо другое представление.

Могу ли я что-нибудь сделать, чтобы избежать этой проблемы? Я уже вижу некоторый потенциал для оптимизации представления, но, похоже, это решение с ограниченной областью действия - хотя я не уверен, какой я бы был универсальный; Насколько целесообразно, например, вызывать gc.collect () или gc.set_threshold () вручную?

Общий совет (возможно, вы это знаете, но в любом случае): избегайте циклических ссылок и глобальных слов (включая threading.local). Старайтесь прерывать циклы и очищать глобалы, когда дизайну django трудно их избежать. gc.get_referrers(obj) может помочь вам найти места, требующие внимания. Другой способ - отключить сборщик мусора и вызывать его вручную после каждого запроса, когда это лучше всего сделать (это предотвратит перемещение объектов в следующее поколение).

Я не думаю, что есть способ сделать gc.get_objects () поколением, то есть только получить объекты из поколения 2, например?

К сожалению, это невозможно с интерфейсом gc. Но есть несколько способов пойти. Вы можете рассмотреть конец списка, возвращаемый только gc.get_objects(), так как объекты в этом списке сортируются по поколениям. Вы можете сравнить список с одним из предыдущих вызовов, сохранив слабые ссылки на них (например, в WeakKeyDictionary) между вызовами. Вы можете переписать gc.get_objects() в своем собственном модуле C (это легко, в основном программирование копирования-вставки!), Так как они хранятся внутренним поколением или даже имеют доступ к внутренним структурам с помощью ctypes (требует довольно глубокого понимания ctypes).

2 голосов
/ 16 ноября 2009

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

Я бы посоветовал вам позвонить по номеру gc.collect() и посмотреть, как это повлияет на ваше время отклика и использование памяти.

Обратите внимание также на этот вопрос , который предполагает, что установка DEBUG=True съедает память так, как будто она почти прошла продажу по дате.

...