为啥在 django 中进行大型查询(或一系列查询)后内存没有释放到系统?

Posted

技术标签:

【中文标题】为啥在 django 中进行大型查询(或一系列查询)后内存没有释放到系统?【英文标题】:Why doesn't memory get released to system after large queries (or series of queries) in django?为什么在 django 中进行大型查询(或一系列查询)后内存没有释放到系统? 【发布时间】:2011-07-26 12:51:56 【问题描述】:

首先,DEBUG = False 在 settings.py 中,所以不,connections['default'].queries 在耗尽所有内存之前不会不断增长。

让我们从我已经从 django.contrib.auth.models.User 加载了包含 10000 个用户的 User 表开始(每个名为“test#”,其中 # 是 1 到 10000 之间的数字)。

这里是视图:

from django.contrib.auth.models import User
from django.http import HttpResponse

import time

def leak(request):
    print "loading users"

    users = []
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())
    users += list(User.objects.all())

    print "sleeping"
    time.sleep(10)

    return HttpResponse('')

我已将上面的视图附加到 /leak/ url 并启动开发服务器(使用 DEBUG=False,并且我已经测试过它与运行开发服务器无关与其他实例相比)。

运行后:

% curl http://localhost:8000/leak/

runserver 进程的内存增长到从下面的ps aux 输出中看到的大小,然后保持在该级别。

USER       PID %CPU %MEM    VSZ    RSS TTY      STAT START   TIME COMMAND
dlamotte 25694 11.5 34.8 861384 705668 pts/3    Sl+  19:11   2:52 /home/dlamotte/tmp/django-mem-leak/env/bin/python ./manage.py runserver

然后运行上面的curl 命令似乎并没有增加实例的内存使用量(我预计这是真正的内存泄漏?),它必须重新使用内存?但是,我觉得这里有问题,内存没有释放到系统(但是,我理解python不释放内存可能会更好的性能)。

在此之后,我天真地试图看看 python 是否会释放它分配的大块内存。所以我从 python 会话中尝试以下操作:

>>> a = ''
>>> a += 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' * 10000000
>>> del a

内存按预期分配在a += ... 行,但是当del a 发生时,内存被释放。为什么 django 查询集的行为不同?这是 django 打算做的事情吗?有没有办法改变这种行为?

我确实花了 2 天时间来调试这种行为,但不知道下一步该去哪里(我学会了使用 guppy 和 objgraph,这似乎没有指向我能弄清楚的任何有趣的东西)。

更新:这可能只是工作中的 python 内存管理,与 Django 无关(在 django-users 邮件列表中建议),但我想通过在 python 中以某种方式复制它来确认在 Django 之外。

更新:使用 python 版本 2.6.5

【问题讨论】:

+1。对这个问题的答案很感兴趣。我还发现 Django 在生产中的一段时间内出现了奇怪的内存泄漏(DEBUG=False 以及非常简单的应用程序/项目)。 如果你del users会发生什么? 您使用的是什么版本的 Python(用于服务器和命令行测试)?旧版本不会将分配给对象的内存释放回系统,因此如果您的本地版本是 2.5 或更高版本,但您的服务器运行的是 2.4,这可能是您的问题。单个大分配(比如你的大字符串)也可能绕过分配器 - 看看像 ([[]] * 10**6) 这样的东西会发生什么。 由于有很多小对象,您还可以看到内存碎片问题,因此即使是较新的分配器也无法释放完整的块以返回操作系统。作为一个实验,使用我之前评论中的大列表,获取对每 10 个对象 x = seq[::10] 左右的引用,然后终止原始列表,看看你还给操作系统多少内存(可能很少)。跨度> 我在上述 cmets 中的示例不正确 - 我发布了一个更正版本作为答案。 【参考方案1】:

我决定将我的 cmets 移到答案中以使事情更清楚。

从 Python 2.5 开始,CPython 内存分配会跟踪小对象分配器的内部内存使用情况,并尝试将完全空闲的区域返回给底层操作系统。这在大多数情况下都有效,但对象无法在内存中移动这一事实意味着碎片可能是一个严重的问题。

试试下面的实验(我用的是3.2,但是如果你用xrange的话2.5+应该差不多):

