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

Ted*_*ard 15 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
Run Code Online (Sandbox Code Playgroud)

当我运行这个确切的代码时,我发现内存使用量随着每个迭代外部循环而稳定增加(我print('mem', process.memory_info().rss)用来检查内存使用情况).

important_buildings即使在超出范围之后,列表似乎也会占用内存.如果我替换important_buildings.append(building)_ = building.pk,它不再消耗大量内存,但我确实需要该列表进行一些分析.

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

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

Lau*_*t S 8

您没有提供有关模型有多大的信息,也没有提供它们之间的链接,所以这里有一些想法:

默认情况下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)
Run Code Online (Sandbox Code Playgroud)

  • 在每个循环结束时调用 `gc.collect()` 不是开销吗?因为评估大型系统中的每个内存对象可能需要相当长的时间 (2认同)

Ray*_*nda 8

很快回答.

内存被释放,rss不是一个非常准确的工具,用于说明内存消耗的位置,rss给出了进程使用的内存量,而不是进程正在使用的内存(继续阅读以查看演示),您可以使用包memory-profiler为了逐行检查,你的函数的内存使用.

那么,如何强制Django模型从内存中释放?,你不能说只是使用这样的问题process.memory_info().rss.

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

建议的解决方案:

正如后面在同一篇文章中所示,应用del到列表不会是解决方案,使用chunk_sizefor的优化iterator将有所帮助(请注意在Django 2.0中添加了chunk_size选项iterator),这是肯定的,但这里真正的敌人是那个令人讨厌的列表.

说,你可以使用你需要执行分析的只是字段的列表(我假设你的分析当时不能解决一个建筑物),以减少存储在该列表中的数据量.

尝试获取您需要的属性,并使用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 ...
Run Code Online (Sandbox Code Playgroud)

非常重要的是要注意,如果你使用这样的解决方案,那么在填充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
Run Code Online (Sandbox Code Playgroud)

因此,即使a释放内存,最后一个数字也会更大.这是因为memory_info.rss()是总内存的过程中已经使用的,而不是内存使用的那一刻,因为这里所说的文档:memory_info.

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

图像与时间. 我使用memory-profiler中的脚本mprof来生成此图形.

您可以看到内存已完全释放,而不是您在使用时查看的内容process.memory_info().rss.

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

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

而另一方面,您也可以看到所使用的内存不会像您预期的那样线性增长.为什么?

从这个优秀的网站我们可以阅读:

追加方法是"摊销"O(1).在大多数情况下,已经分配了附加新值所需的内存,严格来说是O(1).一旦列表中的C数组耗尽,就必须对其进行扩展以适应进一步的追加.这个周期性的扩展过程相对于新数组的大小是线性的,这似乎与我们声称附加为O(1)相矛盾.

但是,扩展速率巧妙地选择为阵列先前大小的三倍 ; 当我们将扩展成本分摊到这个额外空间所提供的每个额外附加费用时,每个附加成本按摊销计算为O(1).

它速度快但是有内存成本.

真正的问题不是Django模型没有从内存中释放出来.问题是你实现的算法/解决方案,它使用了太多的内存.当然,名单是恶棍.

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