【问题标题】:Go memory leak when doing concurrent os/exec.Command.Wait()执行并发 os/exec.Command.Wait() 时发生内存泄漏
【发布时间】:2016-03-24 14:22:14
【问题描述】:

我遇到了一个情况,一个 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"))
}

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

#!/bin/bash
sleep 5

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

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

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

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

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

【问题讨论】:

  • VirtualMemory,尤其是在 OSX 上,毫无意义。 RSS 可以更好地指导进程占用多少内存。
  • 提示:Run() 执行进程并挂起直到退出。
  • 我还建议分配一个pool of goroutines 来限制同时运行的进程数。
  • @Zippoxer 你的分数很受欢迎,但是 Run 也遇到了同样的虚拟内存爆炸。 Run 只是 .Start() 后跟 .Wait() 的简写。对于许多项目来说,goroutines 池是一个很好的架构选择,但在这种情况下并不是我真正想要的。
  • @Gattster 我建议您使用此信息更新帖子。虚拟内存增长到数 GB 本身并没有任何问题。即使我们人类不喜欢在pstop 中看到如此庞大的数字,也可以抓取大量可能仍未使用的虚拟内存。你最终会遇到 OOM,至少如果情况是这样的话您确实要等到所有 300 个请求都完成并且 shell 脚本终止,然后再触发另一个 300 个请求的测试是一个问题。

标签: linux go memory-leaks


【解决方案1】:

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

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

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

【讨论】:

  • 感谢您的回答。这很有启发性,但我认为这里正在发生更多错误。如果我启动 300 个命令并让它们处于等待状态,我的进程只使用几兆的 virt ram。如果我同时启动这 300 个命令,那么我们开始使用 10+gb 的 VIRT,并且只有在我同时启动命令时它才会再次增长。
  • @Gattster 问题出在“完全相同的时间”部分。如果wait 完全被串行调用,线程可以被重用,但是当所有现有线程都在阻塞系统调用中时,运行时需要产生一个新线程才能继续运行。查看两个示例之间进程中的线程数。
  • 谢谢@JimB。我正在考虑通过使用daemonize /tmp/sleepscript.sh 运行我的长时间运行的子进程并将我的调用同步到Cmd.Run() 来避免这个问题。
  • @Gattster run 也叫wait。此处回答的示例代码stackoverflow.com/questions/33948726/… 可能会有所帮助
  • @JiangYD:对于长时间运行的服务器(这可能是一个 http 服务器)来说,这不是一个好主意,因为您的示例将每个执行的命令都视为僵尸。
猜你喜欢
  • 2018-09-25
  • 1970-01-01
  • 2014-12-11
  • 2013-07-09
  • 1970-01-01
  • 1970-01-01
  • 2011-05-13
  • 2018-06-08
  • 2011-05-03
相关资源
最近更新 更多