Python subprocess.Popen“OSError:[Errno 12]无法分配内存”

134

注意:此问题最初是在这里提出的,但赏金时间已过期,即使没有找到可接受的答案。我重新提出这个问题,包括原始问题中提供的所有细节。

一个Python脚本正在使用sched模块每60秒运行一组类函数:

# sc is a sched.scheduler instance
sc.enter(60, 1, self.doChecks, (sc, False))

脚本正在使用代码here作为守护进程运行。
一些被称为doChecks的类方法使用subprocess模块调用系统函数以获取系统统计信息:
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 -m 命令输出为:

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

这个服务器正在运行CentOS 5.3。我自己的CentOS盒子上无法复现,也没有其他用户报告同样的问题。

我尝试了原始问题中建议的许多调试方法:

  1. 在调用Popen之前,记录free -m的输出。内存使用量没有显著变化,即脚本运行时内存不会逐渐被使用完。

  2. 我在Popen调用中添加了close_fds=True,但这并没有改变什么——脚本仍然因为同样的错误而崩溃。建议参见此处此处

  3. 我检查了rlimits,发现RLIMIT_DATA和RLIMIT_AS都显示为(-1,-1),如此处所建议。

  4. 一篇文章认为没有交换空间可能是原因,但实际上根据Web主机的说法,交换空间是按需提供的,并且此处也认为这是一个虚假的原因。

  5. 进程被关闭是因为使用.communicate()的行为,这得到了Python源代码和注释的支持此处

整个检查过程可以在GitHub这里找到,其中getProcesses函数定义在442行。该函数由doChecks()在520行开始调用。

在崩溃之前,使用strace运行脚本并输出如下:

