【问题标题】:Python subprocess.Popen erroring with OSError: [Errno 12] Cannot allocate memory after period of timePython subprocess.Popen 出现 OSError 错误:[Errno 12] 一段时间后无法分配内存
【发布时间】:2010-11-16 01:14:17
【问题描述】:

注意:此问题已被重新提出,并提供了所有调试尝试的摘要here


我有一个 Python 脚本作为后台进程运行,每 60 秒执行一次。其中一部分是调用subprocess.Popen 以获取ps 的输出。

ps = subprocess.Popen(['ps', 'aux'], stdout=subprocess.PIPE).communicate()[0]

运行几天后,调用出错:

文件“/home/admin/sd-agent/checks.py”,第 436 行,在 getProcesses 中 __init__ 中的文件“/usr/lib/python2.4/subprocess.py”,第 533 行 _get_handles 中的文件“/usr/lib/python2.4/subprocess.py”,第 835 行 OSError: [Errno 12] 无法分配内存

但是free 在服务器上的输出是:

$免费-m 缓存的已用空闲共享缓冲区总数 内存:894 345 549 0 0 0 -/+ 缓冲区/缓存:345 549 交换:0 0 0

我搜索了这个问题,发现 this article 上面写着:

解决方案是为您的服务器添加更多交换空间。当内核 fork 以启动建模器或发现进程时,它首先确保交换存储上有足够的可用空间,如果需要的话,新进程。

我注意到上面的免费输出中没有可用的交换。这是否可能是问题所在和/或可能有哪些其他解决方案?

2009 年 8 月 13 日更新 作为一系列监控功能的一部分,上面的代码每 60 秒调用一次。该进程是守护进程,并使用sched 安排检查。上述函数的具体代码为:

def getProcesses(self):
    self.checksLogger.debug('getProcesses: start')

    # Memory logging (case 27152)
    if self.agentConfig['debugMode'] and sys.platform == 'linux2':
        mem = subprocess.Popen(['free', '-m'], stdout=subprocess.PIPE).communicate()[0]
        self.checksLogger.debug('getProcesses: memory before Popen - ' + str(mem))

    # Get output from ps
    try:
        self.checksLogger.debug('getProcesses: attempting Popen')

        ps = subprocess.Popen(['ps', 'aux'], stdout=subprocess.PIPE).communicate()[0]

    except Exception, e:
        import traceback
        self.checksLogger.error('getProcesses: exception = ' + traceback.format_exc())
        return False

    self.checksLogger.debug('getProcesses: Popen success, parsing')

    # Memory logging (case 27152)
    if self.agentConfig['debugMode'] and sys.platform == 'linux2':
        mem = subprocess.Popen(['free', '-m'], stdout=subprocess.PIPE).communicate()[0]
        self.checksLogger.debug('getProcesses: memory after Popen - ' + str(mem))

    # Split out each process
    processLines = ps.split('\n')

    del processLines[0] # Removes the headers
    processLines.pop() # Removes a trailing empty line

    processes = []

    self.checksLogger.debug('getProcesses: Popen success, parsing, looping')

    for line in processLines:
        line = line.split(None, 10)
        processes.append(line)

    self.checksLogger.debug('getProcesses: completed, returning')

    return processes

这是一个称为检查的更大类的一部分,它在守护程序启动时初始化一次。

整个检查类可以在 http://github.com/dmytton/sd-agent/blob/82f5ff9203e54d2adeee8cfed704d09e3f00e8eb/checks.py 找到,getProcesses 函数从第 442 行开始定义。从第 520 行开始,它由 doChecks() 调用。

【问题讨论】:

  • 如果你运行top,你是否看到你的后台进程消耗更多的内存?鉴于它失败的代码,我会怀疑文件描述符是否用完(尽管这应该是不同的 errno)。你每 60 秒还在做什么?
  • 在每次 Popen 调用前后记录了 free -m 的输出,内存保持不变。如何检查文件描述符?各种其他进程也正在启动,但它们也会被记录下来,并且内存不会随着时间的推移而“被用完”。
  • 我用另一个建议更新了我的答案。

标签: python linux memory


【解决方案1】:

您的 python 脚本继承了一些 resource limit (RLIMIT_DATA, RLIMIT_AS?) 限制了内存泄漏。在运行脚本之前检查您的 *ulimit(1)*s,并按照其他人的建议分析脚本的内存使用情况。

在您向我们展示的代码 sn-p 之后,您如何处理变量 ps 您是否保留对它的引用,永远不会被释放?引用subprocess module docs

