Go 1.3垃圾收集器不会将服务器内存释放回系统如何解决?

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (2)
  • 关注 (0)
  • 查看 (299)

我们编写了最简单的TCP服务器(使用次要日志记录)来检查内存占用情况(请参见下面的tcp-server.go)

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

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

第一个附加图是通过记录runtime.ReadMemStats获得的,当客户端数量已经改变了500的倍数时,第二个图形是服务器进程的“top”所看到的RES内存大小。

服务器的内存可以忽略不计1.6KB。然后通过10k连接的“大”脉冲设置存储器,大小约60MB(如上图所示),或ReadMemStats所见的大约16MB“SystemMemory”。正如预期的那样,当10K脉冲结束时,正在使用的内存下降,最终程序开始将内存释放回操作系统,如灰色的“已释放内存”行所示。

问题在于系统内存(相应地,“top”看到的RES内存)永远不会显着下降(尽管它在第二个图中看到的有点下降)。

我们预计,在10K脉冲结束后,内存将继续释放,直到RES大小为处理每个1k脉冲所需的最小值(从顶部看,其值为8m RES,运行时报告为2MB使用中.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)
  }
}
提问于
用户回答回答于

goroutine堆栈目前不能发布。

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

有一些未决的变化可能有助于go1.4(如4k堆栈),但这是我们现在必须忍受的事实。

用户回答回答于

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

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

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

但是你不在Windows上,对吧?

那么这个线程的确定性不大,但它说:

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

据我了解,内存在被GC标记为空闲后大约5分钟后返回到操作系统。GC每隔两分钟运行一次,如果不是由内存使用增加引起的。所以最坏的情况是7分钟才能获得释放。 在这种情况下,我认为该片没有被标记为释放,但在使用中,所以它永远不会被返回到操作系统。

有可能你没有足够长的时间进行GC扫描,然后进行OS返回扫描,可能会在最后的“大”脉冲后7分钟。可以明确地强制执行此操作runtime.FreeOSMemory,但请记住,除非GC已运行,否则它不会执行任何操作。

(请注意,你可以强制垃圾收集,runtime.GC()但显然你需要小心你多久使用一次;你可能能够将它与突然下降的连接同步)。

除此之外,我找不到一个明确的来源(除了我发布的第二个帖子,其中有人提到了同样的事情),但我记得它多次提到,并非所有使用的记忆都是“真实的“记忆。如果它是由运行时分配的,但实际上并未被程序使用,则操作系统实际上已经使用了内存,而不管该内存是什么topMemStats所说的,所以程序“真正”使用的内存量通常是非常少报的。

这个问题是在Golang坚果上交叉发布的,我们从Dmitri Vyukov得到了一个相当明确的答案:

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

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

所以我概述的只适用于堆变量,Goroutine堆栈上的内存永远不会被释放。这与我最后一次“并非所有显示分配的系统内存都是真实内存”相互作用的原因仍有待观察。

扫码关注云+社区

领取腾讯云代金券