python请求heroku上的内存使用

时间:2016-03-26 01:25:46

标签: python heroku python-requests cpython

关于Heroku的一些观察结果与我的心理模型没有完全吻合。

我的理解是CPython一旦被操作系统分配就永远不会释放内存。因此,我们永远不应该观察到CPython进程的驻留内存减少。这实际上是我偶尔在Heroku上分析我的Django应用程序的观察结果;有时居民记忆会增加,但永远不会减少。

然而,有时Heroku会提醒我,我的工作人员dyno正在使用> 100%的内存配额。当我对外部服务(使用requests库)的长时间运行的响应数据量大的HTTPS请求由于服务器端超时而失败时,通常会发生这种情况。在这种情况下,当警报停止时,内存使用量将超过100%,然后逐渐回落到不到100%的配额。

我的问题是,这个内存如何被释放回操作系统? AFAIK它不能让CPython发布它。我的猜测是来自长时间运行的TCP连接的传入字节正由OS缓冲,它具有解除分配的能力。当TCP字节的“所有权”转移到我的Django应用程序时,对我来说很模糊。我当然没有明确地从输入流中读取行,我将所有这些行委托给requests

2 个答案:

答案 0 :(得分:0)

CPython会释放记忆,但它有点模糊。

CPython一次分配一块内存,让我们称之为字段。

当您实例化一个对象时,CPython将尽可能使用现有字段中的内存块;可能的是,对于所述物体有足够的传染性阻滞。 如果没有足够的传染性阻滞,它将分配一个新的领域。

这里变得模糊不清。

只有当一个字段包含零个对象时才会被释放,而在CPython中有垃圾收集时,那里就没有"垃圾压缩器"。因此,如果在几个字段中有几个对象,并且每个字段只有70%已满,则CPython不会将这些对象全部移动并释放一些字段。

从HTTP呼叫中提取的大数据块被分配给" new"似乎很合理。字段,但随后有些东西横向移动,对象的引用计数变为零,然后垃圾收集运行并将这些字段返回给操作系统。

答案 1 :(得分:0)

显然,CPython曾经一度没有将内存释放回操作系统。然后在Python 2.5中引入了patch,允许在特定情况下释放内存,详细here。因此,说python不释放内存已不再适用;它只是因为它没有经常释放内存,因为它不能很好地处理内存碎片。

在高级别,python会在名为arenas的256K块中跟踪其内存。对象池保存在这些场所中。 Python非常聪明,当它们被清空时可以将竞技场释放回操作系统,但它仍然不能很好地处理竞技场中的碎片。

在我的特殊情况下,我正在阅读大量的HTTP响应。如果您在请求库中挖掘以HttpAdapter.send()开头的代码链,您最终会发现python套接字库中的socket.read()正在进行系统调用,以便从其套接字中接收8192字节(默认缓冲区大小)。这是操作系统将字节从内核复制到进程的点,CPython将它们指定为大小为8K的字符串对象并将其推入竞技场。请注意,StringIO是套接字的python-land缓冲区对象,它只保留这些8K字符串的列表,而不是将它们组合成一个超级字符串对象。

由于8K恰好在256K中适合32次,我认为发生的事情是接收到的字节很好地填满了整个竞技场而没有太多碎片。然后,当填充它们的8K字符串被删除时,这些竞技场可以被释放到操作系统。

我想我理解为什么内存会逐渐释放(异步垃圾收集?),但我仍然不明白为什么连接错误后需要这么长时间才能释放。如果内存释放总是花费这么长时间,我应该一直看到这些内存使用错误,因为每当进行其中一个调用时,我的python内存使用量就会出现峰值。我已经检查了我的日志,我有时会看到这些违规持续数分钟。似乎是一段长时间的记忆释放。

编辑:我现在对这个问题有一个坚实的理论。记录系统向我报告此错误,该系统保留对最后一个回溯的引用。回溯维护对回溯帧中所有变量的引用,包括StringIO缓冲区,后者又保存对从套接字读取的所有8K字符串的引用。请参阅sys.exc_clear()下的注释:仅在少数几个不明显的情况下才需要此功能。其中包括记录有关最后或当前异常的信息的日志记录和错误处理系统。

因此,在例外的情况下,8K字符串引用计数不会降至零并立即清空它们的竞技场,就像它们在快乐路径中一样;我们必须等待后台垃圾收集来检测它们的参考周期。

当这个异常发生时,很多对象在5分钟内被分配到超时这一事实使得GC延迟更加复杂,我猜测很多8K字符串都有足够的时间进入第二代。默认GC阈值为(700,10,10),字符串对象大约需要700 * 10个分配才能进入第二代。那就是7000 * 8192~ = 57MB,这意味着在字节流的最后57MB之前收到的所有字符串都会成为第二代,如果570MB是流式传输的话,甚至可能是第三代(但这看起来很高)。

对于第二代垃圾收集,大约几分钟的间隔时间似乎非常长,但我想这是可能的。回想一下GC不仅仅是通过分配触发,公式实际上是trigger == (allocations - deallocations > threshold)

TL; DR 大型响应填满了填充竞技场的套接字缓冲区,没有太多碎片,允许Python实际将内存释放回操作系统。在非常例的情况下,这个内存将在退出引用缓冲区的任何上下文后立即释放,因为缓冲区上的引用计数将降为零,从而触发立即回收。在特殊情况下,只要回溯仍然存在,缓冲区仍将被引用,因此我们将不得不等待垃圾回收来回收它们。如果异常发生在连接中间并且已经传输了大量数据,那么到异常时,许多缓冲区将被归类为老一代的成员,并且我们将不得不等待更长时间来进行垃圾收集收回它们。