轉載於:在Go語言裏檢測內存泄漏 | DLCoder.com http://dlcoder.com/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" _ "github.com/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 > heap.gv
命令生成可以在graphviz裏面看的gv文件,在查看各個方法/函數的內存消耗的同時查看它們之間的調用關係 - 或者生成了gv文件之後通過
dot -Tpng heap.gv > heap.png
生成調用關係網與內存消耗圖的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"
_ "github.com/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_%s.prof", 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行的pprof.Lookup("heap").WriteTo(f, 1)
,有的同學可能想用pprof.WriteHeapProfile(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% github.com/go-sql-driver/mysql.(*MySQLDriver).Open
0.5 27.7% 68.1% 1.6 93.6% main.query
0.2 11.7% 79.8% 0.2 11.7% newdefer
0.1 6.9% 86.7% 0.1 6.9% database/sql.convertAssign
0.1 4.6% 91.3% 0.1 4.7% main.func路001
0.0 1.2% 92.5% 0.0 1.2% net.newFD
0.0 1.0% 93.5% 0.0 1.0% github.com/go-sql-driver/mysql.parseDSNParams
0.0 0.9% 94.5% 0.0 0.9% runtime.malg
0.0 0.6% 95.1% 0.0 0.6% runtime.allocm
0.0 0.5% 95.6% 0.0 0.5% resizefintab
0.0 0.5% 96.1% 0.0 0.5% github.com/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 > heap.gv
,這樣將得到一個heap.gv文件,我們在graphviz裏面打開這個文件將得到一個更詳細的包括調用關係在內的內存消耗圖。當然,我們如果只需要一張圖,也可以運行dot
-Tpng heap.gv > heap.png
將這個gv文件另存爲png圖,這樣就可以像我一樣,在下面展示剖析結果了。
除了在給定的時刻打印出內存剖析信息到文件裏以外,如果你希望能夠隨時看到剖析結果,也可以有很簡單的方法,那就是把net/http和net/http/pprof這兩個包給import進來,其中net/http/pprof包需要以import _ "net/http/pprof"
的方式導入,然後在代碼裏面加一個自定義端口(如6789)的http服務器,像這樣:
|
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命令查看對應函數(實際上是匹配函數名的正則表達式)裏具體哪一行導致的性能/內存損耗。