recv(4, "Total Accesses: 516662\nTotal kBy"..., 234, 0) = 234
gettimeofday({1250893252, 887805}, NULL) = 0
write(3, "2009-08-21 17:20:52,887 - checks"..., 91) = 91
gettimeofday({1250893252, 888362}, NULL) = 0
write(3, "2009-08-21 17:20:52,888 - checks"..., 74) = 74
gettimeofday({1250893252, 888897}, NULL) = 0
write(3, "2009-08-21 17:20:52,888 - checks"..., 67) = 67
gettimeofday({1250893252, 889184}, NULL) = 0
write(3, "2009-08-21 17:20:52,889 - checks"..., 81) = 81
close(4)                                = 0
gettimeofday({1250893252, 889591}, NULL) = 0
write(3, "2009-08-21 17:20:52,889 - checks"..., 63) = 63
pipe([4, 5])                            = 0
pipe([6, 7])                            = 0
fcntl64(7, F_GETFD)                     = 0
fcntl64(7, F_SETFD, FD_CLOEXEC)         = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xb7f12708) = -1 ENOMEM (Cannot allocate memory)
write(2, "Traceback (most recent call last"..., 35) = 35
open("/usr/bin/sd-agent/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/bin/sd-agent/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/lib/python24.zip/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/plat-linux2/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/lib/python2.4/lib-tk/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/lib-dynload/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/site-packages/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
write(2, "  File \"/usr/bin/sd-agent/agent."..., 52) = 52
open("/home/admin/sd-agent/daemon.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/bin/sd-agent/daemon.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/lib/python24.zip/daemon.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/daemon.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/plat-linux2/daemon.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/lib/python2.4/lib-tk/daemon.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/lib-dynload/daemon.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/site-packages/daemon.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
write(2, "  File \"/home/admin/sd-agent/dae"..., 60) = 60
open("/usr/bin/sd-agent/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/bin/sd-agent/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/lib/python24.zip/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/plat-linux2/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/lib/python2.4/lib-tk/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/lib-dynload/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/site-packages/agent.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
write(2, "  File \"/usr/bin/sd-agent/agent."..., 54) = 54
open("/usr/lib/python2.4/sched.py", O_RDONLY|O_LARGEFILE) = 8
write(2, "  File \"/usr/lib/python2.4/sched"..., 55) = 55
fstat64(8, {st_mode=S_IFREG|0644, st_size=4054, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7d28000
read(8, "\"\"\"A generally useful event sche"..., 4096) = 4054
write(2, "    ", 4)                     = 4
write(2, "void = action(*argument)\n", 25) = 25
close(8)                                = 0
munmap(0xb7d28000, 4096)                = 0
open("/usr/bin/sd-agent/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/bin/sd-agent/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/lib/python24.zip/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/plat-linux2/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/lib/python2.4/lib-tk/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/lib-dynload/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/site-packages/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
write(2, "  File \"/usr/bin/sd-agent/checks"..., 60) = 60
open("/usr/bin/sd-agent/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/bin/sd-agent/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/lib/python24.zip/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/plat-linux2/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOMEM (Cannot allocate memory)
open("/usr/lib/python2.4/lib-tk/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/lib-dynload/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/lib/python2.4/site-packages/checks.py", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
write(2, "  File \"/usr/bin/sd-agent/checks"..., 64) = 64
open("/usr/lib/python2.4/subprocess.py", O_RDONLY|O_LARGEFILE) = 8
write(2, "  File \"/usr/lib/python2.4/subpr"..., 65) = 65
fstat64(8, {st_mode=S_IFREG|0644, st_size=39931, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7d28000
read(8, "# subprocess - Subprocesses with"..., 4096) = 4096
read(8, "lso, the newlines attribute of t"..., 4096) = 4096
read(8, "code < 0:\n        print >>sys.st"..., 4096) = 4096
read(8, "alse does not exist on 2.2.0\ntry"..., 4096) = 4096
read(8, " p2cread\n        # c2pread    <-"..., 4096) = 4096
write(2, "    ", 4)                     = 4
write(2, "errread, errwrite)\n", 19)    = 19
close(8)                                = 0
munmap(0xb7d28000, 4096)                = 0
open("/usr/lib/python2.4/subprocess.py", O_RDONLY|O_LARGEFILE) = 8
write(2, "  File \"/usr/lib/python2.4/subpr"..., 71) = 71
fstat64(8, {st_mode=S_IFREG|0644, st_size=39931, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7d28000
read(8, "# subprocess - Subprocesses with"..., 4096) = 4096
read(8, "lso, the newlines attribute of t"..., 4096) = 4096
read(8, "code < 0:\n        print >>sys.st"..., 4096) = 4096
read(8, "alse does not exist on 2.2.0\ntry"..., 4096) = 4096
read(8, " p2cread\n        # c2pread    <-"..., 4096) = 4096
read(8, "table(self, handle):\n           "..., 4096) = 4096
read(8, "rrno using _sys_errlist (or siml"..., 4096) = 4096
read(8, " p2cwrite = None, None\n         "..., 4096) = 4096
write(2, "    ", 4)                     = 4
write(2, "self.pid = os.fork()\n", 21)  = 21
close(8)                                = 0
munmap(0xb7d28000, 4096)                = 0
write(2, "OSError", 7)                  = 7
write(2, ": ", 2)                       = 2
write(2, "[Errno 12] Cannot allocate memor"..., 33) = 33
write(2, "\n", 1)                       = 1
unlink("/var/run/sd-agent.pid")         = 0
close(3)                                = 0
munmap(0xb7e0d000, 4096)                = 0
rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x589978}, {0xb89a60, [], SA_RESTORER, 0x589978}, 8) = 0
brk(0xa022000)                          = 0xa022000
exit_group(1)                           = ?

1
是否出现了“管道”或文件描述符不足,或者与这些相关的内核资源不足? - Blauohr
检查 /var/log/messagesdmesg 命令。 - mark4o
日志中没有与此相关的内容。 - davidmytton
你解决这个问题了吗?我有非常类似的症状。我有足够的备用内存,但是在添加交换空间之后(就像你的一些答案建议的那样),问题消失了。只是想知道在那时和现在之间你是否发现了任何信息。--谢谢! - dpb
我遇到了同样的问题,但是没有解决方案 -- 有什么想法吗? - user248237
@user248237,你需要(按优先顺序):要么(1)增加更多的交换空间,或者(2)放宽超额提交策略(这样操作系统即使可用的空闲内存很少,也允许您fork一个大型进程如python,因为它“承诺”会紧随fork之后立即执行一个更小的进程),或者(3)减少脚本的内存占用,因此fork就不必代表您请求那么多的内存(追踪内存泄漏和膨胀等),或者**(4)生成辅助进程,使用posix_spawn/vfork**。 - vladr
8个回答

108
作为一般规则(即在原始内核中),fork/clone失败并显示ENOMEM 具体发生,因为要么是真正的内存不足情况dup_mmdup_task_structalloc_pidmpol_dupmm_init等崩溃),要么是由于security_vm_enough_memory_mm执行超额承诺策略时未能满足您的要求。

首先检查在fork尝试时失败的进程的vmsize,然后将其与空闲内存量(物理和交换)相比较,以了解过度承诺策略(插入数字)的相关性。

在您的特定情况下,请注意Virtuozzo在超额承诺实施方面有{{link1:其他检查}}。此外,我不确定您在容器内部对{{link3:交换和超额承诺配置}}有多少控制权(以影响执行结果)。

现在,为了真正前进,我会说您只剩下两个选择

  • 升级到更大的实例,或者
  • 在代码中付出一些努力来更有效地控制您的脚本使用的内存

注意,如果事实证明不是你,而是在同一服务器上与你共享空间的其他人在胡闹,那么编码的努力可能会白费。

就内存而言,我们已经知道subprocess.Popen 使用 fork/clone under the hood,这意味着每次调用它时,您都会请求比 Python 已经使用的内存多一倍的空间,也就是额外的几百 MB,只为了然后执行一个微小的 10kB 可执行文件,例如freeps。 在不良的过度承诺策略下,您很快就会看到 ENOMEM

替代 fork 的方法,不会出现父进程页面表复制等问题的有 vforkposix_spawn。但如果您不想重写 subprocess.Popen 的代码以使用 vfork/posix_spawn,可以考虑仅在脚本开始时(Python 的内存占用最小)使用一次 suprocess.Popen 来启动一个 shell 脚本,然后在脚本中并行运行 free/ps/sleep 等命令;轮询脚本的输出或同步读取它,如果有其他异步任务需要处理,则可能需要从单独的线程中进行 -- 在 Python 中进行数据处理,但将分叉留给子进程。 然而,在您的特定情况下,您可以完全跳过调用psfree; 直接从procfs中以Python直接获得该信息,无论您选择自己访问还是通过现有的库和/或软件包访问它。如果psfree是您运行的唯一工具,则可以完全不使用subprocess.Popen

最后,无论您在subprocess.Popen方面做什么,如果您的脚本泄漏内存,最终仍将达到极限。请注意,并检查内存泄漏


8
我发现在运行subprocess.Popen之前运行gc.collect()有帮助,特别是当垃圾收集器有一段时间没有运行时。 - letmaik
我编写了一个守护进程来处理辅助脚本策略:https://github.com/SeanHayes/errand-boy/我正在与我的一个客户一起在生产中使用它,我们的“无法分配内存”问题已经解决了。 - Seán Hayes
我会很感激一个简单的诊断,例如跟踪 /proc/fd/maps 来确定是否真的存在超额内存问题。 - Dima Tisnek

21

通过查看free -m的输出结果,我发现您实际上没有可用的交换空间。我不确定在Linux中交换空间是否总是会自动按需提供,但我遇到了同样的问题,这里的任何答案都没有真正帮助我。然而,添加一些交换内存解决了我的问题,因此既然这可能有助于其他遇到同样问题的人,我在这里发布如何添加1GB交换内存的答案(在Ubuntu 12.04上,但其他发行版应该类似)。

您可以首先检查是否启用了任何交换内存。

$sudo swapon -s

如果它是空的,意味着你没有启用任何交换空间。要添加 1GB 的交换空间:

$sudo dd if=/dev/zero of=/swapfile bs=1024 count=1024k
$sudo mkswap /swapfile
$sudo swapon /swapfile
将以下行添加到 fstab 中,使交换永久性。
$sudo vim /etc/fstab

     /swapfile       none    swap    sw      0       0 

可以在这里找到源代码和更多信息。


1
那个修复了同样的问题还是其他问题? - Dima Tisnek
这对我在CentOS 6.4上安装awstats时遇到的错误问题解决了,谢谢。 - Ruslan Abuzant
虽然这让我可以执行代码,但它并没有真正解决问题,问题可能在我使用的库中。 - philmaweb
1
你解决了我的问题。谢谢!+1 - sscirrus

13

为了简单的解决问题,您可以

echo 1 > /proc/sys/vm/overcommit_memory

如果您确信您的系统有足够的内存,请查看Linux over commit heuristic


1
非常感谢!这是一个如此简单的解决方案,你救了我的一天) - igolkotek
也许您知道如何在Windows上修复它?谢谢 - warfish
@warfish:在我的Windows电脑上还没有发生过。 - serv-inc

9

交换空间可能不是之前所说的红鲱鱼。在出现ENOMEM之前,要翻译的Python进程有多大?

在2.6内核下,/proc/sys/vm/swappiness控制内核多主动地使用交换空间,而overcommit*文件则控制内核可以眨眨眼睛分配多少和多精确的内存。就像你的Facebook关系状态一样,这很复杂

......但根据网络主机,交换空间实际上是按需提供的...

但是,根据您的free (1)命令输出,在您的服务器实例中没有识别到交换空间。现在,您的网络主机肯定比我更了解这个主题,但我使用过的虚拟RHEL / CentOS系统已报告为来宾操作系统提供可用的交换空间。

适应Red Hat KB Article 15252

Red Hat Enterprise Linux 5系统 不需要任何交换空间也可以正常运行, 只要匿名内存和系统V共享内存的总和小于 RAM量的3/4。...具有4GB或更少内存的系统 [建议至少]拥有2GB的交换空间。

将您的/proc/sys/vm设置与纯CentOS 5.3安装进行比较。添加一个交换文件。降低swappiness并查看是否生存更长。


最佳方法检查Python进程的大小是什么?使用ps命令吗? - davidmytton
类似于 ps -o user,pid,vsz="Mem(Kb)" -o cmd $PYTHON_PID 或者 top(1) 这样的命令应该可以实现。 - pilcrow

5
我认为你的客户/用户可能加载了一些内核模块或驱动程序,这些模块或驱动程序干扰了clone()系统调用(可能是某些晦涩的安全增强功能,类似于LIDS,但更加晦涩?)或者以某种方式填充了一些内核数据结构,这些结构是fork()/clone()操作所必需的(进程表、页面表、文件描述符表等)。

以下是fork(2)手册页的相关部分:

ERRORS
       EAGAIN fork()无法分配足够的内存来复制父进程的页面表并为子进程分配任务结构。
EAGAIN 由于遇到了调用者的RLIMIT_NPROC资源限制,因此无法创建新进程。要超过此限制,进程必须具有CAP_SYS_ADMIN或CAP_SYS_RESOURCE能力之一。
ENOMEM fork()由于内存不足而无法分配必要的内核结构。

我建议用户在启动原始、通用内核并仅加载了一组最小的模块和驱动程序(运行应用程序/脚本所需的最小集合)后尝试此操作。从那里开始,假设它在该配置中工作,则可以在该配置和出现问题的配置之间执行二进制搜索。这是标准的系统管理员故障排除101。

在您的strace中,相关行是:

clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xb7f12708) = -1 ENOMEM (Cannot allocate memory)

我知道其他人已经谈论了交换和内存可用性(我建议您设置至少一个小的交换分区,讽刺的是即使它在RAM磁盘上...当Linux内核有即使只有一点点交换可用时,代码路径会被广泛使用,而那些(异常处理路径)其中没有交换可用则不是这样。

然而,我怀疑这仍然是一个红鲱鱼。

free报告缓存和缓冲区未使用任何内存为0(零)的事实非常令人不安。我怀疑free输出......以及可能是您的应用程序问题,都是由某些专有的内核模块引起的,该模块以某种方式干扰内存分配。

根据fork()/clone()的手册页,如果您的调用会导致资源限制违规(RLIMIT_NPROC),fork()系统调用应返回EAGAIN......但是,它并没有说EAGAIN是否应由其他RLIMIT*违规返回。无论如何,如果您的目标/主机具有某种奇怪的Vormetric或其他安全设置(甚至如果您的进程在某种奇怪的SELinux策略下运行),那么它可能会导致此-ENOMEM故障。

这很不可能是普通的Linux / UNIX问题。您有一些非标准的东西在那里。


1
服务器正在运行基于Media Template (dv)的操作系统,该操作系统使用Virtuozzo进行虚拟化。 - davidmytton
尝试搜索Virtuozzo消息板和错误跟踪系统,或许还要寻找Virtuozzo子系统的升级。 - Jim Dennis

2

您尝试过使用以下方法吗:

(status,output) = commands.getstatusoutput("ps aux")

我原以为这解决了我的同样的问题。 但是后来我的进程被杀死了,而不是无法生成,这甚至更糟糕...
经过一些测试,我发现这只发生在较旧版本的Python上:它在2.6.5上发生,但在2.7.2上却没有发生。
我的搜索将我带到了这里python-close_fds-issue,但取消设置closed_fds并没有解决问题。但这篇文章仍然值得一读。
我发现Python通过仅关注它来泄漏文件描述符:
watch "ls /proc/$PYTHONPID/fd | wc -l"

和你一样,我也想捕获命令的输出,并且避免OOM错误...但看起来唯一的方法是让人们使用一个不那么有bug的Python版本。这并不理想...


1

0
munmap(0xb7d28000, 4096) = 0 write(2, "OSError", 7) = 7
我见过一些代码很懒,看起来像这样:
serrno = errno;
some_Syscall(...)
if (serrno != errno)
/* sound alarm: CATROSTOPHIC ERROR !!! */

你应该检查一下Python代码中是否发生了这种情况。如果前面的系统调用失败,Errno才是有效的。

编辑后添加:

你没有说这个进程存在多长时间。可能会消耗内存的消费者有:

  • 分叉进程
  • 未使用的数据结构
  • 共享库
  • 内存映射文件

2
是的,但我们从 OP 的 strace 中看到第一个系统调用失败——来自 clone()——是 ENOMEM,正如所报告的那样。即使 C 库的 errno 在此过程中被多次重置,这个错误仍然会在 Python 低内存跌倒时通过 traceback 构造得以保留。 - pilcrow

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