Possible memory leak problem

650 views
Skip to first unread message

silvia

unread,
Sep 28, 2010, 8:29:28 AM9/28/10
to golang-nuts
I wated to measure the response time of an http server while serving a
variable number of concurrent clients. So I've written a simple http
server and I've created a simple app which starts many goroutines (its
number is got by an app flag). Each goroutine sends a variable number
of requests to the server and collects the response time.
The time is ok. The problem is on the memory allocated by the server.
It grows and grows and is never freed.
I've chacked memory occupation with "ps au" and also with the
profiler: "gopprof --web http://localhost:6060/debug/pprof/heap".

Can you please help me to find out if there is a memory leak in the
server?
Thank you in advance.
Silvia

Here the code:

httpservt.go

/*
Test HTTP server.
*/

package main

import (
"http"
"log"
)
import _ "http/pprof"


// Server configuration
const indirSrv = "localhost"
const portaSrv = "6060"
const indirCompletoSrv = indirSrv + ":" + portaSrv

func StringToByteSlice(s string) []byte {
buf := make([]byte, len(s))
copyString(buf, 0, s)
return buf
}
// Copy from string to byte array at offset doff. Assume there's
room.
func copyString(dst []byte, doff int, str string) {
for soff := 0; soff < len(str); soff++ {
dst[doff] = str[soff]
doff++
}
}
// Server handler function.
func TempiServer(c *http.Conn, req *http.Request) {

c.Write(StringToByteSlice("OK"))
}


func main() {

http.Handle("/crono/", http.HandlerFunc(TempiServer))
log.Stdoutf("HTTP server listening on %s\n", indirCompletoSrv)
err := http.ListenAndServe(indirCompletoSrv, nil)
if err != nil {
log.Crash("ListenAndServe error: ", err)
}
log.Stdout("HTTP server: END")
}

----------
clirunnert.go

/*
Client launcher

Usage:
clirunnert [-conn=HTTP | SOCKET] (default HTTP)
[-maxThread=# of client sessions to execute] (default 5)
[-maxRichieste=# of requests per session] (default 4)
*/

package main

import (
"os"
"bytes"
"log"
"fmt"
"time"
"flag"
"strings"
"bufio"
"io"
"http"
"net")

