转1.3垃圾收集器不将服务器内存释放回系统

Ale*_*sis 15 memory memory-management go

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

服务器只接受连接而不执行任何操作.它运行在Ubuntu 12.04.4 LTS服务器(内核3.2.0-61-generic)上,Go版本为go1.3 linux/amd64.

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

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

服务器的内存可以忽略不计1.6KB.然后,内存由10k连接的"大"脉冲设置在~60MB(如上图所示),或者由ReadMemStats看到的大约16MB"SystemMemory".正如预期的那样,当10K脉冲结束时,使用中的内存会下降,最终程序开始将内存释放回OS,如灰色的"释放内存"行所示.

问题是系统内存(相应地,"顶部"看到的RES内存)从未显着下降(尽管它下降了一点,如第二张图所示).

我们可以预期,在10K脉冲结束后,内存将继续释放,直到RES大小是处理每个1k脉冲所需的最小值(由"top"看到的是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)
  }
}
Run Code Online (Sandbox Code Playgroud)

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

Lin*_*ope 16

首先,请注意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,则每隔两分钟运行一次GC.所以最糟糕的情况是7分钟被释放.

在这种情况下,我认为切片未标记为已释放,但在使用中,因此它永远不会返回到操作系统.

有可能你没有等待GC扫描,然后是OS返回扫描,这可能是最后"大"脉冲后7分钟.您可以明确强制执行此操作runtime.FreeOSMemory,但请记住,除非已运行GC,否则它不会执行任何操作.

(编辑:请注意,您可以强制进行垃圾收集,runtime.GC()但显然您需要小心使用它的频率;您可以将其与连接中突然向下的峰值同步).

稍微一点,我找不到一个明确的来源(除了我发布的第二个帖子,有人提到同样的事情),但我记得有几次被提到并不是所有内存Go使用的都是"真实的"记忆.如果它由运行时分配但实际上没有被程序使用,那么OS实际上使用了内存而不管是什么topMemStats说什么,因此程序"真正"使用的内存量通常被过度报告.


编辑:作为Kostix notex在评论中并支持JimB的回答,这个问题是在Golang-nuts上转发的,我们从Dmitri Vyukov得到了一个相当确定的答案:

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

我今天没有解决方案.大多数内存似乎被goroutine堆栈占用,我们不会将该内存释放到操作系统.在下一个版本中会有所改善.

所以我的概述仅适用于堆变量,Goroutine堆栈上的内存永远不会被释放.这与我上一次"并非所有显示分配的系统内存都是'实际内存'"的交互方式还有待观察.


sir*_*nga 6

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

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