# Create the big lists in advance to avoid skewing the memory counts
seq1 = [None] * 10**6 # Big list of references to None
seq2 = seq1[::10]

# Create and reference a lot of smaller lists
seq1[:] = [[] for x in range(10**6)] # References all the new lists
seq2[:] = seq1[::10] # Grab a second reference to 10% of the new lists

# Memory fragmentation in action
seq1[:] = [None] * 10**6 # 90% of the lists are no longer referenced here
seq2[:] = seq1[::10] # But memory freed only after last 10% are dropped

请注意,即使您删除了对 seq1seq2 的引用,上述顺序也可能会让您的 Python 进程占用大量额外内存。

当人们谈论 PyPy 使用比 CPython 更少的内存时,这是他们谈论的主要部分。因为 PyPy 在底层不使用直接指针引用,所以它能够使用压缩 GC,从而避免了大部分碎片问题,并且更可靠地将内存返回给操作系统。

【讨论】:

Dropbox 团队实际上在他们的 Pycon 演讲中对这个问题给出了非常好的描述(在 18:00 显示的幻灯片中首次提到,在 ~26:00 有更多详细信息):pycon.blip.tv/file/4878722 你能链接到源的相关部分吗? 对象分配器的开发版本:hg.python.org/cpython/file/default/Objects/obmalloc.c 将 URL 中的“默认”段更改为相关的 Python 版本,以获取该分支上分配器代码的最新版本。【参考方案2】:

许多应用程序、语言运行时,甚至一些系统内存分配器都会尽可能长时间地保留已释放的内存,以便重新使用它,纯粹是出于性能目的。在像 Django 这样的复杂系统中,它可能是任何数量的扩展,可能是用 C 实现的,它们表现出这种行为,或者它可能是带有某种内存池或惰性垃圾收集的 Python。

甚至可能是底层 malloc 实现执行此操作,或者您的操作系统为您的进程保留了一定数量的内存空间,即使该进程没有明确使用它。不过不要引用我的话——我已经有一段时间没有研究过这些事情了。

但总的来说,如果在初始分配和释放后重复分配过程不会使使用的内存量增加一倍,那么您看到的不是内存泄漏而是内存池。只有当您有很多进程在该机器上争用有限的内存时,这才可能成为问题。

【讨论】:

很好的答案,只是对很多“挥手”的神奇解释并不感到兴奋......我喜欢 ncoghlan 给了我一个如何重复问题的例子,不过感谢您的意见 遗憾的是,有必要进行一些操作,因为现在您很少能在高级语言的资源分配和操作系统级别的进程内存使用之间获得完全确定的映射 - 各种内存经理们对此有太多的优化。这就是您无法仅从 ps 或任务管理器诊断内存泄漏的原因。 ncoghlan 似乎以一种有意义的方式解释了它。内存变得碎片化,因此解释器无法释放。我确信内存管理器在某些时候会发挥作用......但我认为你已经夸大了这一点。事实是,记忆是支离破碎的,与更深层次的东西无关。 内存碎片化并且解释器无法释放的唯一原因是 - 因为 - 这里有一个特殊的内存管理器在工作。完全相同的问题也适用于系统分配器,具体取决于实现。我的回答只是 ncoghlan 特别强调的内容的一个更一般的版本。 在对问题有了更多了解并重新阅读您的答案后,我对您的答案投了赞成票,因为我认为这是一个很好的答案。但是,您解决了整个潜在问题并且没有专门帮助我解决我的问题,所以我认为 ncoghlan 对我的帮助比您做得更好。但我很欣赏这个答案。

以上是关于为啥在 django 中进行大型查询(或一系列查询)后内存没有释放到系统?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Django 在更改列表视图页面中进行不必要的 SQL 查询?

为啥通过 django QuerySet 进行查询比在 Django 中使用游标慢得多?

为啥这个 Django 原始 SQL 查询不返回输出?

当查询被填充时,有没有办法在 Django 模板中呈现大型查询集?

在 Django 中,如何优雅地将查询集过滤器添加到大型组或对象的所有成员?

使用大型列表优化 Django 查询集调用?