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

时间:2014-06-24 00:59:33

标签: 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的倍数时,第二个图形是服务器进程的“top”看到的RES内存大小。

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

问题是系统内存(相应地,“顶部”看到的RES内存)从未显着下降(尽管它在第二个图中看到的有点下降)。

我们预计在10K脉冲结束后,内存将继续释放,直到RES大小是处理每个1k脉冲所需的最小值(这是8m RES,如“top”所示,2MB在使用中由runtime.ReadMemStats)。相反,RES保持在大约56MB,并且在使用中永远不会从最高值60MB下降。

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

First graph

Second graph

代码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)
  }
}

3 个答案:

答案 0 :(得分:15)

首先,请注意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扫描,然后进行操作系统返回扫描,这可能是在最终&#34;大&#34;之后最多7分钟。脉冲。您可以使用runtime.FreeOSMemory明确强制执行此操作,但请记住,除非已运行GC,否则它不会执行任何操作。

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

稍微一点,我找不到这个明确的来源(除了我发布的第二个帖子,有人提到同样的事情),但我记得有几次被提到并不是所有的记忆Go用途是&#34;真实&#34;记忆。如果它由运行时分配但实际上没有被程序使用,那么无论topMemStats表示什么,操作系统实际上都使用了内存,因此程序的内存量是真的&#34;使用经常被过度报道。


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

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

  

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

所以我的概述仅适用于堆变量,Goroutine堆栈上的内存永远不会被释放。这与我的上一次&#34;以及所有显示的分配的系统内存的交互方式究竟是真实内存&#39;&#34;&#34;还有待观察。

答案 1 :(得分:4)

遗憾的是,答案非常简单,目前无法发布goroutine堆栈。

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

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

答案 2 :(得分:1)

正如Jsor所说,你应该等待至少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都需要一些时间(不多),并且如果从Go1.3开始GOMAXPROCS>1,它可以部分并行运行。