注意:读取的数据是缓存在内存中的,所以不要使用这个 数据量大或无限制时的方法。

... 和 ps aux 在繁忙的系统上可能很冗长...

更新

您可以使用resource 模块从您的python 脚本中检查rlimits:

import resource
print resource.getrlimit(resource.RLIMIT_DATA) # => (soft_lim, hard_lim)
print resource.getrlimit(resource.RLIMIT_AS)

如果这些返回“无限”——(-1, -1)——那么我的假设是不正确的,你可以继续!

另见resource.getrusage,尤其是。 ru_??rss 字段,它可以帮助您使用 python 脚本检测内存消耗,而无需使用外部程序。

【讨论】:

  • 我已经更新了问题,以包含有关最终调用 Popen 的函数调用的更多详细信息。在代码 sn-p 之后没有对 ps 变量做任何特定的事情 - 函数返回处理后的结果。
  • @DavidM,感谢您的更新。这将我的问题推到了一层——然后processes 会发生什么,它是否曾经被破坏等等?我现在将使用更 Python 的方式来检查资源限制...
  • RLIMIT_DATA 和 RLIMIT_AS 上的 rlimits 都显示为 (-1, -1)。进程被返回,然后用于将该数据发送回监控系统。它没有被破坏。我用关于整个守护进程的更多信息更新了 Q。
【解决方案2】:

当你使用 popen 时,如果你希望它关闭额外的文件描述符,你需要提交 close_fds=True。

创建一个新管道,该管道发生在来自回溯的 _get_handles 函数中,会创建 2 个文件描述符,但您当前的代码永远不会关闭它们,并且您最终会达到系统最大 fd 限制。

不确定为什么您收到的错误表明内存不足:它应该是文件描述符错误,因为pipe() 的返回值包含此问题的错误代码。

【讨论】:

  • 我认为这只是在子进程运行时关闭额外的描述符。当子进程退出时,它无论如何都会关闭它的所有描述符,不是吗?
  • @Vinay Sajip,是的,这个答案似乎离谱。 “close_fds”与子进程的继承 fds(如 Perl 的 $^F)有关,子进程 module/communicate() 负责智能地关闭父子进程之间的管道。您的 ENOMEM 实际上是伪装的 ENFILE/EMFILE 似乎也不太可能。
  • 深入研究了代码,管道 FD 已正确关闭。当使用 close_fds=False 发生分叉时,父进程中的所有 FD 都被复制到子进程中,在这种情况下,python 进程的所有 FD 都被复制,因为此代码是一些较大脚本的一部分,因此可能会打开很多。根据 POSIX,这些应该在子进程退出时关闭,但是导致这种情况不会发生的情况很常见(谷歌快速搜索 fd 泄漏将提供参考)。我仍然认为fd是问题所在。 OP 能否确认这是否解决了问题?
  • 这并没有解决问题。我在stackoverflow.com/questions/1367373/… 转发了这个问题
【解决方案3】:

那个交换空间的答案是假的。从历史上看,Unix 系统想要像这样可用的交换空间,但它们不再那样工作了(而 Linux 从来没有那样工作过)。您甚至还没有接近耗尽内存,所以这不太可能是真正的问题 - 您正在耗尽其他一些有限的资源。

考虑到错误发生的位置(_get_handles 调用 os.pipe() 来为子级创建管道),您可能遇到的唯一真正问题是没有足够的可用文件描述符。我会寻找未关闭的文件(lsof -p 在执行 popen 的进程的 PID 上)。如果您的程序确实需要同时保持大量文件打开,则增加打开文件描述符的用户限制和/或系统限制。

