如何强制 Django 模型从内存中释放

Posted

技术标签:

【中文标题】如何强制 Django 模型从内存中释放【英文标题】:How to force Django models to be released from memory 【发布时间】:2019-06-06 17:17:39 【问题描述】:

我想使用管理命令对马萨诸塞州的建筑物进行一次性分析。我已将有问题的代码减少为 8 行 sn-p,以演示我遇到的问题。 cmets 只是解释了我为什么要这样做。我在一个空白的管理命令中逐字运行下面的代码

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 列表正在占用内存,即使超出范围也是如此。如果我将important_buildings.append(building) 替换为_ = building.pk,它将不再消耗太多内存,但我确实需要该列表进行一些分析。

所以,我的问题是:如何强制 Python 在超出范围时释放 Django 模型列表?

编辑:我觉得堆栈溢出有一点点 22 ——如果我写了太多细节,没有人愿意花时间阅读它(这成为一个不太适用的问​​题),但如果我写得太详细了,我冒着忽略部分问题的风险。无论如何,我真的很感谢这些答案,并计划在这个周末尝试一些建议,当我终于有机会回到这个问题时!!

【问题讨论】:

您的分析代码是否碰巧在building 的实例之间创建了引用,这样您最终会产生一个引用循环,从而阻止gc 完成其工作? 我已经把分析代码拿出来了。上面的代码是我运行的逐字逐句 您是否使用 DEBUG=True 运行此代码? catch-22 的解决方法是提供代码的最低重现性示例和重现问题的条件。由于您没有提供,猜测往往会浮出水面。并且在 SO 形式中,最好的猜测会收到你的 1/2 赏金。 上述代码的重现性极低。任何 django 模型都会产生我提到的效果,因为我误解了 process.memory_info().rss 的工作原理。原来在上面的 sn-p 中没有内存问题。出于这个原因,我授予了全部赏金 【参考方案1】:

非常快速的回答:内存正在被释放,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 不会告诉你内存被释放的事情。

我对你的问题和你在这里描述的事实很感兴趣:

似乎 important_buildings 列表正在占用内存,即使超出范围也是如此。

确实如此,但似乎并非如此。看下面的例子:

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

所以即使a 内存被释放,最后一个数字更大。这是因为memory_info.rss() 是进程使用的总内存,而不是当前使用的内存,如文档中所述:memory_info。 p>

下图是与之前相同但带有range(10000000)的代码的图(内存/时间)

我使用 memory-profiler 中的脚本 mprof 来生成此图。

您可以看到内存已完全释放,这与您使用 process.memory_info().rss 进行分析时看到的不同。

如果我将 important_buildings.append(building) 替换为 _ = building 使用更少的内存

总是这样,对象列表总是比单个对象使用更多的内存。

另一方面,您还可以看到所使用的内存并没有像您预期的那样线性增长。为什么?

从这个优秀的site我们可以读到:

追加方法是“摊销” O(1)。在大多数情况下,追加新值所需的内存已经分配,​​严格来说是 O(1)。一旦列表下的 C 数组被用尽,就必须扩展它以适应进一步的追加。这种周期性的扩展过程相对于新数组的大小是线性的,这似乎与我们声称追加是 O(1) 的说法相矛盾。

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

速度很快,但需要消耗内存。

真正的问题不是Django 模型没有从内存中释放。问题是您实现的算法/解决方案,它使用了太多内存。当然,名单是反派。

Django 优化的黄金法则:尽可能替换对查询集的使用列表。

【讨论】:

该列表不是问题,因为它在循环的各个通道中确实非常小,而我的问题是关于在循环的多次迭代中线性累积内存。我仍在使用该列表。但是您提供的其他信息,特别是关于内存分析的信息,帮助我诊断出真正的问题。谢谢。 我很乐意随时提供帮助。【参考方案2】:

你没有提供太多关于你的模型有多大的信息,也没有提供它们之间有什么联系,所以这里有一些想法:

默认情况下QuerySet.iterator() 将加载2000 elements in memory(假设您使用的是 django >= 2.0)。如果您的 Building 模型包含大量信息,这可能会占用大量内存。您可以尝试将chunk_size 参数更改为更低的值。

您的Building 模型是否在实例之间存在链接,这些链接可能导致gc 无法找到的引用循环?您可以使用gc 调试功能来获取更多详细信息。

或者将上述想法短路,也许只是在每个循环结束时调用del(important_buildings)del(buildings) 后跟gc.collect() 来强制垃圾回收?

变量的范围是函数,而不仅仅是for 循环,因此将代码分解为更小的函数可能会有所帮助。虽然请注意,python 垃圾收集器并不总是将内存返回给操作系统,因此正如 this answer 中所述,您可能需要采取更残酷的措施才能看到 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)

【讨论】:

rss 永远不会下降,是衡量进程使用的内存,而不是进程正在使用的内存。 在每个循环结束时调用gc.collect() 不是开销吗?因为评估大型系统中的每个内存对象可能需要相当长的时间【参考方案3】:

Laurent S 的回答非常中肯(+1,我做得很好:D)。

为了减少内存使用,需要考虑以下几点:

    iterator 用法:

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

    onlydefer 选项:

    defer():在一些复杂的数据建模情况下,您的模型可能包含很多字段,其中一些可能包含大量数据(例如,文本字段) ,或者需要昂贵的处理才能将它们转换为 Python 对象。如果您在最初获取数据时不知道是否需要这些特定字段的情况下使用查询集的结果,您可以告诉 Django 不要从数据库中检索它们。

    only(): 或多或少与defer()相反。您可以在检索模型时使用不应延迟的字段调用它。如果您的模型几乎所有字段都需要延迟,则使用 only() 指定互补的字段集可以使代码更简单。

    因此,您可以减少在每个迭代器步骤中从模型中检索的内容,并仅保留操作所需的基本字段。

    1234563 , 对于您的每个操作(这会减慢您的操作,但会减少内存使用量)。

    您可以改进您的查询,以便解决部分(甚至全部)分析,但目前您的问题状态我无法确定(请参阅 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)

如果这仍然占用太多你喜欢的内存,你可以像这样使用上面的第 3 点:

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:如果您可以用更详细的信息更新您的答案,例如您的模型结构和您尝试运行的一些分析操作,我们也许可以提供更具体的答案帮你!

【讨论】:

【参考方案4】:

你考虑过Union吗?通过查看您发布的代码,您正在该命令中运行大量查询,但您可以使用 Union 将其卸载到数据库。

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

调整上述内容可以基本上将该功能所需的查询范围缩小到一个。

DjangoDebugToolbar 也值得一看——如果你还没有看过的话。

【讨论】:

【参考方案5】:

要释放内存,你必须将内循环中建筑物的每个重要细节复制到一个新对象中,以备后用,同时消除那些不合适的。在原始帖子中未显示的代码中存在对内部循环的引用。因此内存问题。通过将相关字段复制到新对象,可以按预期删除原件。

【讨论】:

以上是关于如何强制 Django 模型从内存中释放的主要内容,如果未能解决你的问题,请参考以下文章

如何强制通用应用程序释放内存?

强制向操作系统释放内存

linux下清理内存以及swap

有没有办法强制 JavaFX 释放视频内存?

colab如何释放内存?

如何在Python中显式释放内存?