Утечка памяти в Django ORM
Предыстория. Напарсил полмиллиона записей и решил их скриптом (на базе моделей и ORM django) обработать. Но вот незадача - на 143 тысяче скрипт умирал от нехватки памяти.
Стал я, значит, думать, кто виноват и что делать. Конечно, легче всего было свалить всё на django ORM. что я и сделал ) Найдя виноватого, я с чистой совестью пошёл читать всяческие документации и дискуссии в django developers на тему мемори ликов. Выяснил две вещи:
- я неправильно понимал работу django ORM
- memory leaks в django orm отсутствуют
Теперь по порядку. Ранее я думал, что если я пишу
то делается один select запрос, а потом данные вытягиваются по одной строчке из таблицы. Оказывается, всё не так. Данные вытягиваютя сразу ВСЕ. Т.е. если у вас есть полмиллиона строк в таблице модели Foo, они вытянутся сразу все, причём будет создано полмиллиона объектов модели Foo. Частично проблему можно решить использовав:
Насколько я понял, данные будут по прежнему загружаться одним махом из БД, НО объекты модели Foo будут создаваться по одному на каждой итерации. Также из обсуждения в django developers я понял, что после query set refactoring проблемы загрузки все данных за один раз не будет. Ну, а пока, дабы не забивать память сервера до отказа, можно использовать различные workarounds. Например, делать серию запросов с ограничениями по количеству записей. Приведу конкретный пример, из которого вам станет всё понятно
Таким образом вы будете работать только с небольшими объёмами информации, тем самым разгрузив оперативную память сервера.
Переходим ко второму пункту, который гласит о том, что в django ORM нет утечек памяти. Почему же тогда при большом количестве итераций я наблюдал через top постоянную утечку памяти? Ответ я нашёл в faq )
В результате мой скрипт приобрёл следующий вид.
Пока я писал это сообщение, скрипт обработал 250000 записей, что составляет половину от их общего количества. Думаю, скрипт успешно завершит свою работу. По показаниям top размер потребляемой памяти зафиксировался на 11 мегабайтах.
UPD: Проще всего отлючить запросы таким образом: в начале скрипта пишем
Стал я, значит, думать, кто виноват и что делать. Конечно, легче всего было свалить всё на django ORM. что я и сделал ) Найдя виноватого, я с чистой совестью пошёл читать всяческие документации и дискуссии в django developers на тему мемори ликов. Выяснил две вещи:
- я неправильно понимал работу django ORM
- memory leaks в django orm отсутствуют
Теперь по порядку. Ранее я думал, что если я пишу
for foo in Foo.objects.all():
то делается один select запрос, а потом данные вытягиваются по одной строчке из таблицы. Оказывается, всё не так. Данные вытягиваютя сразу ВСЕ. Т.е. если у вас есть полмиллиона строк в таблице модели Foo, они вытянутся сразу все, причём будет создано полмиллиона объектов модели Foo. Частично проблему можно решить использовав:
for foo in Foo.objects.all().iterator():
Насколько я понял, данные будут по прежнему загружаться одним махом из БД, НО объекты модели Foo будут создаваться по одному на каждой итерации. Также из обсуждения в django developers я понял, что после query set refactoring проблемы загрузки все данных за один раз не будет. Ну, а пока, дабы не забивать память сервера до отказа, можно использовать различные workarounds. Например, делать серию запросов с ограничениями по количеству записей. Приведу конкретный пример, из которого вам станет всё понятно
total = Text.objects.all().count()
step_size = 1000
for step in xrange(0, total, step_size):
for text in Text.objects.all().order_by('id')[step:step + step_size]:
print count
text.number = count
text.save()
count += 1
Таким образом вы будете работать только с небольшими объёмами информации, тем самым разгрузив оперативную память сервера.
Переходим ко второму пункту, который гласит о том, что в django ORM нет утечек памяти. Почему же тогда при большом количестве итераций я наблюдал через top постоянную утечку памяти? Ответ я нашёл в faq )
Why is Django leaking memory?
Django isn’t known to leak memory. If you find your Django processes are allocating more and more memory, with no sign of releasing it, check to make sure your DEBUG setting is set to True. If DEBUG is True, then Django saves a copy of every SQL statement it has executed.
(The queries are saved in django.db.connection.queries. See How can I see the raw SQL queries Django is running?.)
To fix the problem, set DEBUG to False.
If you need to clear the query list manually at any point in your functions, just call reset_queries(), like this:
from django import db
db.reset_queries()
В результате мой скрипт приобрёл следующий вид.
from django import db
from text.models import Text
total = Text.objects.all().count()
step_size = 1000
count = 1
for step in xrange(0, total, step_size):
for text in Text.objects.all().order_by('id')[step:step + step_size]:
print count
text.number = count
text.save()
count += 1
db.reset_queries()
Пока я писал это сообщение, скрипт обработал 250000 записей, что составляет половину от их общего количества. Думаю, скрипт успешно завершит свою работу. По показаниям top размер потребляемой памяти зафиксировался на 11 мегабайтах.
UPD: Проще всего отлючить запросы таким образом: в начале скрипта пишем
from django.conf import settings
settings.DEBUG = False




















Comments
Отличная статья!
Отличная заметка, спасибо. (…побежал выключать Debug=True)
Прикольно, не знал про эту фичу с сохранением всех запросов при дебаге. :-) Спасибо.
Действительно чем дальше, тем интереснее, вообще после соскакивания с php, связка django/python мне нравится всё больше и больше.
Как-то сочетание django/python слух/глаз режет, а что ещё есть связка django/php? )
Ну к примеру можно привести сочетание Zope/python, turbogears/python. Суть та же. )
ЗЫ. А django/php - это просто должно звучать гордо :)
Кстати openid глючит на подтверждении завалился в ошибку.
Нет, это не так. Как следует из кода в db.models.query._QuerySet, данные будут фетчится из базы кусками по GET_ITERATOR_CHUNK_SIZE строк. По-умолчанию по 100 строк.