转载于:在Go语言里检测内存泄漏 | /archives/5648
在影响软件系统稳定性的因素里,我们最担心的一个问题是内存泄漏,随着系统的运行,系统消耗的内存越来越多,直到最后整个操作系统越来越慢,甚至还会导致系统崩溃。在Go语言里,我们检测内存泄漏主要依靠的是go里面的pprof包,除此之外,我们还可以使用浏览器来查看系统的实时内存信息(包括CPU、goroutine等的信息)。主要是用net/http/pprof包在进程里建立一个HTTP服务器,对外输出pprof包的内部性能剖析信息。参见这篇文章。
Go语言的pprof包不仅可以诊断内存堆信息(毕竟,内存泄漏都是在堆里发生的),而且可以诊断CPU信息、goroutine信息、堵塞信息(帮助诊断死锁)以及操作系统线程创建信息。它们虽然诊断的内容不一样,但是在使用上大体的流程都是一致的。pprof包实际上使用的是Google自己做的另一个perftools开源产品实现的。perftools是非常强大的,可以用在Linux下的基于C的各个程序里,甚至不用修改代码即可进行CPU与内存的性能剖析。
耳听为虚,眼见为实,我们下面来实操一下,通过一个示例GO程序来展示如何使用pprof来诊断Go程序里的内存泄漏。
我们先来设定一下数据库,建立一个MySQL数据库表,名为users,里面有login_name、nickname、uid、password、forbidden几个字段,其中uid与forbidden为int类型字段,其他均为varchar类型,而password为用户密码md5后的结果,因此长度均为32。我们使用的MySQL数据库引擎为go-sql-driver/mysql。
下面是初始代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
package
main
import
(
"database/sql"
"fmt"
_
"/go-sql-driver/mysql"
"os"
"os/signal"
"time"
)
func
waitForSignal
(
)
os
.
Signal
{
signalChan
:
=
make
(
chan
os
.
Signal
,
1
)
defer
close
(
signalChan
)
signal
.
Notify
(
signalChan
,
os
.
Kill
,
os
.
Interrupt
)
s
:
=
<
-
signalChan
signal
.
Stop
(
signalChan
)
return
s
}
func
connect
(
source
string
)
*
sql
.
DB
{
db
,
err
:
=
sql
.
Open
(
"mysql"
,
source
)
if
err
!=
nil
{
return
nil
}
if
err
:
=
db
.
Ping
(
)
;
err
!=
nil
{
return
nil
}
return
db
}
type
User
struct
{
uid
int
name
string
nick
string
forbidden
int
cid
int
}
func
query
(
db *
sql
.
DB
,
name
string
,
id
int
,
dataChan
chan *
User
)
{
for
{
time
.
Sleep
(
time
.
Millisecond
)
user
:
=
&
User
{
cid
:
id
,
name
:
name
,
}
err
:
=
db
.
QueryRow
(
"SELECT nickname, uid, forbidden FROM users WHERE login_name = ?"
,
name
)
.
Scan
(
&
user
.
nick
,
&
user
.
uid
,
&
user
.
forbidden
)
if
err
!=
nil
{
continue
}
dataChan
<
-
user
}
}
func
main
(
)
{
db
:
=
connect
(
"mytest:mytest@tcp(localhost:3306)/mytest?charset=utf8"
)
if
db
==
nil
{
return
}
userChan
:
=
make
(
chan *
User
,
100
)
for
i
:
=
0
;
i
<
100
;
i
++
{
go
query
(
db
,
"Alex"
,
i
+
1
,
userChan
)
}
allUsers
:
=
make
(
[
]
*
User
,
1
<<
12
)
go
func
(
)
{
for
user
:
=
range
userChan
{
fmt
.
Printf
(
"routine[%d] get user %+v\n"
,
user
.
cid
,
user
)
allUsers
=
append
(
allUsers
,
user
)
}
}
(
)
s
:
=
waitForSignal
(
)
fmt
.
Printf
(
"signal got: %v, all users: %d\n"
,
s
,
len
(
allUsers
)
)
}
|
上面的程序当然有蛮严重的内存泄漏问题,我们下面来看看如何加入代码,让pprof帮我们定位到产生内存泄漏的具体代码段里。
下面是内存泄漏问题诊断的一般流程:
- 我们要加入对pprof包里的方法调用,程序才能将运行时候程序的堆内存分配状态记录到文件(也可以是写到其他地方,例如网络等)中,以便进一步的分析;
- 准备
go tool pprof
的运行环境,直接运行这个命令需要用到perl,在Windows下可以安装ActivePerl。此外如果想不仅看到各个方法/函数的内存消耗排名,还想看到它们之间的调用关系,那就需要安装graphviz或者ghostview才行,因为我对graphviz更喜欢,因此我只安装了graphviz。在Windows上安装了上述软件后,别忘了要先把它们的bin目录加入你的PATH环境变量中,再开一个cmd窗口环境变量才能生效; - 编译生成一个可执行程序,例如叫做your-executable-name,然后运行它,会在你指定的位置生成一个内存剖析文件,我们称其为profile-filename
- 使用
go tool pprof your-executable-name profile-filename
即可进入pprof命令模式分析数据 - 或者使用
go tool pprof your-executable-name --text profile-filename
查看各个函数/方法的内存消耗排名 - 或者使用
go tool pprof your-executable-name --dot profile-filename >
命令生成可以在graphviz里面看的gv文件,在查看各个方法/函数的内存消耗的同时查看它们之间的调用关系 - 或者生成了gv文件之后通过
dot -Tpng >
生成调用关系网与内存消耗图的png图形文件
我们首先加入对pprof包的调用,修改代码如下,注意高亮的部分是增加的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
package
main
import
(
"database/sql"
"fmt"
_
"/go-sql-driver/mysql"
"os"
"os/signal"
"runtime"
"runtime/pprof"
"strings"
"time"
)
var
(
pid
int
progname
string
)
func
init
(
)
{
pid
=
os
.
Getpid
(
)
paths
:
=
strings
.
Split
(
os
.
Args
[
0
]
,
"/"
)
paths
=
strings
.
Split
(
paths
[
len
(
paths
)
-
1
]
,
string
(
os
.
PathSeparator
)
)
progname
=
paths
[
len
(
paths
)
-
1
]
runtime
.
MemProfileRate
=
1
}
func
saveHeapProfile
(
)
{
runtime
.
GC
(
)
f
,
err
:
=
os
.
Create
(
fmt
.
Sprintf
(
"prof/heap_%s_%d_%"
,
progname
,
pid
,
time
.
Now
(
)
.
Format
(
"2006_01_02_03_04_05"
)
)
)
if
err
!=
nil
{
return
}
defer
f
.
Close
(
)
pprof
.
Lookup
(
"heap"
)
.
WriteTo
(
f
,
1
)
}
func
waitForSignal
(
)
os
.
Signal
{
signalChan
:
=
make
(
chan
os
.
Signal
,
1
)
defer
close
(
signalChan
)
signal
.
Notify
(
signalChan
,
os
.
Kill
,
os
.
Interrupt
)
s
:
=
<
-
signalChan
signal
.
Stop
(
signalChan
)
return
s
}
func
connect
(
source
string
)
*
sql
.
DB
{
db
,
err
:
=
sql
.
Open
(
"mysql"
,
source
)
if
err
!=
nil
{
return
nil
}
if
err
:
=
db
.
Ping
(
)
;
err
!=
nil
{
return
nil
}
return
db
}
type
User
struct
{
uid
int
name
string
nick
string
forbidden
int
cid
int
}
func
query
(
db *
sql
.
DB
,
name
string
,
id
int
,
dataChan
chan *
User
)
{
for
{
time
.
Sleep
(
time
.
Millisecond
)
user
:
=
&
User
{
cid
:
id
,
name
:
name
,
}
err
:
=
db
.
QueryRow
(
"SELECT nickname, uid, forbidden FROM users WHERE login_name = ?"
,
name
)
.
Scan
(
&
user
.
nick
,
&
user
.
uid
,
&
user
.
forbidden
)
if
err
!=
nil
{
continue
}
dataChan
<
-
user
}
}
func
main
(
)
{
defer
saveHeapProfile
(
)
db
:
=
connect
(
"mytest:mytest@tcp(localhost:3306)/mytest?charset=utf8"
)
if
db
==
nil
{
return
}
userChan
:
=
make
(
chan *
User
,
100
)
for
i
:
=
0
;
i
<
100
;
i
++
{
go
query
(
db
,
"Alex"
,
i
+
1
,
userChan
)
}
allUsers
:
=
make
(
[
]
*
User
,
1
<<
12
)
go
func
(
)
{
for
user
:
=
range
userChan
{
fmt
.
Printf
(
"routine[%d] get user %+v\n"
,
user
.
cid
,
user
)
allUsers
=
append
(
allUsers
,
user
)
}
}
(
)
s
:
=
waitForSignal
(
)
fmt
.
Printf
(
"signal got: %v, all users: %d\n"
,
s
,
len
(
allUsers
)
)
}
|
稍微值得注意的是第37行的("heap").WriteTo(f, 1)
,有的同学可能想用(f)
来代替,这样保存的信息会少很多的,具体看一下WriteHeapProfile
的说明就知道了。此外,保存堆信息之前先GC了一下,以进行垃圾回收,之后保存下来的堆信息将更精确地告诉我们哪些地方可能会造成内存泄露,无法被垃圾回收的。
通过go build
把代码编译成可执行程序之后,运行之,就可以在当前程序的prof目录下出现一个heap_main_87756_2014_01_11_08_04_25.prof类似文件名的文件,其中main是你可执行程序的名字。
之后执行go tool pprof your-executable-name --text profile-filename
即可得到类似下面的结果(仅截取前几行):
Adjusting heap profiles for 1-in-1 sampling rate
Total: 1.7 MB
0.7 40.4% 40.4% 1.0 56.2% /go-sql-driver/mysql.(*MySQLDriver).Open
0.5 27.7% 68.1% 1.6 93.6%
0.2 11.7% 79.8% 0.2 11.7% newdefer
0.1 6.9% 86.7% 0.1 6.9% database/
0.1 4.6% 91.3% 0.1 4.7% 路001
0.0 1.2% 92.5% 0.0 1.2%
0.0 1.0% 93.5% 0.0 1.0% /go-sql-driver/
0.0 0.9% 94.5% 0.0 0.9%
0.0 0.6% 95.1% 0.0 0.6%
0.0 0.5% 95.6% 0.0 0.5% resizefintab
0.0 0.5% 96.1% 0.0 0.5% /go-sql-driver/mysql.(*mysqlConn).readColumns
0.0 0.5% 96.6% 0.0 0.5% database/sql.(*DB).addDepLocked
这个表格里每一列的意义参见perftool的这个文档。
运行go tool pprof
命令,不带–text参数后将直接进入pprof的命令行模式,可以首先执行top10,就可以得到与上述结果类似的排名,从里面可以看到消耗内存最多的是mysql的Open方法,说明我们调用了Open方法后没有释放资源。
此外我们也可以运行go tool pprof your-executable-name --dot profile-filename >
,这样将得到一个文件,我们在graphviz里面打开这个文件将得到一个更详细的包括调用关系在内的内存消耗图。当然,我们如果只需要一张图,也可以运行dot -Tpng >
将这个gv文件另存为png图,这样就可以像我一样,在下面展示剖析结果了。
除了在给定的时刻打印出内存剖析信息到文件里以外,如果你希望能够随时看到剖析结果,也可以有很简单的方法,那就是把net/http和net/http/pprof这两个包给import进来,其中net/http/pprof包需要以import _ "net/http/pprof"
的方式导入,然后在代码里面加一个自定义端口(如6789)的http服务器,像这样:
1
2
3
|
go
func
(
)
{
http
.
ListenAndServe
(
":6789"
,
nil
)
}
(
)
|
这样,在程序运行起来以后,你就可以通过go tool pprof your-executable-name http://localhost:6789/debug/pprof/heap
获得实时的内存剖析信息了,数据格式与通过文件保存下来的格式一致,之后的处理就都一样了。
2014.1.22 补充
在go tool pprof之后,进入pprof的命令行模式下,可以使用list命令查看对应函数(实际上是匹配函数名的正则表达式)里具体哪一行导致的性能/内存损耗。