Python subprocess.Popen在一段时间后出现OSError错误:[Errno 12]无法分配内存。

11

注意:此问题已经重新提出,并附有所有调试尝试的摘要在此处


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

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

运行几天后,调用出现错误:

File "/home/admin/sd-agent/checks.py", line 436, in getProcesses
File "/usr/lib/python2.4/subprocess.py", line 533, in __init__
File "/usr/lib/python2.4/subprocess.py", line 835, in _get_handles
OSError: [Errno 12] Cannot allocate memory

然而,服务器上的free输出为:

$ free -m
                  total       used       free     shared     buffers    cached
Mem:                894        345        549          0          0          0
-/+ buffers/cache:  345        549
Swap:                 0          0          0

我已经搜索了该问题并找到了此文章,其中提到:

解决方案是为服务器添加更多的交换空间。当内核复制以启动模型或发现进程时,它首先确保在交换存储器中有足够的空间存储新进程(如果需要)。

我注意到,从上面的free输出中没有可用的交换。这可能是问题吗?还有哪些其他解决方案?

更新于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行开始。这由doChecks()在520行开始调用。


如果你运行top命令,你是否看到后台进程消耗了更多的内存?鉴于代码失败的位置,我会怀疑是否用尽了文件描述符(虽然这应该是不同的errno)。你每60秒还在做哪些其他事情? - bstpierre
在每个Popen调用之前和之后记录了free -m的输出,内存保持不变。我该如何检查文件描述符?还启动了各种其他进程,但它们也被记录下来,内存随时间而“被使用完”。 - davidmytton
我更新了我的答案,提出了另一个建议。 - Vinay Sajip
9个回答

5
您可能遇到了一个内存泄漏问题,由某个资源限制RLIMIT_DATARLIMIT_AS?)继承到您的Python脚本中。在运行脚本之前,请检查您的 *ulimit(1)*s,并像其他人建议的那样对脚本的内存使用情况进行分析。 在您展示给我们的代码片段后,您会如何处理变量ps 您会保留对它的引用,永远不会释放吗?引用 subprocess模块文档 的话:

注意:读取的数据在内存中缓冲,因此如果数据大小很大或无限制,请勿使用此方法。

...而且 ps aux 在繁忙的系统上可能会很冗长... 更新 你可以使用resource模块在Python脚本中检查资源限制:
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的函数调用的细节。在代码片段之后没有对ps变量进行特定的操作 - 函数返回已处理的结果。 - davidmytton
@DavidM,感谢您的更新。这将我的问题推出了一个层次——那么processes会发生什么,它是否被销毁等等?我将立即更新更Pythonic的方法来检查资源限制... - pilcrow
rlimits 在 RLIMIT_DATA 和 RLIMIT_AS 上都显示为 (-1, -1)。进程被返回,然后用于将数据发送回监控系统。它不会被销毁。我已经更新了问题,并提供了有关整个守护程序的更多信息。 - davidmytton

3

当你使用popen时,如果想关闭额外的文件描述符,需要传入close_fds=True参数。

在回溯中的_get_handles函数中创建一个新的管道会创建两个文件描述符,但是你当前的代码没有关闭它们,最终可能会达到系统的最大文件描述符限制。

不确定为什么你收到的错误指示内存不足:因为pipe()的返回值有一个用于表示此问题的错误代码,应该是一个文件描述符错误。


我认为这只是在子进程运行时关闭额外的描述符。当子进程退出时,它将无论如何关闭所有描述符,不是吗? - Vinay Sajip
@Vinay Sajip,是的,这个答案似乎不太准确。“close_fds”与子进程继承的文件描述符有关(类似于Perl中的$^F),而子进程模块/communicate()会智能地关闭父子进程之间的管道。你所遇到的ENOMEM实际上很可能是ENFILE/EMFILE的伪装。 - pilcrow
深入研究了代码,管道FD已正确关闭。当使用close_fds=False进行分叉时,所有来自父进程的FD都会被复制到子进程中,在这种情况下,Python进程的所有FD都会被复制。由于此代码是某个较大脚本的一部分,可能会有很多打开的FD。根据POSIX标准,这些应在子进程退出时关闭,但很常见会出现某些问题导致未能关闭(快速搜索fd泄漏将提供参考)。我仍然认为FD是问题所在。OP能否确认是否解决了该问题? - Mark
2
这并没有解决问题。我重新发布了问题,网址为https://dev59.com/L-o6XIcBkEYKwwoYTzIw - davidmytton

3
那个关于交换空间的回答是错误的。历史上,Unix系统希望有这样的交换空间可用,但现在它们不再这样工作了(而Linux从未这样工作过)。你甚至没有接近内存耗尽的情况,所以那不太可能是实际问题 - 你正在耗尽其他一些有限资源。
考虑到错误发生的位置(_get_handles调用os.pipe()创建管道到子进程),你可能遇到的唯一真正问题是没有足够的空闲文件描述符。我建议查找未关闭的文件(在执行popen的进程PID上运行lsof -p命令)。如果你的程序确实需要同时保持大量文件打开状态,则应增加用户限制和/或打开文件描述符的系统限制。

