【问题标题】:Go 1.3 Garbage collector not releasing server memory back to systemGo 1.3 垃圾收集器没有将服务器内存释放回系统
【发布时间】:2014-08-14 03:16:15
【问题描述】:

我们编写了最简单的 TCP 服务器(带有少量日志记录)来检查内存占用(参见下面的 tcp-server.go)

服务器只接受连接并且什么都不做。它正在使用 Go 版本 go1.3 linux/amd64 的 Ubuntu 12.04.4 LTS 服务器(内核 3.2.0-61-generic)上运行。

附加的基准测试程序 (pulse.go) 在此示例中创建 10k 连接,在 30 秒后断开它们,重复此循环 3 次,然后连续重复 1k 连接/断开的小脉冲。用于测试的命令是 ./pulse -big=10000 -bs=30。

附上第一张图是通过记录runtime.ReadMemStats当客户端数量发生500倍变化时得到的,第二张图是“top”看到的服务器进程的RES内存大小。

服务器以可忽略不计的 1.6KB 内存启动。然后内存由 10k 连接的“大”脉冲设置为 ~60MB(如顶部所示),或大约 16MB“SystemMemory”,如 ReadMemStats 所示。正如预期的那样,当 10K 脉冲结束时,正在使用的内存下降,最终程序开始将内存释放回操作系统,灰色的“已释放内存”线就是证明。

问题在于系统内存(以及相应地,“top”看到的 RES 内存)从未显着下降(尽管它下降了一点,如第二张图所示)。

我们预计在 10K 脉冲结束后,内存将继续释放,直到 RES 大小达到处理每个 1k 脉冲所需的最小值(如“top”所示为 8m RES,由运行时.ReadMemStats)。相反,RES 保持在 56MB 左右,并且在使用中从未从最高值 60MB 下降。

我们希望确保偶尔出现峰值的不规则流量的可扩展性,并能够在同一机器上运行多个在不同时间出现峰值的服务器。有没有办法有效地确保在合理的时间范围内将尽可能多的内存释放回系统?

代码https://gist.github.com/eugene-bulkin/e8d690b4db144f468bc5

server.go:

package main

import (
  "net"
  "log"
  "runtime"
  "sync"
)
var m sync.Mutex
var num_clients = 0
var cycle = 0

func printMem() {
  var ms runtime.MemStats
  runtime.ReadMemStats(&ms)
  log.Printf("Cycle #%3d: %5d clients | System: %8d Inuse: %8d Released: %8d Objects: %6d\n", cycle, num_clients, ms.HeapSys, ms.HeapInuse, ms.HeapReleased, ms.HeapObjects)
}

func handleConnection(conn net.Conn) {
  //log.Println("Accepted connection:", conn.RemoteAddr())
  m.Lock()
  num_clients++
  if num_clients % 500 == 0 {
    printMem()
  }
  m.Unlock()
  buffer := make([]byte, 256)
  for {
    _, err := conn.Read(buffer)
    if err != nil {
      //log.Println("Lost connection:", conn.RemoteAddr())
      err := conn.Close()
      if err != nil {
        log.Println("Connection close error:", err)
      }
      m.Lock()
      num_clients--
      if num_clients % 500 == 0 {
        printMem()
      }
      if num_clients == 0 {
        cycle++
      }
      m.Unlock()
      break
    }
  }
}

func main() {
  printMem()
  cycle++
  listener, err := net.Listen("tcp", ":3033")
  if err != nil {
    log.Fatal("Could not listen.")
  }
  for {
    conn, err := listener.Accept()
    if err != nil {
      log.Println("Could not listen to client:", err)
      continue
    }
    go handleConnection(conn)
  }
}

pulse.go:

package main

import (
  "flag"
  "net"
  "sync"
  "log"
  "time"
)

var (
  numBig = flag.Int("big", 4000, "Number of connections in big pulse")
  bigIters = flag.Int("i", 3, "Number of iterations of big pulse")
  bigSep = flag.Int("bs", 5, "Number of seconds between big pulses")
  numSmall = flag.Int("small", 1000, "Number of connections in small pulse")
  smallSep = flag.Int("ss", 20, "Number of seconds between small pulses")
  linger = flag.Int("l", 4, "How long connections should linger before being disconnected")
)

var m sync.Mutex

var active_conns = 0
var connections = make(map[net.Conn] bool)

func pulse(n int, linger int) {
  var wg sync.WaitGroup

  log.Printf("Connecting %d client(s)...\n", n)
  for i := 0; i < n; i++ {
    wg.Add(1)
    go func() {
      m.Lock()
      defer m.Unlock()
      defer wg.Done()
      active_conns++
      conn, err := net.Dial("tcp", ":3033")
      if err != nil {
        log.Panicln("Unable to connect: ", err)
        return
      }
      connections[conn] = true
    }()
  }
  wg.Wait()
  if len(connections) != n {
    log.Fatalf("Unable to connect all %d client(s).\n", n)
  }
  log.Printf("Connected %d client(s).\n", n)
  time.Sleep(time.Duration(linger) * time.Second)
  for conn := range connections {
    active_conns--
    err := conn.Close()
    if err != nil {
      log.Panicln("Unable to close connection:", err)
      conn = nil
      continue
    }
    delete(connections, conn)
    conn = nil
  }
  if len(connections) > 0 {
    log.Fatalf("Unable to disconnect all %d client(s) [%d remain].\n", n, len(connections))
  }
  log.Printf("Disconnected %d client(s).\n", n)
}