/* --------------------------------------------------------------
Options
-------------------------------------------------------------- */
var tipoConnessione *string = flag.String("conn", "HTTP", "Tipo
connessione (HTTP | SOCKET)")
var maxThread *int = flag.Int("maxThread", 5, "N. thread da lanciare")
var maxRichieste *int = flag.Int("maxRichieste", 4, "N. richieste x
sessione")


/* --------------------------------------------------------------
Client configuration
-------------------------------------------------------------- */
const indirSrv = "localhost"
const portaSrv = "6060"
const indirCompletoSrv = indirSrv + ":" + portaSrv

/* --------------------------------------------------------------
Client Definition
-------------------------------------------------------------- */
type HttpResponse struct {
Resp http.Response
}

type Richiedente interface {
OpenConnection(url string) (err os.Error)
MakeRequest(url string, method string, body io.Reader) (err os.Error)
GetResponse()(HttpResponse)
CloseConnection()
}
func (NoCloser) Close() os.Error {return nil}



type HttpclientSoc struct {
Conn net.Conn
Resp HttpResponse
}
func (cli *HttpclientSoc) OpenConnection(url string) (err os.Error) {
var conn net.Conn
conn, err = net.Dial("tcp", "", url)
if err == nil {
// Assegno la connessione al client
cli.Conn = conn
}
return err
}
func (cli *HttpclientSoc) CloseConnection() {
cli.Conn.Close()
}
func (cli *HttpclientSoc) MakeRequest(url string, method string,
body io.Reader) (err os.Error) {

var req http.Request
req, err = creDefaultReq(url, method, body)
if err != nil {
return os.NewError("MakeRequest()-error in creDefaultReq(): "
+ err.String())
}
err = req.Write(cli.Conn)
if err != nil {
cli.Conn.Close()
return os.NewError("MakeRequest()-error in req.Write(): " +
err.String())
}

cli.Resp = *new(HttpResponse)

var resp *http.Response
reader := bufio.NewReader(cli.Conn)
resp, err = http.ReadResponse(reader, req.Method)
if err != nil {
cli.Conn.Close()
return os.NewError("MakeRequest()-error in http.ReadResponse:
" + err.String())
}
cli.Resp.Resp = *resp

cli.Resp.Resp.Body = readClose{cli.Resp.Resp.Body, cli.Conn}

return err

}
func (cli *HttpclientSoc) GetResponse() (HttpResponse) {
return cli.Resp
}


type HttpclientHttp struct {
Conn *http.ClientConn
Resp HttpResponse
}
func (cli *HttpclientHttp) OpenConnection(url string) (err os.Error) {
var conn net.Conn
conn, err = net.Dial("tcp", "", url)
if err == nil {
httpConn := http.NewClientConn(conn, nil)
cli.Conn = httpConn
}

return err
}
func (cli *HttpclientHttp) CloseConnection() {
cli.Conn.Close()
}
func (cli *HttpclientHttp) MakeRequest(url string, method string,
body io.Reader) (err os.Error) {

var req http.Request
req, err = creDefaultReq(url, method, body)
if err != nil {
return err
}

cli.Conn.Write(&req)
if err != nil {
cli.Conn.Close()
return err
}

cli.Resp = *new(HttpResponse)

var resp *http.Response
resp, err = cli.Conn.Read()
if err != nil {
cli.Conn.Close()
return err
}
cli.Resp.Resp = *resp

return err

}
func (cli *HttpclientHttp) GetResponse() (HttpResponse) {
return cli.Resp
}


// Used in Send to implement io.ReadCloser by bundling together the
// io.BufReader through which we read the response, and the underlying
// network connection.
type readClose struct {
io.Reader
io.Closer
}

type NoCloser struct {
io.Reader
}

func creDefaultReq(url string, method string,
body io.Reader) (req http.Request, err os.Error) {

req.Method = strings.ToUpper(method)
req.ProtoMajor = 1
req.ProtoMinor = 1
req.Close = false
req.Body = NoCloser{body}

req.URL, err = http.ParseURL(url)
if err != nil {
return req, err
}
// Header
req.Header = map[string]string {
"Content-Type": "application/xml",
"Host": req.URL.Host,
"Accept": "application/xml",
"User-Agent": "httpclient",
"Connection": "keep-alive"}

return req, nil

}

/* --------------------------------------------------------------
Client execution
-------------------------------------------------------------- */

func contattaServer(client Richiedente, id, i int) (elapsed int64, err
os.Error) {

var bodyStr string
var path string

path = fmt.Sprintf("http://%s", indirCompletoSrv) + "/crono/"

start := time.Nanoseconds()
err = client.MakeRequest(path, "GET", bytes.NewBufferString(bodyStr))
if err != nil {
return 0, os.NewError("contattaServer(): Error in MakeRequest(): " +
err.String())
}
elapsed = time.Nanoseconds() - start

return elapsed, err
}


func creaClient(tipoConnessione string) (client Richiedente, err
os.Error) {

if tipoConnessione == "SOCKET" {
client = new(HttpclientSoc)
} else {
client = new(HttpclientHttp)
}

err = client.OpenConnection(indirCompletoSrv)
if err != nil {
err = os.NewError("Errore in connessione " + tipoConnessione + " al
server " + indirCompletoSrv + ": " + err.String())
}

return client, err
}


func sessioneClient(tipoConnessione string, id, maxRichieste int)
(mediaT int64, err os.Error) {

var client Richiedente
var clientSoc Richiedente

var elapsedT int64
var sommaT int64

if tipoConnessione == "SOCKET" {

for i:= 1; i <= maxRichieste; i++ {

clientSoc, err = creaClient(tipoConnessione)
if err != nil {
err = os.NewError(fmt.Sprintf("Conn. %s, thread %d/%d, richiesta
%d/%d. Errore nella creazione del client: %s", tipoConnessione, id,
*maxThread, i, maxRichieste, err.String()))
return 0, err
}

elapsedT, err = contattaServer(clientSoc, id, i)
if err != nil {
err = os.NewError(fmt.Sprintf("Conn. %s, thread %d/%d, richiesta
%d/%d. Errore nella comunicazione col server: %s", tipoConnessione,
id, *maxThread, i, maxRichieste, err.String()))
return 0, err
}

sommaT += elapsedT

clientSoc.GetResponse().Resp.Body.Close()

clientSoc.CloseConnection()
}
} else {

client, err = creaClient(tipoConnessione)
if err != nil {
err = os.NewError(fmt.Sprintf("Conn. %s, thread %d/%d. Errore nella
creazione del client: %s", tipoConnessione, id, *maxThread,
err.String()))
return 0, err
}

for i:= 1; i <= maxRichieste; i++ {

elapsedT, err = contattaServer(client, id, i)
if err != nil {
err = os.NewError(fmt.Sprintf("Conn. %s, thread %d/%d, richiesta
%d/%d. Errore nella comunicazione col server: %s", tipoConnessione,
id, *maxThread, i, maxRichieste, err.String()))
return 0, err
}
sommaT += elapsedT
}
client.CloseConnection()
}
mediaT = sommaT / int64(maxRichieste)
return mediaT, err
}

func validaFlag(tipoConnessione *string, maxThread, maxRichieste *int)
(err os.Error) {

*tipoConnessione = strings.ToUpper(*tipoConnessione)
if *tipoConnessione != "HTTP" && *tipoConnessione != "SOCKET" {
err = os.NewError("Il tipo della connessione deve essere HTTP o
SOCKET")
return err
}

if *maxThread <= 0 {
err = os.NewError("Il N. thread da generare deve essere > 0")
return err
}

if *maxRichieste <= 0 {
err = os.NewError("Il N. richieste x sessione deve essere > 0")
return err
}

return err
}


type tempiRilevati struct {
media int64
err os.Error
}

/*****************************************************************
main()
*****************************************************************/
func main() {

flag.Parse()
err := validaFlag(tipoConnessione, maxThread, maxRichieste)
if err != nil {
log.Exit(err)
}

var mediaTot int64

var done = make(chan tempiRilevati)

var start = make(chan bool)

for i := 1; i <= *maxThread; i++ {

go func(start chan bool, done chan tempiRilevati, i int) {
<- start
media, err := sessioneClient(*tipoConnessione, i, *maxRichieste)
tempi := tempiRilevati{media, err}
done <- tempi
return
}(start, done, i)
}

for i := 1; i <= *maxThread; i++ {
start <- true
}

for i := 1; i <= *maxThread; i++ {
tempi := <-done

if tempi.err != nil {

fmt.Printf("Conn\tN.Thread\tN.Rich.\tT.Medio\n")
fmt.Printf("%s\t%d\t%d\t\tABORT-%s\n", *tipoConnessione,
*maxThread, *maxRichieste, tempi.err)
os.Exit(1)
}

mediaTot += tempi.media
}

mediaTot = mediaTot / int64(*maxThread)

fmt.Printf("Conn\tN.Thread\tN.Rich.\tT.Medio\n")
fmt.Printf("%s\t%d\t%d\t%.9f\n", *tipoConnessione, *maxThread,
*maxRichieste, float(mediaTot)/1e9)


}

Russ Cox

unread,
Sep 29, 2010, 10:27:55 PM9/29/10
to silvia, golang-nuts
Can you tell us:

* what architecture are you using? amd64 or 386
* what operating system are you using?
* how do you run the client program?
* what kind of memory usage do you see?

I ran the server using 6g (amd64) on OS X
and then ran the client repeatedly with -maxRichieste 1000.
After the first three runs the server was using 5160K
according to ps.
After 20 more runs the server was using 5292K.
After 20 more runs the server was using 5316K.
After 20 more runs the server was using 5372K.

That seems more like fragmentation than a memory
leak to me. If it's a leak it's very slow one.
I'm guessing you are seeing different behavior.

Russ

silvia

unread,
Sep 30, 2010, 10:09:04 AM9/30/10
to golang-nuts

> * what architecture are you using? amd64 or 386
386
> * what operating system are you using?
ububtu 9.10
> * how do you run the client program?
> * what kind of memory usage do you see?
Start the server:
# httpservt
Get server profile info:
# gopprof --web http://localhost:6060/debug/pprof/heap
Total MB: 1.5; malg 1.0
Get its initial status with "ps au":
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME
COMMAND
silvia 2235 0.0 0.4 3948 2236 pts/1 Sl+ 15:41 0:00
httpservt

Run the client:
# clirunnert -conn=HTTP -maxThread=10

Get the server status again:
Total MB: 1.0; malg 1.0
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME
COMMAND
silvia 2245 0.0 0.4 3948 2464 pts/1 Sl+ 15:47 0:00
httpservt

Run the client:
# clirunnert -conn=HTTP -maxThread=20

Get the server status again:
Total MB: 1.0; malg 1.0
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME
COMMAND
silvia 2245 0.0 0.5 4972 2952 pts/1 Sl+ 15:47 0:00
httpservt

Run the client:
# clirunnert -conn=HTTP -maxThread=200

Get the server status again:
Total MB: 6.0; malg 1.5
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME
COMMAND
silvia 2455 2.7 1.4 9080 7224 pts/1 Sl+ 15:47 0:00
httpservt

Run the client:
# clirunnert -conn=HTTP -maxThread=1000

Get the server status again:
Total MB: 17.5; in the squares in the graph are written memory
addresses (like 0x000000000804d258) and no function names (no malg)
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME
COMMAND
silvia 2455 1.1 5.4 29640 27704 pts/1 Sl+ 15:47 0:05
httpservt

Run the client 5 times:
# clirunnert -conn=HTTP -maxThread=1000
# clirunnert -conn=HTTP -maxThread=1000
# clirunnert -conn=HTTP -maxThread=1000
# clirunnert -conn=HTTP -maxThread=1000
# clirunnert -conn=HTTP -maxThread=1000

Get the server status again:
Total MB: 17.0; in the squares in the graph are written memory
addresses (like 0x000000000804d258) and no function names (no malg)
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME
COMMAND
silvia 2455 2.9 7.3 39024 37372 pts/1 Sl+ 15:47 0:21
httpservt

I don't know how to interpret these figuers. Moreover, if the client
asks the server for a service of the arith example present in the
comment of the Go code, the total heap increases very rapidly: after #
clirunnert -conn=HTTP -maxThread=10, 20, 200, and 1000 the heap is
510MB.

Silvia

Andrew Gerrand

unread,
Oct 4, 2010, 5:55:46 AM10/4/10
to silvia, golang-nuts
Unless I'm missing something, those numbers don't seem very surprising.

Is that the memory usage after all the requests have been made, and
the server is sitting idle?

Memory is not garbage collected immediately, so it's not unusual for a
server's memory use to grow substantially under heavy load.

Are you keeping old references around? The code you have posted is too
long for me to check through. Try cutting it down to the smallest
possible example of code that leaks. (You'll probably find the problem
in the process.)

Andrew

Silvia

unread,
Oct 4, 2010, 6:01:10 PM10/4/10
to golang-nuts
>Unless I'm missing something, those numbers don't seem very surprising.
>Is that the memory usage after all the requests have been made, and
>the server is sitting idle?
Yes. The numbers are taken when the server has finished to serve all
the requests and is idle. The memory usage does't decrease also after
some idle time.

>Memory is not garbage collected immediately, so it's not unusual for a
>server's memory use to grow substantially under heavy load.
Ok. This could be the explanation. Can you please give me some more
informations about the Go GC (logic, thresholds...)?

>Are you keeping old references around? The code you have posted is too
>long for me to check through. Try cutting it down to the smallest
>possible example of code that leaks. (You'll probably find the problem
>in the process.)
I've shrinked the code, mainly the client code. But the problem is in
the server code which is quite short and above all consists of calling
http.ListenAndServe function.
With this shortened code I have the same figures I had before.

I will be glad if you can confirm your explanation (memory is not
garbage collected immediately but it will be in the future or when
it's necessary) or help me understand what else is going on.

Silvia

The shrinked code:

The server:
===========
/*
Test HTTP server.
*/

package main

import (
"http"
"log"
)
import _ "http/pprof"


// Server configuration
const srvAddr = "localhost:6060"

func stringToByteSlice(s string) []byte {
buf := make([]byte, len(s))
copyString(buf, 0, s)
return buf
}
// Copy from string to byte array at offset doff. Assume there's
room.
func copyString(dst []byte, doff int, str string) {
for soff := 0; soff < len(str); soff++ {
dst[doff] = str[soff]
doff++
}
}
// Server handler function.
func cronoServer(c *http.Conn, req *http.Request) {

c.Write(stringToByteSlice("OK"))
}


func main() {

http.Handle("/crono/", http.HandlerFunc(cronoServer))
log.Stdoutf("HTTP server listening on %s\n", srvAddr)
err := http.ListenAndServe(srvAddr, nil)
if err != nil {
log.Crash("ListenAndServe error: ", err)
}
log.Stdout("HTTP server: END")
}

The client:
===========
/*
Client launcher

Usage:
clirunnert [-maxThread=# of client sessions to execute] (default 5)
*/

package main

import (
"os"
"bytes"
"fmt"
"flag"
"strings"
"io"
"http"
"net")

const srvAddr = "localhost:6060"

var tipoConnessione string = "HTTP"
var maxRichieste int = 4

var maxThread *int = flag.Int("maxThread", 5, "# of client sessions to
execute")

/* --------------------------------------------------------------
Client Definition
-------------------------------------------------------------- */
type NoCloser struct {
io.Reader
}
func (NoCloser) Close() os.Error {return nil}

type HttpclientHttp struct {
Conn *http.ClientConn
Resp http.Response
}
func (cli *HttpclientHttp) OpenConnection(url string) (err os.Error) {
var conn net.Conn
conn, err = net.Dial("tcp", "", url)
if err == nil {
httpConn := http.NewClientConn(conn, nil)
cli.Conn = httpConn
}
return err
}
func (cli *HttpclientHttp) CloseConnection() {
cli.Conn.Close()
}
func (cli *HttpclientHttp) MakeRequest(url string, method string,
body io.Reader) (err os.Error) {

var req http.Request
// Create and send default request
req.Method = strings.ToUpper(method)
req.ProtoMajor = 1
req.ProtoMinor = 1
req.Close = false
req.Body = NoCloser{body}

req.URL, err = http.ParseURL(url)
if err != nil {
return err
}
req.Header = map[string]string {
"Content-Type": "application/xml",
"Host": req.URL.Host,
"Accept": "application/xml",
"User-Agent": "httpclient",
"Connection": "keep-alive"}

cli.Conn.Write(&req)
if err != nil {
cli.Conn.Close()
return err
}
// Get response
cli.Resp = *new(http.Response)
var resp *http.Response
resp, err = cli.Conn.Read()
if err != nil {
cli.Conn.Close()
return err
}
cli.Resp = *resp

return err
}

/* --------------------------------------------------------------
Client execution
-------------------------------------------------------------- */
func sessioneClient(tipoConnessione string, id, maxRichieste int) (err
os.Error) {

var client *HttpclientHttp

var bodyStr string
var path string

client = new(HttpclientHttp)
err = client.OpenConnection(srvAddr)
if err != nil {
return os.NewError(fmt.Sprintf("Conn. %s, thread %d/%d. Error in
OpenConnection: %s", tipoConnessione, id, *maxThread, err.String()))
}

for i:= 1; i <= maxRichieste; i++ {

path = fmt.Sprintf("http://%s", srvAddr) + "/crono/"

err = client.MakeRequest(path, "GET",
bytes.NewBufferString(bodyStr))
if err != nil {
return os.NewError(fmt.Sprintf("Conn. %s, thread %d/%d, richiesta
%d/%d. Error during server communication: %s", tipoConnessione, id,
*maxThread, i, maxRichieste, err.String()))
}
}
client.CloseConnection()
return err
}

func main() {

flag.Parse()

var done = make(chan os.Error)
var start = make(chan bool)

for i := 1; i <= *maxThread; i++ {

go func(start chan bool, done chan os.Error, i int) {
<- start
err := sessioneClient(tipoConnessione, i, maxRichieste)
done <- err
return
}(start, done, i)
}

for i := 1; i <= *maxThread; i++ {
start <- true
}

for i := 1; i <= *maxThread; i++ {
err := <-done

if err != nil {
fmt.Printf("ABORT-%s\n", err)
os.Exit(1)
}
}
fmt.Printf("clirunnert END\n")
}

Andrew Gerrand

unread,
Oct 6, 2010, 8:39:30 PM10/6/10
to Silvia, golang-nuts, Russ Cox
On 5 October 2010 09:01, Silvia <silvia....@gmail.com> wrote:
>>Unless I'm missing something, those numbers don't seem very surprising.
>>Is that the memory usage after all the requests have been made, and
>>the server is sitting idle?
> Yes. The numbers are taken when the server has finished to serve all
> the requests and is idle. The memory usage does't decrease also after
> some idle time.
>
>>Memory is not garbage collected immediately, so it's not unusual for a
>>server's memory use to grow substantially under heavy load.
> Ok. This could be the explanation. Can you please give me some more
> informations about the Go GC (logic, thresholds...)?

I can't give the specifics - it's not documented at the moment. The
code is in pkg/runtime if you want to take a look.

> I will be glad if you can confirm your explanation (memory is not
> garbage collected immediately but it will be in the future or when
> it's necessary) or help me understand what else is going on.
>

> func stringToByteSlice(s string) []byte {
>    buf := make([]byte, len(s))
>    copyString(buf, 0, s)
>    return buf
> }
> // Copy from string to byte array at offset doff.  Assume there's
> room.
> func copyString(dst []byte, doff int, str string) {
>    for soff := 0; soff < len(str); soff++ {
>        dst[doff] = str[soff]
>        doff++
>    }
> }

If this code is being executed on every request, that's a lot of
allocation (every call to make). I don't see a leak here, though.
Could be memory fragmentation? Maybe rsc could provide a better
analysis.

I've got to ask - why do this instead of just performing the
conversion: []bytes(s) ?

Andrew

Nigel Tao

unread,
Oct 7, 2010, 2:09:29 AM10/7/10
to Silvia, golang-nuts
On 5 October 2010 09:01, Silvia <silvia....@gmail.com> wrote:
> func stringToByteSlice(s string) []byte {
>    buf := make([]byte, len(s))
>    copyString(buf, 0, s)
>    return buf
> }
> // Copy from string to byte array at offset doff.  Assume there's
> room.
> func copyString(dst []byte, doff int, str string) {
>    for soff := 0; soff < len(str); soff++ {
>        dst[doff] = str[soff]
>        doff++
>    }
> }

I might be missing something, but can't you replace
stringToByteSlice(s) with just []byte(s)? Does that affect the memory
usage?

Nigel Tao

unread,
Oct 7, 2010, 2:10:15 AM10/7/10
to Silvia, golang-nuts
On 7 October 2010 17:09, Nigel Tao <nigel.t...@gmail.com> wrote:
> I might be missing something, but can't you replace
> stringToByteSlice(s) with just []byte(s)? Does that affect the memory
> usage?

Oops, adg already said that 5 hours ago...

Silvia

unread,
Oct 14, 2010, 6:14:32 PM10/14/10
to golang-nuts
Thank you, now I've used []bytes(s).
I've taken again the memory allocation with the new program version
that uses []bytes() and the memory usage is the same.

Conclusions
I can assume that there is no memory leak, maybe memory fragmentation.

Can somebody tell me if there are some articles about Go garbage
collector?

Thank you all for your help.

Andrew Gerrand

unread,
Oct 14, 2010, 8:50:03 PM10/14/10
to Silvia, golang-nuts
On 15 October 2010 09:14, Silvia <silvia....@gmail.com> wrote:
> Can somebody tell me if there are some articles about Go garbage
> collector?

Unfortunately not. However there are some tangentially related
articles at Russ' blog:
http://www.google.com.au/search?&q=site:swtch.com+garbage+collector

Andrew

Russ Cox

unread,
Oct 14, 2010, 10:56:58 PM10/14/10
to Silvia, golang-nuts
The kind of growth you found does suggest a leak.
I have this email thread on a growing todo list and
would like to take a look at it at some point but
things have been busy. One thing you might try
is using the http/pprof package to get memory
profiles after letting the server memory blow up.
The profiles might point at where the problem is.

Russ

silvia cozzi

unread,
Nov 5, 2010, 7:37:35 PM11/5/10
to golang-nuts
 
Sorry for my late reply.
 
I've let the server respond to thousands of requests and then with gopprof I've got its heap prfile in the attach. AFAIK it seems bufio package NewWriterSize and NewReaderSize functions are involved. How to interpret the image? What else should I do?
 
Silvia
pprof_heap_httpserv1.3.svg

Rob 'Commander' Pike

unread,
Nov 5, 2010, 8:45:10 PM11/5/10
to silvia cozzi, golang-nuts
The obvious first suggestion is that the connections are not being closed so the buffers aren't being freed up. The "malg" number suggests the goroutines aren't going away either. Are your serving goroutines exiting properly, and leaving no references in other data structures?

-rob

Johann Höchtl

unread,
Nov 6, 2010, 2:21:32 PM11/6/10
to golang-nuts


On Nov 6, 1:45 am, "Rob 'Commander' Pike" <r...@google.com> wrote:
> The obvious first suggestion is that the connections are not being closed so the buffers aren't being freed up.  

Another one: As it stands todays, allocated memory is not reclaimed to
the OS, right? So long running processes with dynamic allocation habit
as this somehow contrived example would'nt be a desirble deployment
sceanrio.

> -rob

ron minnich

unread,
Nov 6, 2010, 3:35:49 PM11/6/10
to Johann Höchtl, golang-nuts
On Sat, Nov 6, 2010 at 11:21 AM, Johann Höchtl <johann....@gmail.com> wrote:

>
> Another one: As it stands todays, allocated memory is not reclaimed to
> the OS, right? So long running processes with dynamic allocation habit
> as this somehow contrived example would'nt be a desirble deployment
> sceanrio.

That has not been true for quite some time. The malloc that comes with
gnu libc uses mmap to acquire memory and munmap to free it when done.

ron

Johann Höchtl

unread,
Nov 6, 2010, 4:51:04 PM11/6/10
to golang-nuts


On Nov 6, 8:35 pm, ron minnich <rminn...@gmail.com> wrote:
> On Sat, Nov 6, 2010 at 11:21 AM, Johann Höchtl <johann.hoec...@gmail.com> wrote:
>
> > Another one: As it stands todays, allocated memory is not reclaimed to
> > the OS, right? So long running processes with dynamic allocation habit
> > as this somehow contrived example would'nt be a desirble deployment
> > sceanrio.
>
> That has not been true for quite some time. The malloc that comes with
> gnu libc uses mmap to acquire memory and munmap to free it when done.
>
I thought that this message is still true:
groups.google.com/group/golang-nuts/msg/6b7b0b92a0dcfdc5
Must have missed the news that this shortcoming has been fixed!
> ron

peterGo

unread,
Nov 6, 2010, 5:24:28 PM11/6/10
to golang-nuts
Ron,

On Nov 6, 3:35 pm, ron minnich <rminn...@gmail.com> wrote:
> On Sat, Nov 6, 2010 at 11:21 AM, Johann Höchtl <johann.hoec...@gmail.com> wrote:
> > Another one: As it stands todays, allocated memory is not reclaimed to
> > the OS, right?
>
> That has not been true for quite some time. The malloc that comes with
> gnu libc uses mmap to acquire memory and munmap to free it when done.
>

Go does it's own memory management, acquiring memory by direct mmap
(for Unix-like systems) system calls. Once acquired by Go, memory is
not returned to the OS; it's merely returned to Go available memory
free lists. Read the runtime package memory management code for
details.

"The process might have allocated more
memory and freed it already, but that memory is
sitting in free lists, not given back to the operating
system. " Russ, Sep 27 2010
http://groups.google.com/group/golang-nuts/msg/17c09e412a0f0c60

Peter

On Nov 6, 3:35 pm, ron minnich <rminn...@gmail.com> wrote:

Roger Pau Monné

unread,
Nov 6, 2010, 5:33:30 PM11/6/10
to peterGo, golang-nuts
I'm also using pprof and I found that all the memory used by my
program is by the function mallocgc, does this mean the memory is
freed by the GC?

2010/11/6 peterGo <go.pe...@gmail.com>:

peterGo

unread,
Nov 6, 2010, 6:30:47 PM11/6/10
to golang-nuts
Roger,

> I'm also using pprof and I found that all the memory used by my
> program is by the function mallocgc, does this mean the memory is
> freed by the GC?

Go memory is freed by the Go garbage collector (GC). When it needs to,
Go runs its garbage collector, which, using a conservative algorithm,
frees Go allocated memory that's no longer in use by returning it to
Go memory free lists. You can force the Go GC to run with function
runtime.GC(), which runs a garbage collection. You can see the Go
memory management statistics in the runtime.MemStats variable (type
runtime.MemStatsType). The functions runtime·mallocgc and runtime·gc
are in the runtime package source files malloc.goc, mgc0.c and
malloc.h.

"the Go memory allocator is based on Google's open source tcmalloc
allocator." Russ
Garbage collector problems
http://groups.google.com/group/golang-nuts/msg/289d095238ca16b3

Peter

On Nov 6, 5:33 pm, Roger Pau Monné <roy...@gmail.com> wrote:
> I'm also using pprof and I found that all the memory used by my
> program is by the function mallocgc, does this mean the memory is
> freed by the GC?
>
> 2010/11/6 peterGo <go.peter...@gmail.com>:

ron minnich

unread,
Nov 6, 2010, 6:48:13 PM11/6/10
to Johann Höchtl, golang-nuts
On Sat, Nov 6, 2010 at 1:51 PM, Johann Höchtl <johann....@gmail.com> wrote:

> I thought that this message is still true:
> groups.google.com/group/golang-nuts/msg/6b7b0b92a0dcfdc5
> Must have missed the news that this shortcoming has been fixed!

no, you are right; I just misunderstood your comment.

Thanks

ron

Silvia

unread,
Nov 6, 2010, 7:47:04 PM11/6/10
to golang-nuts
On 6 Nov, 01:45, "Rob 'Commander' Pike" <r...@google.com> wrote:
> The obvious first suggestion is that the connections are not being closed so the buffers aren't being freed up.  The "malg" number suggests the goroutines aren't going away either.  Are your serving goroutines exiting properly, and leaving no references in other data structures?
>

Thanks. In the client program I've added a missing close of the
connection in case of error. But, anyway, running the program again I
have the same results (with bufio NewWriterSize and NewReaderSize with
many MB of memory). Now I believe the client program closes correctly
the connections; the logic used is of a persistent connections (open,
many requests, close). How can I dig it further?

Can you please explain what do you mean with "serving goroutines"? Are
the ones invoked by the server on receiving a pattern (the functions
declared in http.HandleFunc)?

Silvia

Jessta

unread,
Nov 7, 2010, 12:26:50 AM11/7/10
to Johann Höchtl, golang-nuts
On Sun, Nov 7, 2010 at 7:51 AM, Johann Höchtl <johann....@gmail.com> wrote:
> I thought that this message is still true:
> groups.google.com/group/golang-nuts/msg/6b7b0b92a0dcfdc5
> Must have missed the news that this shortcoming has been fixed!

It's not actually a shortcoming, it's really a feature.
If a Go program needs memory and can't allocate it then it's fatal.
In fact this is true of all programs on a modern unix like system.

If a long running program has required an amount of memory in the
past, then it's fair to assume it will require that amount of memory
again sometime in the future.
Returning that memory to the OS is dangerous because there is no
guarantee that it will get that memory back.
Keeping the memory doesn't have much of a downside anyway, if you
don't end up using it then it will be swapped out to disk and will
just cost you a small amount of disk space.

I don't think the jvm or .net vm returns memory to the OS either.

- jessta

--
=====================
http://jessta.id.au

silvia cozzi

unread,
Nov 11, 2010, 5:38:15 PM11/11/10
to golang-nuts
Ok, coming back to the problem, I've checked/corrected the code but I
still have strange figures.
On the client side I've added the code to close the connection (maybe
too many times-or in the wrong places?) in order to reduce the memory
allocated by the server for the io buffers.
On the server side I've written a *do nothing* serving goroutine, co
that malg shouldn't increase.
This is the code:
CLIENT
/*
Client launcher

Usage:
clirunnert [-threadNum=# of client sessions to execute] (default 5)
*/

package main

import (
"os"
"bytes"
"fmt"
"flag"
"strings"
"io"
"http"
"net")

// Server configuration
var srvAddr *string = flag.String("srv", "localhost:6060", "Server
address \"host:port\"")

var reqNum int = 5

var threadNum *int = flag.Int("threadNum", 5, "# of client sessions to execute")
var connType *string = flag.String("conn", "HTTP", "Connection type
(HTTP | SOCKET)")

/* --------------------------------------------------------------
Client Definition
-------------------------------------------------------------- */
type NoCloser struct {
io.Reader
}
func (NoCloser) Close() os.Error {return nil}

type HttpclientHttp struct {
conn *http.ClientConn
resp http.Response


}
func (cli *HttpclientHttp) OpenConnection(url string) (err os.Error) {
var conn net.Conn
conn, err = net.Dial("tcp", "", url)
if err == nil {
httpConn := http.NewClientConn(conn, nil)

cli.conn = httpConn


}
return err
}
func (cli *HttpclientHttp) CloseConnection() {

c, _ := cli.conn.Close()
if c != nil {
// Closes underlying connection, discards unread data
c.Close()


}
}
func (cli *HttpclientHttp) MakeRequest(url string, method string,
body io.Reader) (err os.Error) {

var req http.Request
// Create and send default request
req.Method = strings.ToUpper(method)
req.ProtoMajor = 1
req.ProtoMinor = 1
req.Close = false
req.Body = NoCloser{body}

req.URL, err = http.ParseURL(url)
if err != nil {

cli.CloseConnection()
return os.NewError("MakeRequest()-error in ParseURL:\t" + err.String())
}
cli.conn.Write(&req)
if err != nil {
cli.CloseConnection()
return os.NewError("MakeRequest()-error in Write:\t" + err.String())
}
// Get response
cli.resp = *new(http.Response)
var resp *http.Response
resp, err = cli.conn.Read()
if err != nil {
cli.CloseConnection()
return os.NewError("MakeRequest()-error in Read:\t" + err.String())
}
cli.resp = *resp

return err
}
func (cli *HttpclientHttp) GetResponse() (http.Response) {
return cli.resp


}
/* --------------------------------------------------------------
Client execution
-------------------------------------------------------------- */

func clientSession(connType string, id, reqNum int) (err os.Error) {

var client *HttpclientHttp

var bodyStr string
var path string

client = new(HttpclientHttp)

err = client.OpenConnection(*srvAddr)


if err != nil {
return os.NewError(fmt.Sprintf("Conn. %s, thread %d/%d. Error in

OpenConnection: \t%s", connType, id, *threadNum, err.String()))
}
defer client.CloseConnection()

for i:= 1; i <= reqNum; i++ {

path = fmt.Sprintf("http://%s", *srvAddr) + "/crono/"

err = client.MakeRequest(path, "GET", bytes.NewBufferString(bodyStr))
if err != nil {

client.CloseConnection()
return os.NewError(fmt.Sprintf("Conn. %s, thread %d/%d, request
%d/%d. Error during server communication: %s", connType, id,
*threadNum, i, reqNum, err.String()))
}
}
return err
}

func main() {

flag.Parse()

var done = make(chan os.Error)
var start = make(chan bool)

for i := 1; i <= *threadNum; i++ {

go func(start chan bool, done chan os.Error, i int) {
<- start

err := clientSession(*connType, i, reqNum)


done <- err
return
}(start, done, i)
}

for i := 1; i <= *threadNum; i++ {
start <- true
}

for i := 1; i <= *threadNum; i++ {


err := <-done

if err != nil {
fmt.Printf("ABORT-%s\n", err)
os.Exit(1)
}
}
fmt.Printf("clirunnert END\n")
}

----------------------
SERVER


/*
Test HTTP server.
*/

package main

import (
"http"
"log"
"flag"
)
import _ "http/pprof"


// Server configuration
var srvAddr *string = flag.String("srv", "localhost:6060", "Server
address \"host:port\"")

// Server handler function
func cronoServer(w http.ResponseWriter, req *http.Request) {
}


func main() {
log.Stdout("\n====> httpservt <====\n\n")
flag.Parse()
http.HandleFunc("/crono/", cronoServer)
log.Stdoutf("HTTP server listening on %s\n", *srvAddr)
err := http.ListenAndServe(*srvAddr, nil)


if err != nil {
log.Crash("ListenAndServe error: ", err)
}
log.Stdout("HTTP server: END")
}


Then I've started the server and run 60 times the client with 1000
threads (total 60000) and taken its heap profile "pprof_heap-after 60
x 1000 thread.svg".
Restarted the server and run 10 times the client with 6000 threads
(total 60000) and taken its heap profile "pprof_heap-after 10 x 6000
thread.svg". In this case on my PC I have an error in every run.
As you can see, the buffer allocation and the malg are different. It
seems something isn't released in case of error on the client side.
Can anybody help me to understand and correct the code?
Thanks
Silvia

pprof_heap-after 60 x 1000 thread.svg
pprof_heap-after 10 x 6000 thread.svg
Reply all
Reply to author
Forward
0 new messages