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

Gat*_*ter 5 linux memory-leaks go

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

我是否发现了 go 中的错误,或者我做错了什么?

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

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"))
}
Run Code Online (Sandbox Code Playgroud)

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

#!/bin/bash
sleep 5
Run Code Online (Sandbox Code Playgroud)

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

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

VIRT 内存现在应该有几 GB。如果重新运行上面的 for 循环,VIRT 内存每次都会继续以 GB 为单位增长。

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

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

Jim*_*imB 3

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

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

关键点是生成子进程和调用不是免费的,如果这是一个现实世界的用例,您需要限制可以并发调用wait的次数。startCmd()