我试图通过批量运行大型查询来减少Django应用程序中的内存使用量。尽管我认为自己很聪明,但内存使用量一直在增长,直到最终这个过程被杀死。我目前有一个理论认为查询集不是垃圾收集的,我想知道它是否与我如何批量查询有关。
def grouper(iterable, n, fillvalue=None):
"Collect data into fixed-length chunks or blocks"
args = [iter(iterable)] * n
return zip_longest(*args, fillvalue=fillvalue)
def get_data(pks):
groups = list(grouper(pks, batch_size))
querysets = [_queryset(pks) for pks in groups]
return itertools.chain(*querysets)
for item in get_data([..big list of primary keys..]):
process(item)
假设在get_data
函数中生成了3个查询集。当我使用了第一个查询集中的所有项目时,该查询集是否会被释放?或者我是否仍然在技术上从链中获得它的参考?
我不确定这里是否存在内存(它可能是数据库驱动程序,Django本身内部的内容等),但它似乎是一个不错的候选者。是否有任何好的工具来测量对象类型的内存使用?这个特殊的代码在Python 2上运行(现在)。
我应该注意到我是从ipython shell运行它,只是因为这很重要。
编辑:
看来chain
在这里不负责任。我添加了一些代码来打印每个类的对象计数,并且Model对象的数量保持不变。
import gc
def get_object_counts():
from collections import Counter
classes = []
for obj in gc.get_objects():
if hasattr(obj, '__class__'):
classes.append(str(obj.__class__))
return Counter(classes)
然后按特定间隔(批量大小):
print(get_object_counts().most_common(30))
为了完成起见,这里排名前9位。我认为主要罪魁祸首是django.db.models.base.ModelState
,它会不断增长而不会被收集。
首先:
("<type 'dict'>", 59184)
("<type 'list'>", 48710)
("<type 'function'>", 48300)
("<type 'tuple'>", 38920)
("<type 'cell'>", 10203)
("<type 'weakref'>", 9957)
("<type 'set'>", 7230)
("<type 'type'>", 5947)
("<class 'django.db.models.base.ModelState'>", 4682)
第二
("<type 'dict'>", 59238)
("<type 'list'>", 48730)
("<type 'function'>", 48315)
("<type 'tuple'>", 38937)
("<type 'cell'>", 10207)
("<type 'weakref'>", 9959)
("<type 'set'>", 7230)
("<type 'type'>", 5950)
("<class 'django.db.models.base.ModelState'>", 4696)
答案 0 :(得分:3)
我自己也看到了类似的行为,似乎itertools.chain
(和itertools.chain.from_iterable
)保留对作为参数传递的任何内容的引用,直到它们停止迭代为止。我希望这就是为什么你的django查询集以及它们的缓存结果不会被垃圾收集。这似乎是python2和python3上的行为,也是您从任何用户定义的python函数中看到的行为(请参阅How to delete a function argument early?)。也许在诸如itertools之类的C语言库中的函数可以更自由地在退出之前删除对参数的引用,但显然他们不会选择这样做。
作为一种变通方法,您可以使用itertools.chain
或iter
本身将各个参数包装到迭代器中的itertools.chain
。看来,一旦这些单独的迭代器耗尽,它们就会删除它们对底层迭代的引用,并允许它被垃圾收集。
最后要注意的是,即使完全消耗结果也不足以释放内存 - 必须在迭代器(或sub)使用的内存之前再次将控件返回到迭代器(或链中的任何子迭代器) -iterator)发布。同样,这是你可能期望的普通python函数。
下面的代码显示了这一切:
from __future__ import print_function
import itertools
import gc
def print_whats_left_after(num, numbers_iter):
""" Read three numbers and print what pairs haven't been gc'd """
for _ in range(num):
next(numbers_iter, None)
gc.collect()
# Print integer pairs that were not garbage collected
print(sorted([o for o in gc.get_objects()
if isinstance(o, list) and len(o) == 2 and
all(isinstance(i, int) for i in o)]))
print_whats_left_after(2, itertools.chain([1, 2], [3, 4]))
# -> [[1, 2], [3, 4]]
print_whats_left_after(3, itertools.chain([1, 2], [3, 4]))
# -> [[1, 2], [3, 4]]
print_whats_left_after(4, itertools.chain([1, 2], [3, 4]))
# -> [[1, 2], [3, 4]]
print_whats_left_after(5, itertools.chain([1, 2], [3, 4]))
# -> []
print_whats_left_after(2, itertools.chain.from_iterable([[1, 2], [3, 4]]))
# -> [[1, 2], [3, 4]]
print_whats_left_after(3, itertools.chain.from_iterable([[1, 2], [3, 4]]))
# -> [[1, 2], [3, 4]]
print_whats_left_after(2, itertools.chain(itertools.chain([1, 2]), [3, 4]))
# -> [[1, 2], [3, 4]]
print_whats_left_after(3, itertools.chain(itertools.chain([1, 2]), [3, 4]))
# -> [[3, 4]] # [1, 2] was gc'd!!!
print_whats_left_after(2, itertools.chain(iter([1, 2]), [3, 4]))
# -> [[1, 2], [3, 4]]
print_whats_left_after(3, itertools.chain(iter([1, 2]), [3, 4]))
# -> [[3, 4]] # [1, 2] was gc'd!!!
print_whats_left_after(4, itertools.chain(iter([1, 2]), [3, 4]))
# -> [[3, 4]]
print_whats_left_after(5, itertools.chain(iter([1, 2]), [3, 4]))
# -> []
def arg_clobberer(arg):
arg = None
yield
print_whats_left_after(0, arg_clobberer([1, 2]))
# -> [[1, 2]]
print_whats_left_after(1, arg_clobberer([1, 2]))
# -> []
def arg_deleter(arg):
del arg
yield
print_whats_left_after(0, arg_deleter([1, 2]))
# -> [[1, 2]]
print_whats_left_after(1, arg_deleter([1, 2]))
# -> []
希望这有帮助!
答案 1 :(得分:0)
不确定但是你可以通过将它们转换为iterables / generators来避免创建临时列表(并在源处修复分配问题):
def get_data(pks):
groups = grouper(pks, batch_size) # turn off explicit list conversion
querysets = (_queryset(pks) for pks in groups) # gencomp not listcomp
return itertools.chain.from_iterable(querysets) # nicer with "from_iterable"