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

时间:2019-01-11 18:49:18

标签: python django memory-management django-models geodjango

我想使用管理命令对马萨诸塞州的建筑物进行一次分析。我已将令人反感的代码缩减为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列表似乎在占用内存,即使超出范围也是如此。如果我将important_buildings.append(building)替换为_ = building.pk,则它不再消耗太多内存,但是对于某些分析,我确实需要该列表。

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

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

5 个答案:

答案 0 :(得分:8)

关于模型的大小以及它们之间的链接,您没有提供太多信息,因此这里有一些想法:

默认情况下,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)

答案 1 :(得分:7)

非常快速的答案。

正在释放内存,rss并不是一个非常准确的工具,它可以告诉正在消耗内存的地方rss可以衡量进程具有的内存< strong>使用,而不是使用(请继续阅读以查看演示)的内存,您可以使用软件包memory-profiler来逐行检查,您的函数对内存的使用。

因此,如何强制从内存中释放Django模型?,仅使用process.memory_info().rss就无法判断是否存在此类问题。

但是,我可以为您提出优化代码的解决方案。并编写一个演示,说明为什么process.memory_info().rss不是衡量某些代码中已使用的内存的非常准确的工具。

建议的解决方案:

如同一篇文章稍后所述,将del应用于列表将不是解决方案,对chunk_size使用iterator进行优化将有所帮助(请注意{{1}可以肯定,chunk_size的}选项是在Django 2.0中添加的),这是肯定的,但是这里真正的敌人是那个讨厌的列表。

说,您可以使用仅执行分析所需的字段的列表(我假设您的分析当时无法同时针对一栋建筑物进行处理),以减少该列表中存储的数据量

尝试随时随地获取所需的属性,并使用Django的ORM选择目标建筑物。

iterator

非常重要,请注意,如果使用这样的解决方案,则只会在填充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. # Analisis according size. data = important_buildings.values_list('size', flat=True) # Analisis 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 ... 变量时访问数据库。当然,您在内存中将仅具有完成分析所需的最低要求。

事先考虑。

遇到此类问题时,您应该开始考虑并行性,集群化,大数据等问题。另请参阅ElasticSearch,它具有很好的分析能力。

演示

data不会告诉您内存已释放。

您的问题和您在此处描述的事实真的让我很感兴趣:

  

看来即使在超出范围后,重要的建筑物列表仍在占用内存。

确实,似乎但不是。看下面的例子:

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 内存,最后一个数字也会更大。这是因为a是进程已使用的总内存,而不是当前正在使用的内存,如文档中所述:{{3} }。

下图是与以前相同的代码(带有memory_info.rss()

的图(内存/时间)

memory_info 我使用Image against time.附带的脚本range(10000000)来生成图形。

您可以看到内存已完全释放,而不是使用mprof进行配置时看到的内容。

  

如果我将_ =建筑替换为Important_buildings.append(building),请使用更少的内存

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

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

从这个出色的memory-profiler中,我们可以读到:

  

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

     

但是,明智地将扩展速率选择为数组先前大小的三倍;当我们将扩展成本分摊到此额外空间所提供的每个附加附件时,每个附件的费用为O(1)(按摊销基础)。

速度快,但具有存储成本。

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

Django优化的黄金法则:在任何地方都尽可能取代对querisets的列表使用。

答案 2 :(得分:3)

Laurent S的回答很关键(+1,我做得很好:D)。

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

  1. iterator的用法:

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

  2. onlydefer选项:

      

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

         

    only() :与defer()相反。您使用检索模型时不应推迟的字段来调用它。如果您有一个模型,其中几乎所有字段都需要延迟,则使用only()指定互补的字段集可以简化代码。

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

  3. 如果您的查询仍然占用过多内存,则可以选择将building_id保留在important_buildings列表中,然后使用此列表从{{ 1}}的模型,针对您的每个操作(这会降低您的操作速度,但会减少内存使用量)。

  4. 您可能会改进查询以解决部分(甚至整个)分析问题,但目前无法确定您的问题状态(请参阅 PS < / strong>(在此答案的末尾)

现在,让我们尝试将以上所有要点整合到示例代码中:

Building

如果这仍然占用太多内存,您可以使用上面的第3点,如下所示:

# 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)

PS::如果您可以使用更具体的信息来更新答案,例如模型的结构以及您要尝试运行的某些分析操作,我们可能会为您提供更具体的答案帮助您!

答案 3 :(得分:0)

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

memberships#new

对以上内容进行调整实际上可以将此功能所需的查询范围缩小到一个。

值得一看的是DjangoDebugToolbar-如果您还没有看过它。

答案 4 :(得分:0)

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