memory - Go 1.3 垃圾收集器没有将服务器内存释放回系统

标签 memory memory-management go

我们编写了最简单的 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,而 2MB in-use 报告为运行时.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)
  }
}

最佳答案

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

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

The heap is freed, you can check this using runtime.ReadMemStats(), but the processes virtual address space does not shrink -- ie, your program will not return memory to the operating system. On Unix based platforms we use a system call to tell the operating system that it can reclaim unused parts of the heap, this facility is not available on Windows platforms.

但你不是在 Windows 上,对吧?

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

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

As I understand, memory is returned to the OS about 5 minutes after is has been marked as free by the GC. And the GC runs every two minutes top, if not triggered by an increase in memory use. So worst-case would be 7 minutes to be freed.

In this case, I think that the slice is not marked as freed, but in use, so it would never be returned to the OS.

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

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

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


编辑:正如评论中的 Kostix 注释并支持 JimB 的回答,这个问题被交叉发布在 Golang-nuts 上,我们从 Dmitri Vyukov 那里得到了一个相当明确的回答:

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

I don't there is a solution today. Most of the memory seems to be occupied by goroutine stacks, and we don't release that memory to OS. It will be somewhat better in the next release.

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

关于memory - Go 1.3 垃圾收集器没有将服务器内存释放回系统,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24376817/

相关文章:

c - 在C语言中,如何更改指针指向的内存地址?

c++ - C++ 中枚举数的作用域

c# - Hashtable 的内存是如何分配的?

c++ - 将对象插入具有动态内存的数组中(不允许 vector )C++

logging - 是否可以修改其他包中定义的类型的方法集?

c++ - 为什么在 Linux 上使用更多线程时内存消耗会增加? (C++)

"File".exe : 0xC0000005: Access violation reading location 0x00000044 中 0x6C7DB2AA (SDL2.dll) 处的 C++ SDL 未处理异常

c - 2D 和 3D 数组的动态分配/解除分配

azure - 使用 Go 的 Azure ServiceBus 队列的 OpenTelemetry 传播问题

Golang Scan 不扫描下一行