执行并发os / exec.Command.Wait()时出现内存泄漏

时间:2015-12-17 23:16:30

标签: linux go memory-leaks

我遇到了一个go程序占用15gig虚拟内存并继续增长的情况。问题只发生在我们的CentOS服务器上。在我的OSX开发机器上,我无法重现它。

我是否发现了一个错误,或者我做错了什么?

我把问题归结为一个简单的演示,我现在将其描述。首先构建并运行此服务器:

package main

import (
    "net/http"
    "os/exec"
)

func main() {
    http.HandleFunc("/startapp", startAppHandler)
    http.ListenAndServe(":8081", nil)
}

func startCmd() {
    cmd := exec.Command("/tmp/sleepscript.sh")
    cmd.Start()
    cmd.Wait()
}

func startAppHandler(w http.ResponseWriter, r *http.Request) {
    startCmd()
    w.Write([]byte("Done"))
}

创建一个名为/tmp/sleepscript.sh的文件,并将其chmod为755

#!/bin/bash
sleep 5

然后向/ startapp发出几个并发请求。在bash shell中,您可以这样做:

for i in {1..300}; do (curl http://localhost:8081/startapp &); done

VIRT内存现在应该是几千兆字节。如果重新运行上面的for循环,VIRT内存将每次继续增长千兆字节。

更新1:问题是我在CentOS上遇到了OOM问题。 (谢谢@nos)

更新2 :使用daemonize并将调用同步到Cmd.Run(),解决了该问题。感谢@JimB确认在其自己的线程中运行的.Wait()是POSIX api的一部分,并且没有办法避免在不泄漏资源的情况下调用.Wait()

1 个答案:

答案 0 :(得分:2)

您发出的每个请求都要求Go在子进程上为Wait生成一个新的OS线程。每个线程将消耗2MB的堆栈,以及更大的VIRT内存(这不太相关,因为它是虚拟的,但你可能仍然会达到ulimit设置)。 Go运行时重用了线程,但它们目前从未被销毁,因为大多数使用大量线程的程序都会再次这样做。

如果您同时发出300个请求,并在创建任何其他请求之前等待它们完成,则内存应该稳定下来。但是,如果在其他请求完成之前继续发送更多请求,则会耗尽一些系统资源:内存,文件描述符或线程。

关键是产生子进程并调用wait并不是免费的,如果这是一个真实世界的用例,你需要限制startCmd()被调用的次数同时。