func main() {
  flag.Parse()
  for i := 0; i < *bigIters; i++ {
    pulse(*numBig, *linger)
    time.Sleep(time.Duration(*bigSep) * time.Second)
  }
  for {
    pulse(*numSmall, *linger)
    time.Sleep(time.Duration(*smallSep) * time.Second)
  }
}

【问题讨论】:

  • 还询问(并回答)here。 (Alec,在制作交叉帖子时提供指向他们自己的交叉帖子的链接是一个很好的网友的责任。)

标签: memory memory-management go


【解决方案1】:

正如 LinearZoetrope 所说,您应该等待至少 7 分钟来检查释放了多少内存。有时需要两次 GC,所以需要 9 分钟。

如果还是不行,或者时间太长,可以给 FreeOSMemory 添加周期性调用(之前不需要调用 runtime.GC(),由 debug.FreeOSMemory() 完成)

类似这样的:http://play.golang.org/p/mP7_sMpX4F

package main

import (
    "runtime/debug"
    "time"
)

func main() {
    go periodicFree(1 * time.Minute)

    // Your program goes here

}

func periodicFree(d time.Duration) {
    tick := time.Tick(d)
    for _ = range tick {
        debug.FreeOSMemory()
    }
}

考虑到每次调用 FreeOSMemory 都需要一些时间(不多),如果 GOMAXPROCS&gt;1 从 Go1.3 开始,它可以部分并行运行。

【讨论】:

    【解决方案2】:

    首先,请注意 Go 本身并不总是缩小自己的内存空间:

    https://groups.google.com/forum/#!topic/Golang-Nuts/vfmd6zaRQVs

    堆被释放,你可以使用 runtime.ReadMemStats() 来检查, 但进程虚拟地址空间不会缩小 - 即,您的 程序不会将内存返回给操作系统。基于 Unix 平台我们使用系统调用告诉操作系统它 可以回收堆中未使用的部分,此设施不可用 在 Windows 平台上。

    但你不是在 Windows 上,对吧?

    嗯,这个帖子不太确定,但它说:

    https://groups.google.com/forum/#!topic/golang-nuts/MC2hWpuT7Xc

    据我了解,内存在被标记后大约 5 分钟返回给操作系统 由 GC 免费提供。 GC 每两分钟运行一次,如果不是 由内存使用量增加触发。所以最坏的情况是 7 分钟被释放。

    在这种情况下,我认为切片没有被标记为已释放,而是在 使用,所以它永远不会返回给操作系统。

    您可能没有等待足够长的时间来等待 GC 扫描,然后是操作系统返回扫描,这可能在最后一个“大”脉冲之后长达 7 分钟。您可以使用runtime.FreeOSMemory 显式强制执行此操作,但请记住,除非已运行 GC,否则它不会执行任何操作。

    (编辑:请注意,您可以使用runtime.GC() 强制进行垃圾收集,但显然您需要注意使用它的频率;您可以在连接突然下降的情况下同步它)。

    顺便说一句,我找不到明确的来源(除了我发布的第二个帖子有人提到了同样的事情),但我记得它被多次提到并不是 Go 使用的所有内存是“真实”的记忆。如果它是由运行时分配但实际上并没有被程序使用,那么无论topMemStats 说什么,操作系统实际上都在使用内存,因此程序“真正”使用的内存量通常非常多报。


    编辑:作为 cmets 中的 Kostix notex 并支持 JimB 的回答,这个问题被交叉发布在 Golang-nuts 上,我们从 Dmitri Vyukov 那里得到了相当明确的回答:

    https://groups.google.com/forum/#!topic/golang-nuts/0WSOKnHGBZE/discussion

    我今天没有解决方案。 大部分内存似乎都被 goroutine 堆栈占用了,我们不会将这些内存释放给 OS。 下个版本会更好一些。

    所以我所概述的仅适用于堆变量,Goroutine 堆栈上的内存永远不会被释放。这与我最后一个“并非所有显示的分配的系统内存都是‘真实内存’”这一点的交互作用还有待观察。

    【讨论】:

    • 我们实际上等了 30 多分钟:通过查看第一张图中的灰线,您可以看到内存被释放了两次(大约 7 分钟和 9 分钟)。 runtime.ReadMemStats 显示的内存释放量与第二张图中top 显示的 RES 下降完全对应。正如 JimB 在下面回答的那样,也在我的交叉帖子 [groups.google.com/forum/#!topic/golang-nuts/0WSOKnHGBZE/…] 中回答,不幸的是,分配的 goroutines 永远不会将内存返回给操作系统......
    • 嗯,部分问题在于该图表对程序运行的时间不明确,因为我不知道“短脉冲”之间的间隔。无论哪种方式,我都忍者编辑了我的帖子,以便在您发表评论之前参考 Dmitri 的答案。
    • 我知道这是一个旧线程,但我相信runtime.FreeOSMemory() 强制进行垃圾收集,因此无需在runtime.FreeOSMemory() 之前调用runtime.GC() 编辑:实际上没有注意到下面的答案提到了这一点。
    • 现在 go1.16 变了!看到这个:github.com/golang/go/commit/…
    【解决方案3】:

    不幸的是,答案很简单,goroutine 堆栈目前无法释放。

    由于您要同时连接 10k 个客户端,因此您需要 10k 个 goroutine 来处理它们。每个 goroutine 都有一个 8k 的堆栈,即使只有第一页出错,您仍然需要至少 40M 的永久内存来处理最大连接数。

    有一些待定的更改可能对 go1.4 有所帮助(例如 4k 堆栈),但这是我们现在必须接受的事实。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-11-22
      • 1970-01-01
      • 2019-02-10
      • 2014-12-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多