Go 1.3 Сборщик мусора, не отбрасывающий серверную память обратно в систему

Мы написали простейший возможный TCP-сервер (с незначительным протоколированием), чтобы изучить область памяти (см. ниже tcp-server.go)

Сервер просто принимает подключения и ничего не делает. Он запускается на сервере Ubuntu 12.04.4 LTS (ядро 3.2.0-61-generic) с версией Go go1.3 linux/amd64.

Прилагаемая программа бенчмаркинга (pulse.go) создает в этом примере соединения 10 тыс., отключает их через 30 секунд, повторяет этот цикл три раза, а затем непрерывно повторяет небольшие импульсы 1k соединений/отключений. Команда, используемая для тестирования, была. /pulse -big = 10000 -bs = 30.

Первый прикрепленный график получается путем записи runtime.ReadMemStats, когда количество клиентов изменилось на несколько из 500, а второй график - размер памяти RES, видимый "сверху" для серверного процесса.

Сервер начинается с незначительной 1,6 Кбайт памяти. Затем память устанавливается "большими" импульсами 10k-соединений на уровне ~ 60 МБ (как видно сверху) или примерно на 16 МБ "SystemMemory", как видно ReadMemStats. Как и ожидалось, когда импульсы 10K заканчиваются, операционная память падает, и в конечном итоге программа начинает освобождать память обратно в ОС, о чем свидетельствует серая строка "Выпущенная память".

Проблема в том, что системная память (и, соответственно, память RES, видимая "сверху" ), никогда не падает значительно (хотя она немного падает, как видно на втором графике).

Мы ожидаем, что после окончания 10K импульсов память будет продолжать выпускаться до тех пор, пока размер RES не станет минимальным, необходимым для обработки каждого импульса 1k (что составляет 8 м ВИЭ, как видно из "верхнего" и 2 МБ, runtime.ReadMemStats). Вместо этого RES остается около 56 МБ, а использование никогда не падает с самого высокого значения 60 МБ вообще.

Мы хотим обеспечить масштабируемость для нерегулярного трафика с периодическими всплесками, а также возможность запуска нескольких серверов в том же ящике, который имеет шипы в разное время. Есть ли способ эффективно обеспечить, чтобы как можно больше памяти было выпущено обратно в систему в разумные сроки?

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

Ответы

Ответ 1

Во-первых, обратите внимание, что сам 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 использует является "реальной" памятью. Если он выделяется средой выполнения, но фактически не используется программой, ОС фактически использует память независимо от того, что говорит top или MemStats, поэтому объем памяти, который программа "действительно" использует, часто очень overreported.


Редактировать: Как Kostix notex в комментариях и поддерживает ответ JimB, этот вопрос был перекрестным на Golang-орехах, и мы получили довольно окончательный ответ от Дмитрия Вюкова:

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

Сегодня у меня нет решения. Большая часть памяти, похоже, занята стекями goroutine, и мы не выпускаем эту память для ОС. В следующем выпуске он будет несколько лучше.

Итак, то, что я опишу, применимо только к переменным кучи, память в стеке Goroutine никогда не будет выпущена. Как именно это взаимодействует с моей последней "не все показанной выделенной системной памятью", "точка" реальной памяти "остается не видной.

Ответ 2

Ответ, к сожалению, довольно прост, goroutine stacks в настоящее время не могут быть выпущены.

Поскольку вы подключаете сразу 10k клиентов, вам нужно 10k goroutines для их обработки. Каждый goroutine имеет 8-килобайтный стек, и даже если сбой только первой страницы, вам по-прежнему требуется как минимум 40M постоянной памяти для обработки ваших максимальных подключений.

Есть некоторые ожидающие изменения, которые могут помочь в go1.4 (например, 4k стеки), но это факт, с которым мы должны жить сейчас.

Ответ 3

Как сказал 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 займет некоторое время (не много), и его можно частично запустить параллельно, если GOMAXPROCS>1 с Go1.3.