python垃圾收集器发疯了

Ser*_*lov 5 python ssl garbage-collection memory-leaks

Python 2.7.12,Ubuntu 16.04。

简单的服务器应用程序,每 10 秒检查一次电子邮件,并在调试日志中写入没有消息。邮箱真的是空的,所以应用程序真的什么都不做。单线程。不使用任何自定义 C 扩展。通常使用 17 Mb 的内存或类似的东西。

但是几个小时后,可能是一天后,它突然开始在内存中增长到 8 GB,消耗 100% 的 CPU 并且不再写入调试日志。

我阅读了http://tech.labs.oliverwyman.com/blog/2008/11/14/tracing-python-memory-leaks/,在 pdb 下运行我的应用程序,等待一个晚上,看看 objgraph 可以显示。没有。没有广泛增长的对象类型,所有计数都正常(我首先检查正常状态下的应用程序)。

然后我尝试使用https://pythonhosted.org/Pympler/muppy.html像这样:

(Pdb) from pympler import muppy
(Pdb) all_objects = muppy.get_objects()
*** MemoryError: 
Run Code Online (Sandbox Code Playgroud)

过了一会儿,我在系统监视器中看到,该进程 pdb 现在仅消耗 2Gb 内存!但是它是单线程的(我用 ps 检查)并且我的应用程序被停止了,所以不知何故 6Gb 就消失了......

故事结束:经过一些实验,我意外地发现如何在这个 pdb 会话中快速达到 100% CPU 和 8-12 Gb。只需调用 gc.collect() 即可。muppy.get_objects() 具有相同的效果。

但是在 pdb 中 gc.collect() 完成后的几分钟内资源会收缩回 2Gb,这似乎在应用中情况不太一样。

顺便说一下,gc.collect() 返回 0。

我怀疑有人破坏了 python 堆,我怀疑 SSL 模块(只是因为它很复杂,没有任何更具体的原因)。我在使用标准 imaplib 通过 SSL 通过 IMAP 连接到邮箱时使用它 + 一些我的代码,使其理解超时(并且我的所有代码都在 Python 中)。

我还能做些什么来查找/修复问题?可能是一些好的内存检查工具或 python 库中的一个已知错误?


伙计们,我明白了!似乎 GC 问题只是视觉效果,真正的问题在这里:

# IMAP with timeouts
class NonBlockingSSL_IMAP(imaplib.IMAP4):
    def __init__(self, timeout, *args, **kwargs):
        self.timeout = timeout
        imaplib.IMAP4.__init__(self, *args, **kwargs)

    ....

    def read(self, size):
        readed = 0
        buffer = []

        # ssl socket is not entirely normal socket, so may be there are some data
        # and select do not know about them. So we should dry socket first.
        dried = False

        while readed < size:
            if dried:
                self.wait_for(recv=self.sock)
            try:
                data = self.sslobj.recv(size - readed)                
                buffer.append(data)   # <----- here !!!!
                readed += len(data)
            except ssl.SSLWantReadError:
                dried = True
            except socket.error as se:
                if se.errno != errno.EAGAIN:
                    # something bad happened
                    raise se
                dried = True
        return ''.join(buffer)

    ....

    def wait_for(self, recv=None, send=None):
        rd = [recv] if recv is not None else []
        wr = [send] if send is not None else []

        ready_r, ready_w, _ = select.select(rd, wr, [], self.timeout)

        if not ready_r and not ready_w:
            raise socket.error(errno.EAGAIN, "timeout")
Run Code Online (Sandbox Code Playgroud)

如果 recv 返回空字符串,我将其添加到缓冲区,并且不增加读取(因为 len == 0)。它可能是一个无限循环。结果,我们有一个非常大的空字符串列表——根本没有其他对象(因为所有 '' 都是一个对象;你好,objgraph)。只有一个非常非常大的清单。可能Python无法正确处理它,我可以理解。