如何强制释放Django模型的内存。

20
我想使用管理命令对马萨诸塞州的建筑进行一次性分析。我将问题代码简化为一个8行代码段,以演示遇到的问题。注释只是解释我为什么要这样做。我正在原样运行以下代码,在一个空白的管理命令中。

我想使用管理命令对马萨诸塞州的建筑进行一次性分析。我已经将有问题的代码简化成一个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列表占据了大量内存,即使超出范围。如果我用_ = building.pk替换important_buildings.append(building),它就不再消耗太多内存,但是我确实需要这个列表进行一些分析。
因此,我的问题是:当Python超出范围时,如何强制释放Django模型列表? 编辑:我觉得在stackoverflow上有一个小陷阱--如果我写得太详细,没有人愿意花时间去阅读它(它变成了一个不太适用的问题),但如果我写得太少,我会冒着忽略问题的风险。无论如何,我真的很感谢这些答案,并计划在这个周末终于有机会回到这个问题时尝试一些建议!!

你的分析代码是否会在building实例之间创建引用,从而导致引用循环,阻止gc执行其工作? - Laurent S
你是否使用 DEBUG=True 运行这段代码? - Daniel Hepper
@DanielHepper 我已经尝试了两种方法。它似乎会产生一些微小的差异(为什么?),但我不确定为什么会产生这种差异,并且将其设置为False并不能解决问题。 - Teddy Ward
3
提供您的代码最小可重现样本及重现问题的条件可以解决这个进退两难的情况。由于您没有提供这些信息,所以猜测往往会出现。在 Stack Overflow 上,最佳猜测将获得您一半的赏金。 - user10316640
1
以上代码是最小可重现的。任何Django模型都会产生我提到的效果,因为我误解了process.memory_info().rss的工作原理。结果证明,在上面的片段中没有内存问题。出于这个原因,我授予了完整的赏金。 - Teddy Ward
显示剩余3条评论
5个回答

13
非常快速的答案:内存正在被释放,rss不是一个非常准确的工具来告诉我们内存消耗的位置,rss提供了进程已经使用的内存量,而不是进程正在使用的内存量(继续阅读以查看演示),您可以使用memory-profiler包逐行检查函数的内存使用情况。
那么,如何强制释放Django模型所占用的内存?您不能仅使用process.memory_info().rss来解决此类问题。
然而,我可以为您提供优化代码的解决方案,并演示为什么process.memory_info().rss不是衡量某个代码块中正在使用的内存的非常准确的工具。
建议的解决方案:正如本文稍后所示,在列表上应用del不会是解决方案,使用chunk_size优化iterator将有所帮助(请注意,iteratorchunk_size选项是在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's 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”变量时才会访问数据库。当然,您只需要在内存中保存完成分析所需的最小数据。
提前思考。
当您遇到此类问题时,应开始考虑并行性、集群化、大数据等等…阅读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

所以即使释放了一个内存,最后的数字也会更大。这是因为memory_info.rss()是进程已使用的总内存,而不是此时正在使用的内存,如文档中所述:memory_info
下面的图像是相同代码的绘图(内存/时间),但使用了range(10000000) Image against time. 我使用memory-profiler中的脚本mprof生成此图。
您可以看到内存完全被释放,这与使用process.memory_info().rss进行分析时所看到的情况不同。
始终如此,对象列表始终比单个对象使用更多内存。
另一方面,您还可以看到使用的内存并不像您预期的那样呈线性增长。为什么?
从这个出色的site中,我们可以读到:
“amortized” O(1)的append方法通常情况下只需要O(1)的内存即可添加新值,因为大部分情况下已经分配了所需的内存。一旦列表下面的C数组用尽,就必须扩展该数组以适应进一步的添加。这种定期扩展过程相对于新数组的大小是线性的,这似乎与我们声称的O(1)附加矛盾。

然而,扩展速率被巧妙地选择为前一个数组大小的三倍;当我们将扩展成本分摊到每个额外空间提供的附加上时,每个附加的成本在摊销基础上是O(1)。

它很快但有内存成本。

真正的问题不是Django模型没有从内存中释放出来。问题在于您实现的算法/解决方案使用了太多的内存。当然,列表是罪魁祸首。

Django优化的黄金法则:在可能的情况下,用查询集替换列表的使用。


列表并不是问题,因为在循环的单个迭代中它非常小,我的问题是关于在循环的多次迭代中线性累积内存。我仍在使用该列表。但你提供的其他信息,特别是关于内存分析,帮助我诊断了真正的问题。谢谢。 - Teddy Ward
1
我很高兴随时提供帮助。 - Raydel Miranda

10

你没有提供有关模型大小的详细信息,也没有说明它们之间的链接关系,因此这里有一些想法:

默认情况下,QuerySet.iterator() 将在内存中加载 2000 个元素(假设您使用的是 Django >= 2.0)。如果您的 Building 模型包含大量信息,则可能会占用大量内存。您可以尝试将 chunk_size 参数更改为较小的值。

您的 Building 模型是否具有实例之间的链接,可能会导致引用循环,而 gc 找不到?您可以使用 gc 调试功能获取更多详细信息。

或者不必那么麻烦,只需在每次循环结束时调用 del(important_buildings)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)

1
RSS永远不会下降,它是进程已使用的内存量的度量,而不是进程正在使用的内存量。 - Raydel Miranda
2
每次循环结束时调用gc.collect()是否会增加负担?因为在大型系统中评估每个内存对象可能需要相当长的时间。 - Yugandhar Chaudhari

3
Laurent S的回答非常到位(+1并且做得很好:D)。
为了减少内存使用,有一些要考虑的点:
  1. 使用iterator

    可以将迭代器的chunk_size参数设置为能够接受的最小值(例如每个块500个项目)。这会使你的查询变慢(因为迭代器的每一步都会重新评估查询),但会减少内存消耗。

  2. onlydefer选项:

    defer():在某些复杂的数据建模情况下,你的模型可能包含很多字段,其中一些可能包含大量数据(例如文本字段),或需要昂贵的处理才能将它们转换为Python对象。如果你在某种情况下使用queryset的结果,不知道在初始获取数据时是否需要这些特定字段,你可以告诉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:如果您可以更新您的答案,提供更多细节,例如模型的结构和一些您尝试运行的分析操作,我们可能能够提供更具体的答案来帮助您!


0

你考虑过使用Union吗?从你发布的代码来看,你在该命令中运行了很多查询,但是你可以通过使用Union将其转移到数据库中。

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

调整上述内容可以将此函数所需的查询缩小到一个。
另外,如果您还没有看过DjangoDebugToolbar,也值得一看。

0
为了释放内存,您必须将内部循环中每个建筑物的重要详情复制到一个新对象中,以便稍后使用,同时消除不适合的内容。在原始帖子中未显示的代码中存在对内部循环的引用。因此出现了内存问题。通过将相关字段复制到新对象中,可以按预期删除原始对象。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接