2

如果你正在运行一个后台进程,那么很可能已经重定向了进程的stdin/stdout/stderr。

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


1

在增加交换空间之前,您可能希望等待所有 PS 进程完成。

“作为后台进程每 60 秒执行一次”并不是很清晰。

但是,每次调用 subprocess.Popen 都会分叉出一个新的进程。

更新。

我猜您可能不小心让所有这些进程保持运行状态或卡在僵尸状态。 不过,communicate 方法应该清除生成的子进程。


“作为后台进程运行,每60秒执行一次”意味着代码作为一个持续运行的进程的一部分每60秒被调用一次。如果我不调用communicate(),那么我实际上无法获取ps的输出。 - davidmytton
communicate() 等待生成的进程终止,并启动读取其标准输出和标准错误流的线程。 - Vinay Sajip
@DavidM:“代码”?“被调用”?哪段代码?subprocess.Popen吗?它每60秒分叉一个新进程?你是这个意思吗?而且它从不等待任何一个子进程完成? - S.Lott
@Vinay Sajip:虽然据说Communicate会等待子进程,但我并不轻易相信它与正确的“wait”方法是一样的。这个应用程序似乎正在使用子进程超载系统。 - S.Lott
1
@S. Lott:我在Ubuntu上检查了Python 2.4.6的源代码 - communicate确实调用了self.wait()。那不是正确的wait方法吗? - Vinay Sajip
显示剩余2条评论

1

你有观察过时间内的进程吗?

  • lsof
  • ps -aux | grep -i pname
  • top

所有这些命令都应该提供有趣的信息。我认为该进程正在占用应该释放的资源。它是否有可能占用资源句柄(内存块、流、文件句柄、线程或进程句柄)?来自生成的“ps”的stdin、stdout、stderr。来自许多小的增量分配的内存句柄等。当该进程刚完成启动并第一次运行后以及在“坐”在那里定期启动子进程24小时后,我非常想看到上述命令显示的内容。

由于它在几天后会死亡,您可以让它仅运行几个循环,然后每天重新启动它作为解决方法。这将在此期间帮助您。

Jacob


0

你需要

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

释放资源。

注意:此方法在Windows上不可用。


1
Popen.communicate() 调用 Popen.wait(),后者为您调用 os.waitpid()。无需手动调用 os.waitpid()。 - user9876

0

我认为你提供的Zenoss文章中给出的情况并不是导致这个错误信息的唯一原因,所以目前还不清楚交换空间是否肯定是问题所在。我建议记录更多信息,即使是在成功调用时,这样你就可以看到每次在执行ps调用之前的可用内存状态。

还有一件事 - 如果在Popen调用中指定了shell=True,你是否看到了不同的行为?

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


我可以加入shell=True。那到底是做什么的?文档中说:“如果shell为True,则指定的命令将通过shell执行。”但这并没有真正解释区别是什么。 - davidmytton
当您指定 shell=True 时,会生成 shell 程序(例如 Linux 上的 bash,Windows 上的 cmd.exe),该程序再运行您想要生成的实际程序。这不建议作为降低内存使用的途径,而是作为另一个诊断工具,以查看行为如何更改。我期望从记录每个生成的内存条件以及查看失败调用和成功调用与内存、交换等状态的相关性中获得更有用的输入。 - Vinay Sajip
你有没有关于如何在脚本运行时记录内存使用情况的建议?我发现了http://code.activestate.com/recipes/286222/,它似乎可以胜任这个工作。 - davidmytton
重点不在于Python进程使用了多少内存,而在于记录所有ps的生成物返回的free -m。您可以使用subprocess来生成free -m并将结果记录到文件中。 - Vinay Sajip
我调用了mem = subprocess.Popen(['free', '-m'], stdout=subprocess.PIPE).communicate()[0],并在每个Popen调用之前和之后记录输出,内存使用似乎保持相对稳定,即内存不会慢慢耗尽。它始终在894/344/549(总/已用/空闲)左右。交换空间始终为0,但显然这是预期的,并且实际上有可用的交换空间,只是没有显示在free输出中。 - davidmytton
守护程序现在正在运行,并附加了 strace。下一次崩溃时会再次评论(需要几天时间)。 - davidmytton

0

虚拟内存很重要!!!

之前我也遇到过同样的问题,但在我的操作系统上添加了交换空间后就解决了。虚拟内存的计算公式通常是:SwapSize + 50% * PhysicalMemorySize。我最终通过增加物理内存或添加交换磁盘来解决这个问题,而close_fds在我的情况下不起作用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接