【讨论】:

    【解决方案4】:

    如果您正在运行后台进程,则很可能您已经重定向了您的进程 stdin/stdout/stderr。

    在这种情况下,将选项“close_fds=True”附加到您的 Popen 调用中,这将阻止子进程继承您的重定向输出。这可能是您遇到的限制。

    【讨论】:

      【解决方案5】:

      在添加交换空间之前,您实际上可能希望等待所有这些 PS 进程完成。

      完全不清楚“作为后台进程运行每 60 秒执行一次”是什么意思。

      但是您对 subprocess.Popen 的调用每次都会派生一个新进程。

      更新

      我猜你不知何故让所有这些进程运行或挂在僵尸状态。但是,communicate 方法应该清理生成的子进程。

      【讨论】:

      • “作为后台进程运行,每 60 秒执行一次”意味着代码每 60 秒被调用一次,作为持续运行进程的一部分。如果我不调用communicate() 那么我实际上无法得到ps 的输出。
      • communicate() 等待衍生进程终止并启动读取其 stdout 和 stderr 流的线程。
      • @DavidM:“代码”? “被叫”?哪个代码?子进程.Popen?它每 60 秒分叉一个新进程?这就是你说的吗?而且它从不等待一个孩子完成?
      • @Vinay Sajip:虽然 Communicate 据称等待子进程,但我并不容易相信它与正确的 wait 方法相同。该应用程序听起来像是用子进程超出了系统。
      • @S. Lott:我在 Ubuntu 上检查了 Python 2.4.6 的源代码 - communicate 确实 调用 self.wait()。这不是正确的wait 方法吗?
      【解决方案6】:

      随着时间的推移,您是否观察过您的过程?

      • lsof
      • ps -辅助 | grep -i pname
      • 顶部

      所有人都应该提供有趣的信息。我认为该过程正在占用应该释放的资源。是否有可能占用资源句柄(内存块、流、文件句柄、线程或进程句柄)?来自生成的“ps”的标准输入、标准输出、标准错误。内存句柄,...来自许多小的增量分配。我很想看看上面的命令在您的进程刚刚完成启动和第一次运行时以及在定期“坐”在那里启动子进程 24 小时后显示的内容。

      由于它在几天后死机,您可以让它只运行几个循环,然后每天重新启动一次作为解决方法。在此期间,这将对您有所帮助。

      雅各布

      【讨论】:

        【解决方案7】:

        你需要

        ps = subprocess.Popen(["sleep", "1000"])
        os.waitpid(ps.pid, 0)
        

        释放资源。

        注意:这在 Windows 上不起作用。

        【讨论】:

        • Popen.communicate() 调用 Popen.wait() 为您调用 os.waitpid()。无需手动调用 os.waitpid()。
        【解决方案8】:

        我不认为您链接到的 Zenoss 文章中给出的情况是导致此消息的唯一原因,因此尚不清楚交换空间是否肯定是问题所在。我建议即使在成功调用时也要记录更多信息,以便每次在调用 ps 之前都能看到可用内存的状态。

        还有一件事 - 如果您在 Popen 调用中指定 shell=True,您会看到不同的行为吗?

        更新:如果不是内存,下一个可能的罪魁祸首确实是文件句柄。我会建议在strace 下运行失败的命令,以准确查看哪些系统调用失败了。

        【讨论】:

        • 我可以在其中添加 shell=True。这到底是做什么的?文档说“如果 shell 为 True,指定的命令将通过 shell 执行。”但这并不能真正解释其中的区别。
        • 当您指定 shell=True 时,会生成 shell 程序(例如 Linux 上的 bash,Windows 上的 cmd.exe),然后运行您想要生成的实际程序。不建议将其作为降低内存使用率的途径 - 而是作为查看行为如何变化的附加诊断工具。我希望在每次生成时记录内存条件以及查看失败调用和成功调用与内存状态、交换等相关的更多有用输入。
        • 您对如何在脚本运行时记录内存使用情况有什么建议吗?我发现 code.activestate.com/recipes/286222 似乎可以完成这项工作。
        • 这与 Python 进程使用了​​多少内存无关 - 它与记录 free -m 为所有 ps 生成的返回值有关。您可以使用subprocess 生成free -m 并将结果记录到文件中。
        • 我打电话给 mem = subprocess.Popen(['free', '-m'], stdout=subprocess.PIPE).communicate()[0] 并在之前记录输出在每次 Popen 调用之后,内存使用似乎保持相当恒定,即内存不会慢慢耗尽。它始终在 894/344/549 左右(总/已使用/免费)。 Swap 始终保持为 0,但这是意料之中的,实际上有可用的交换,只是没有显示在免费输出中。
        【解决方案9】:

        虚拟内存很重要!!!

        在将交换添加到我的操作系统之前,我遇到了同样的问题。虚拟内存的公式通常为:SwapSize + 50% * PhysicalMemorySize。我终于通过添加更多物理内存或添加交换磁盘来解决这个问题。 close_fds 在我的情况下不起作用。

        【讨论】:

          猜你喜欢
          • 2010-11-24
          • 2013-08-01
          • 2015-05-04
          • 1970-01-01
          • 1970-01-01
          • 2017-07-23
          • 1970-01-01
          • 1970-01-01
          • 2018-03-29
          相关资源
          最近更新 更多