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

How to force Django models to be released from memory

我想使用管理命令 运行 对马萨诸塞州的建筑物进行一次性分析。我已将有问题的代码缩减为 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——如果我写得太多细节,没有人愿意花时间阅读它(这会成为一个不太适用的问​​题),但是如果我写得太少,我就有可能忽略部分问题。无论如何,我真的很感谢你的回答,并计划在这个周末尝试一些建议,当我终于有机会回到这个问题上时!!

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

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

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

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

变量的范围是函数,而不仅仅是 for 循环,因此将代码分解成更小的函数可能会有所帮助。尽管请注意 python 垃圾收集器不会总是 return 内存到 OS,因此如 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 不是一个非常准确的工具来告诉内存在哪里被消耗rss 给出了进程 使用的 内存的度量,而不是进程 使用 的内存(继续阅读看一个演示),你可以使用包 memory-profiler 来逐行检查你的函数的内存使用情况。

那么,如何强制从内存中释放 Django 模型?你不能仅仅使用 process.memory_info().rss.

就知道有这样的问题

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

建议的解决方案:如稍后在同一 post 中所演示的,将 del 应用于列表不会成为解决方案,使用 chunk_size for iterator 会有所帮助(请注意 iteratorchunk_size 选项已添加到 Django 2.0 中),这是肯定的,但这里真正的敌人是那个令人讨厌的列表。

也就是说,您可以使用执行分析所需的字段列表(我假设您的分析当时无法处理一座建筑物),以减少存储在该列表。

尝试使用 Django 的 ORM 随时随地获取您需要的属性和select目标建筑物。

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不会告诉你内存被释放了。

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

It seems like the important_buildings list is hogging up memory, even after going out of scope.

的确,似是而非。看下面的例子:

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.

下图是与之前代码相同但 range(10000000)

的绘图 (memory/time)

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

您可以看到内存已完全释放,这不是您使用 process.memory_info().rss 配置文件时看到的。

If I replace important_buildings.append(building) with _ = building use less memory

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

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

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

The append method is “amortized” O(1). In most cases, the memory required to append a new value has already been allocated, which is strictly O(1). Once the C array underlying the list has been exhausted, it must be expanded in order to accommodate further appends. This periodic expansion process is linear relative to the size of the new array, which seems to contradict our claim that appending is O(1).

However, the expansion rate is cleverly chosen to be three times the previous size of the array; when we spread the expansion cost over each additional append afforded by this extra space, the cost per append is O(1) on an amortized basis.

速度很快,但有内存开销。

真正的问题不是 Django 模型没有从内存中释放。问题是你实现的algorithm/solution,它占用了太多内存。当然,名单是反派。

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

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

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

  1. iterator用法:

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

  2. only and defer选项:

    defer(): In some complex data-modeling situations, your models might contain a lot of fields, some of which could contain a lot of data (for example, text fields), or require expensive processing to convert them to Python objects. If you are using the results of a queryset in some situation where you don’t know if you need those particular fields when you initially fetch the data, you can tell Django not to retrieve them from the database.

    only(): Is more or less the opposite of defer(). You call it with the fields that should not be deferred when retrieving a model. If you have a model where almost all the fields need to be deferred, using only() to specify the complementary set of fields can result in simpler code.

    因此,您可以减少在每个迭代器步骤中从模型中检索的内容,只保留操作的必要字段。

  3. 如果您的查询仍然占用大量内存,您可以选择仅保留 building_idimportant_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)

如果这仍然占用太多内存,您可以像这样使用上面的第 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: 如果您可以用更具体的内容更新您的答案,例如模型的结构和您尝试进行的一些分析操作 运行,我们或许可以提供更具体的答案来帮助您!

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

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

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

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

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