Мы написали простейший возможный 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 МБ вообще.
Мы хотим обеспечить масштабируемость для нерегулярного трафика с периодическими всплесками, а также возможность запуска нескольких серверов в том же ящике, который имеет шипы в разное время. Есть ли способ эффективно обеспечить, чтобы как можно больше памяти было выпущено обратно в систему в разумные сроки?
Код 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)
}
}