關於Go的內存泄露有這麼一句話不知道你聽過沒有:node
10次內存泄露,有9次是goroutine泄露。
我所解決的問題,也是goroutine泄露致使的內存泄露,因此這篇文章主要介紹Go程序的goroutine泄露,掌握瞭如何定位和解決goroutine泄露,就掌握了內存泄露的大部分場景。git
本文草稿最初數據都是生產壞境數據,爲了防止敏感內容泄露,所有替換成了demo數據,demo的數據比生產環境數據簡單多了,更適合入門理解,有助於掌握pprof。
go pprof基本知識
定位goroutine泄露會使用到pprof,pprof是Go的性能工具,在開始介紹內存泄露前,先簡單介紹下pprof的基本使用,更詳細的使用給你們推薦了資料。github
什麼是pprof
pprof是Go的性能分析工具,在程序運行過程當中,能夠記錄程序的運行信息,能夠是CPU使用狀況、內存使用狀況、goroutine運行狀況等,當須要性能調優或者定位Bug時候,這些記錄的信息是至關重要。golang
基本使用
使用pprof有多種方式,Go已經現成封裝好了1個:net/http/pprof
,使用簡單的幾行命令,就能夠開啓pprof,記錄運行信息,而且提供了Web服務,可以經過瀏覽器和命令行2種方式獲取運行數據。web
看個最簡單的pprof的例子:編程
文件:golang_step_by_step/pprof/pprof/demo.goubuntu
package main import ( "fmt" "net/http" _ "net/http/pprof" ) func main() { // 開啓pprof,監聽請求 ip := "0.0.0.0:6060" if err := http.ListenAndServe(ip, nil); err != nil { fmt.Printf("start pprof failed on %s\n", ip) } }
提醒:本文全部代碼部分可左右滑動segmentfault
瀏覽器方式
輸入網址ip:port/debug/pprof/
打開pprof主頁,從上到下依次是5類profile信息:瀏覽器
- block:goroutine的阻塞信息,本例就截取自一個goroutine阻塞的demo,但block爲0,沒掌握block的用法
- goroutine:全部goroutine的信息,下面的
full goroutine stack dump
是輸出全部goroutine的調用棧,是goroutine的debug=2,後面會詳細介紹。 - heap:堆內存的信息
- mutex:鎖的信息
- threadcreate:線程信息
這篇文章咱們主要關注goroutine和heap,這兩個都會打印調用棧信息,goroutine裏面還會包含goroutine的數量信息,heap則是內存分配信息,本文用不到的地方就不展現了,最後推薦幾篇文章你們去看。
命令行方式
當鏈接在服務器終端上的時候,是沒有瀏覽器可使用的,Go提供了命令行的方式,可以獲取以上5類信息,這種方式用起來更方便。
使用命令go tool pprof url
能夠獲取指定的profile文件,此命令會發起http請求,而後下載數據到本地,以後進入交互式模式,就像gdb同樣,可使用命令查看運行信息,如下是5類請求的方式:
# 下載cpu profile,默認從當前開始收集30s的cpu使用狀況,須要等待30s go tool pprof http://localhost:6060/debug/pprof/profile # 30-second CPU profile go tool pprof http://localhost:6060/debug/pprof/profile?seconds=120 # wait 120s # 下載heap profile go tool pprof http://localhost:6060/debug/pprof/heap # heap profile # 下載goroutine profile go tool pprof http://localhost:6060/debug/pprof/goroutine # goroutine profile # 下載block profile go tool pprof http://localhost:6060/debug/pprof/block # goroutine blocking profile # 下載mutex profile go tool pprof http://localhost:6060/debug/pprof/mutex
上面的pprof/demo.go
太簡單了,若是去獲取內存profile,幾乎獲取不到什麼,換一個Demo進行內存profile的展現:
文件:golang_step_by_step/pprof/heap/demo2.go
// 展現內存增加和pprof,並非泄露 package main import ( "fmt" "net/http" _ "net/http/pprof" "os" "time" ) // 運行一段時間:fatal error: runtime: out of memory func main() { // 開啓pprof go func() { ip := "0.0.0.0:6060" if err := http.ListenAndServe(ip, nil); err != nil { fmt.Printf("start pprof failed on %s\n", ip) os.Exit(1) } }() tick := time.Tick(time.Second / 100) var buf []byte for range tick { buf = append(buf, make([]byte, 1024*1024)...) } }
上面這個demo會不斷的申請內存,把它編譯運行起來,而後執行:
$ go tool pprof http://localhost:6060/debug/pprof/heap Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap Saved profile in /home/ubuntu/pprof/pprof.demo.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz //<--- 下載到的內存profile文件 File: demo // 程序名稱 Build ID: a9069a125ee9c0df3713b2149ca859e8d4d11d5a Type: inuse_space Time: May 16, 2019 at 8:55pm (CST) Entering interactive mode (type "help" for commands, "o" for options) (pprof) (pprof) (pprof) help // 使用help打印全部可用命令 Commands: callgrind Outputs a graph in callgrind format comments Output all profile comments disasm Output assembly listings annotated with samples dot Outputs a graph in DOT format eog Visualize graph through eog evince Visualize graph through evince gif Outputs a graph image in GIF format gv Visualize graph through gv kcachegrind Visualize report in KCachegrind list Output annotated source for functions matching regexp pdf Outputs a graph in PDF format peek Output callers/callees of functions matching regexp png Outputs a graph image in PNG format proto Outputs the profile in compressed protobuf format ps Outputs a graph in PS format raw Outputs a text representation of the raw profile svg Outputs a graph in SVG format tags Outputs all tags in the profile text Outputs top entries in text form top Outputs top entries in text form topproto Outputs top entries in compressed protobuf format traces Outputs all profile samples in text form tree Outputs a text rendering of call graph web Visualize graph through web browser weblist Display annotated source in a web browser o/options List options and their current values quit/exit/^D Exit pprof ....
以上信息咱們只關注2個地方:
- 下載獲得的文件:
/home/ubuntu/pprof/pprof.demo.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz
,這其中包含了程序名demo
,profile類型alloc
已分配的內存,inuse
表明使用中的內存。 help
能夠獲取幫助,最早會列出支持的命令,想掌握pprof,要多看看,多嘗試。
關於命令,本文只會用到3個,我認爲也是最經常使用的:top
、list
、traces
,分別介紹一下。
top
按指標大小列出前10個函數,好比內存是按內存佔用多少,CPU是按執行時間多少。
(pprof) top
Showing nodes accounting for 814.62MB, 100% of 814.62MB total flat flat% sum% cum cum% 814.62MB 100% 100% 814.62MB 100% main.main 0 0% 100% 814.62MB 100% runtime.main
top會列出5個統計數據:
- flat: 本函數佔用的內存量。
- flat%: 本函數內存佔使用中內存總量的百分比。
- sum%: 前面每一行flat百分比的和,好比第2行雖然的100% 是 100% + 0%。
- cum: 是累計量,加入main函數調用了函數f,函數f佔用的內存量,也會記進來。
- cum%: 是累計量佔總量的百分比。
list
查看某個函數的代碼,以及該函數每行代碼的指標信息,若是函數名不明確,會進行模糊匹配,好比list main
會列出main.main
和runtime.main
。
(pprof) list main.main // 精確列出函數 Total: 814.62MB ROUTINE ======================== main.main in /home/ubuntu/heap/demo2.go 814.62MB 814.62MB (flat, cum) 100% of Total . . 20: }() . . 21: . . 22: tick := time.Tick(time.Second / 100) . . 23: var buf []byte . . 24: for range tick { 814.62MB 814.62MB 25: buf = append(buf, make([]byte, 1024*1024)...) . . 26: } . . 27:} . . 28: (pprof) list main // 匹配全部函數名帶main的函數 Total: 814.62MB ROUTINE ======================== main.main in /home/ubuntu/heap/demo2.go 814.62MB 814.62MB (flat, cum) 100% of Total . . 20: }() . . 21: ..... // 省略幾行 . . 28: ROUTINE ======================== runtime.main in /usr/lib/go-1.10/src/runtime/proc.go 0 814.62MB (flat, cum) 100% of Total . . 193: // A program compiled with -buildmode=c-archive or c-shared ..... // 省略幾行
能夠看到在main.main
中的第25行佔用了814.62MB內存,左右2個數據分別是flat和cum,含義和top中解釋的同樣。
traces
打印全部調用棧,以及調用棧的指標信息。
(pprof) traces
File: demo2
Type: inuse_space
Time: May 16, 2019 at 7:08pm (CST) -----------+------------------------------------------------------- bytes: 813.46MB 813.46MB main.main runtime.main -----------+------------------------------------------------------- bytes: 650.77MB 0 main.main runtime.main ....... // 省略幾十行
每一個- - - - -
隔開的是一個調用棧,能看到runtime.main
調用了main.main
,而且main.main
中佔用了813.46MB內存。
其餘的profile操做和內存是相似的,這裏就不展現了。
這裏只是簡單介紹本文用到的pprof的功能,pprof功能很強大,也常常和benchmark結合起來,但這不是本文的重點,因此就很少介紹了,爲你們推薦幾篇文章,必定要好好研讀、實踐:
- Go官方博客關於pprof的介紹,很詳細,也包含樣例,能夠實操:Profiling Go Programs。
- 跟煎魚也討論過pprof,煎魚的這篇文章也很適合入門: Golang 大殺器之性能剖析 PProf。
什麼是內存泄露
內存泄露指的是程序運行過程當中已再也不使用的內存,沒有被釋放掉,致使這些內存沒法被使用,直到程序結束這些內存才被釋放的問題。
Go雖然有GC來回收再也不使用的堆內存,減輕了開發人員對內存的管理負擔,但這並不意味着Go程序再也不有內存泄露問題。在Go程序中,若是沒有Go語言的編程思惟,也不遵照良好的編程實踐,就可能埋下隱患,形成內存泄露問題。
怎麼發現內存泄露
在Go中發現內存泄露有2種方法,一個是通用的監控工具,另外一個是go pprof:
- 監控工具:固定週期對進程的內存佔用狀況進行採樣,數據可視化後,根據內存佔用走勢(持續上升),很容易發現是否發生內存泄露。
- go pprof:適合沒有監控工具的狀況,使用Go提供的pprof工具判斷是否發生內存泄露。
這2種方式分別介紹一下。
監控工具查看進程內在佔用狀況
若是使用雲平臺部署Go程序,雲平臺都提供了內存查看的工具,能夠查看OS的內存佔用狀況和某個進程的內存佔用狀況,好比阿里雲,咱們在1個雲主機上只部署了1個Go服務,因此OS的內存佔用狀況,基本是也反映了進程內存佔用狀況,OS內存佔用狀況以下,能夠看到隨着時間的推動,內存的佔用率在不斷的提升,這是內存泄露的最明顯現象:
若是沒有云平臺這種內存監控工具,能夠製做一個簡單的內存記錄工具。
一、創建一個腳本prog_mem.sh
,獲取進程佔用的物理內存狀況,腳本內容以下:
#!/bin/bash prog_name="your_programe_name" prog_mem=$(pidstat -r -u -h -C $prog_name |awk 'NR==4{print $12}') time=$(date "+%Y-%m-%d %H:%M:%S") echo $time"\tmemory(Byte)\t"$prog_mem >>~/record/prog_mem.log
二、而後使用crontab
創建定時任務,每分鐘記錄1次。使用crontab -e
編輯crontab配置,在最後增長1行:
*/1 * * * * ~/record/prog_mem.sh
腳本輸出的內容保存在prog_mem.log
,只要大致瀏覽一下就能夠發現內存的增加狀況,判斷是否存在內存泄露。若是須要可視化,能夠直接黏貼prog_mem.log
內容到Excel等表格工具,繪製內存佔用圖。
go pprof發現存在內存問題
有情提醒:若是對pprof不瞭解,能夠先看 go pprof基本知識,這是下一節,看完再倒回來看。
若是你Google或者百度,Go程序內存泄露的文章,它總會告訴你使用pprof heap,可以生成漂亮的調用路徑圖,火焰圖等等,而後你根據調用路徑就能定位內存泄露問題,我最初也是對此深信不疑,嘗試了若干天后,只是發現內存泄露跟某種場景有關,根本找不到內存泄露的根源,若是哪位朋友用heap就能定位內存泄露的線上問題,麻煩介紹下。
後來讀了Dave的《High Performance Go Workshop》,刷新了對heap的認識,內存pprof的簡要內容以下:
Dave講了如下幾點:
- 內存profiling記錄的是堆內存分配的狀況,以及調用棧信息,並非進程完整的內存狀況,猜想這也是在go pprof中稱爲heap而不是memory的緣由。
- 棧內存的分配是在調用棧結束後會被釋放的內存,因此並不在內存profile中。
- 內存profiling是基於抽樣的,默認是每1000次堆內存分配,執行1次profile記錄。
- 由於內存profiling是基於抽樣和它跟蹤的是已分配的內存,而不是使用中的內存,(好比有些內存已經分配,看似使用,但實際以及不使用的內存,好比內存泄露的那部分),因此不能使用內存profiling衡量程序整體的內存使用狀況。
- Dave我的觀點:使用內存profiling不可以發現內存泄露。
基於目前對heap的認知,我有2個觀點:
- heap能幫助咱們發現內存問題,但不必定能發現內存泄露問題,這個見解與Dave是相似的。heap記錄了內存分配的狀況,咱們能經過heap觀察內存的變化,增加與減小,內存主要被哪些代碼佔用了,程序存在內存問題,這隻能說明內存有使用不合理的地方,但並不能說明這是內存泄露。
- heap在幫助定位內存泄露緣由上貢獻的力量微乎其微。如第一條所言,能經過heap找到佔用內存多的位置,但這個位置一般不必定是內存泄露,就算是內存泄露,也只是內存泄露的結果,並非真正致使內存泄露的根源。
接下來,我介紹怎麼用heap發現問題,而後再解釋爲何heap幾乎不能定位內存泄露的根因。
怎麼用heap發現內存問題
使用pprof的heap可以獲取程序運行時的內存信息,在程序平穩運行的狀況下,每一個一段時間使用heap獲取內存的profile,而後使用base
可以對比兩個profile文件的差異,就像diff
命令同樣顯示出增長和減小的變化,使用一個簡單的demo來講明heap和base的使用,依然使用demo2進行展現。
文件:golang_step_by_step/pprof/heap/demo2.go
// 展現內存增加和pprof,並非泄露 package main import ( "fmt" "net/http" _ "net/http/pprof" "os" "time" ) // 運行一段時間:fatal error: runtime: out of memory func main() { // 開啓pprof go func() { ip := "0.0.0.0:6060" if err := http.ListenAndServe(ip, nil); err != nil { fmt.Printf("start pprof failed on %s\n", ip) os.Exit(1) } }() tick := time.Tick(time.Second / 100) var buf []byte for range tick { buf = append(buf, make([]byte, 1024*1024)...) } }
將上面代碼運行起來,執行如下命令獲取profile文件,Ctrl-D退出,1分鐘後再獲取1次。
go tool pprof http://localhost:6060/debug/pprof/heap
我已經獲取到了兩個profile文件:
$ ls pprof.demo2.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz pprof.demo2.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz
使用base
把001文件做爲基準,而後用002和001對比,先執行top
看top
的對比,而後執行list main
列出main
函數的內存對比,結果以下:
$ go tool pprof -base pprof.demo2.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz pprof.demo2.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz File: demo2 Type: inuse_space Time: May 14, 2019 at 2:33pm (CST) Entering interactive mode (type "help" for commands, "o" for options) (pprof) (pprof) (pprof) top Showing nodes accounting for 970.34MB, 32.30% of 3003.99MB total flat flat% sum% cum cum% 970.34MB 32.30% 32.30% 970.34MB 32.30% main.main // 看這 0 0% 32.30% 970.34MB 32.30% runtime.main (pprof) (pprof) (pprof) list main.main Total: 2.93GB ROUTINE ======================== main.main in /home/ubuntu/heap/demo2.go 970.34MB 970.34MB (flat, cum) 32.30% of Total . . 20: }() . . 21: . . 22: tick := time.Tick(time.Second / 100) . . 23: var buf []byte . . 24: for range tick { 970.34MB 970.34MB 25: buf = append(buf, make([]byte, 1024*1024)...) // 看這 . . 26: } . . 27:} . . 28:
top
列出了main.main
和runtime.main
,main.main
就是咱們編寫的main函數,runtime.main
是runtime包中的main函數,也就是全部main函數的入口,這裏很少介紹了,有興趣能夠看以前的調度器文章《Go調度器系列(2)宏觀看調度器》。
top
顯示main.main
第2次內存佔用,比第1次內存佔用多了970.34MB。
list main.main
告訴了咱們增加的內存都在這一行:
buf = append(buf, make([]byte, 1024*1024)...)
001和002 profile的文件不進去看了,你本地測試下計算差值,絕對是剛纔對比出的970.34MB。
heap「不能」定位內存泄露
heap能顯示內存的分配狀況,以及哪行代碼佔用了多少內存,咱們能輕易的找到佔用內存最多的地方,若是這個地方的數值還在不斷怎大,基本能夠認定這裏就是內存泄露的位置。
曾想按圖索驥,從內存泄露的位置,根據調用棧向上查找,總能找到內存泄露的緣由,這種方案看起來是不錯的,但實施起來卻找不到內存泄露的緣由,結果是事半功倍。
緣由在於一個Go程序,其中有大量的goroutine,這其中的調用關係也許有點複雜,也許內存泄露是在某個三方包裏。舉個栗子,好比下面這幅圖,每一個橢圓表明1個goroutine,其中的數字爲編號,箭頭表明調用關係。heap profile顯示g111(最下方標紅節點)這個協程的代碼出現了泄露,任何一個從g101到g111的調用路徑均可能形成了g111的內存泄露,有2類可能:
- 該goroutine只調用了少數幾回,但消耗了大量的內存,說明每一個goroutine調用都消耗了很多內存,內存泄露的緣由基本就在該協程內部。
- 該goroutine的調用次數很是多,雖然每一個協程調用過程當中消耗的內存很少,但該調用路徑上,協程數量巨大,形成消耗大量的內存,而且這些goroutine因爲某種緣由沒法退出,佔用的內存不會釋放,內存泄露的緣由在到g111調用路徑上某段代碼實現有問題,形成建立了大量的g111。
第2種狀況,就是goroutine泄露,這是經過heap沒法發現的,因此heap在定位內存泄露這件事上,發揮的做用不大。
goroutine泄露怎麼致使內存泄露
什麼是goroutine泄露
若是你啓動了1個goroutine,但並無符合預期的退出,直到程序結束,此goroutine才退出,這種狀況就是goroutine泄露。
提早思考:什麼會致使goroutine沒法退出/阻塞?
goroutine泄露怎麼致使內存泄露
每一個goroutine佔用2KB內存,泄露1百萬goroutine至少泄露2KB * 1000000 = 2GB
內存,爲何說至少呢?
goroutine執行過程當中還存在一些變量,若是這些變量指向堆內存中的內存,GC會認爲這些內存仍在使用,不會對其進行回收,這些內存誰都沒法使用,形成了內存泄露。
因此goroutine泄露有2種方式形成內存泄露:
- goroutine自己的棧所佔用的空間形成內存泄露。
- goroutine中的變量所佔用的堆內存致使堆內存泄露,這一部分是能經過heap profile體現出來的。
Dave在文章中也提到了,若是不知道什麼時候中止一個goroutine,這個goroutine就是潛在的內存泄露:
7.1.1 Know when to stop a goroutineIf you don’t know the answer, that’s a potential memory leak as the goroutine will pin its stack’s memory on the heap, as well as any heap allocated variables reachable from the stack.
怎麼肯定是goroutine泄露引起的內存泄露
掌握了前面的pprof命令行的基本用法,很快就能夠確認是不是goroutine泄露致使內存泄露,若是你不記得了,立刻回去看一下go pprof基本知識。
判斷依據:在節點正常運行的狀況下,隔一段時間獲取goroutine的數量,若是後面獲取的那次,某些goroutine比前一次多,若是多獲取幾回,是持續增加的,就極有多是goroutine泄露。
goroutine致使內存泄露的demo:
文件:golang_step_by_step/pprof/goroutine/leak_demo1.go
// goroutine泄露致使內存泄露 package main import ( "fmt" "net/http" _ "net/http/pprof" "os" "time" ) func main() { // 開啓pprof go func() { ip := "0.0.0.0:6060" if err := http.ListenAndServe(ip, nil); err != nil { fmt.Printf("start pprof failed on %s\n", ip) os.Exit(1) } }() outCh := make(chan int) // 死代碼,永不讀取 go func() { if false { <-outCh } select {} }() // 每s起100個goroutine,goroutine會阻塞,不釋放內存 tick := time.Tick(time.Second / 100) i := 0 for range tick { i++ fmt.Println(i) alloc1(outCh) } } func alloc1(outCh chan<- int) { go alloc2(outCh) } func alloc2(outCh chan<- int) { func() { defer fmt.Println("alloc-fm exit") // 分配內存,假用一下 buf := make([]byte, 1024*1024*10) _ = len(buf) fmt.Println("alloc done") outCh <- 0 // 53行 }() }
編譯並運行以上代碼,而後使用go tool pprof
獲取gorourine的profile文件。
go tool pprof http://localhost:6060/debug/pprof/goroutine
已經經過pprof命令獲取了2個goroutine的profile文件:
$ ls /home/ubuntu/pprof/pprof.leak_demo.goroutine.001.pb.gz /home/ubuntu/pprof/pprof.leak_demo.goroutine.002.pb.gz
同heap同樣,咱們可使用base
對比2個goroutine profile文件:
$go tool pprof -base pprof.leak_demo.goroutine.001.pb.gz pprof.leak_demo.goroutine.002.pb.gz File: leak_demo Type: goroutine Time: May 16, 2019 at 2:44pm (CST) Entering interactive mode (type "help" for commands, "o" for options) (pprof) (pprof) top Showing nodes accounting for 20312, 100% of 20312 total flat flat% sum% cum cum% 20312 100% 100% 20312 100% runtime.gopark 0 0% 100% 20312 100% main.alloc2 0 0% 100% 20312 100% main.alloc2.func1 0 0% 100% 20312 100% runtime.chansend 0 0% 100% 20312 100% runtime.chansend1 0 0% 100% 20312 100% runtime.goparkunlock (pprof)
能夠看到運行到runtime.gopark
的goroutine數量增長了20312個。再經過002文件,看一眼執行到gopark
的goroutine數量,即掛起的goroutine數量:
go tool pprof pprof.leak_demo.goroutine.002.pb.gz File: leak_demo Type: goroutine Time: May 16, 2019 at 2:47pm (CST) Entering interactive mode (type "help" for commands, "o" for options) (pprof) top Showing nodes accounting for 24330, 100% of 24331 total Dropped 32 nodes (cum <= 121) flat flat% sum% cum cum% 24330 100% 100% 24330 100% runtime.gopark 0 0% 100% 24326 100% main.alloc2 0 0% 100% 24326 100% main.alloc2.func1 0 0% 100% 24326 100% runtime.chansend 0 0% 100% 24326 100% runtime.chansend1 0 0% 100% 24327 100% runtime.goparkunlock
顯示有24330個goroutine被掛起,這不是goroutine泄露這是啥?已經能肯定八九成goroutine泄露了。
是什麼致使如此多的goroutine被掛起而沒法退出?接下來就看怎麼定位goroutine泄露。
定位goroutine泄露的2種方法
使用pprof有2種方式,一種是web網頁,一種是go tool pprof
命令行交互,這兩種方法查看goroutine都支持,但有輕微不一樣,也有各自的優缺點。
咱們先看Web的方式,再看命令行交互的方式,這兩種都很好使用,結合起來用也不錯。
Web可視化查看
Web方式適合web服務器的端口能訪問的狀況,使用起來方便,有2種方式:
- 查看某條調用路徑上,當前阻塞在此goroutine的數量
- 查看全部goroutine的運行棧(調用路徑),能夠顯示阻塞在此的時間
方式一
url請求中設置debug=1:
http://ip:port/debug/pprof/goroutine?debug=1
效果以下:
看起來密密麻麻的,其實簡單又十分有用,看上圖標出來的部分,手機上圖看起來可能不方便,那就放大圖片,或直接看下面各字段的含義:
goroutine profile: total 32023
:32023是goroutine的總數量,32015 @ 0x42e15a 0x42e20e 0x40534b 0x4050e5 ...
:32015表明當前有32015個goroutine運行這個調用棧,而且停在相同位置,@後面的十六進制,如今用不到這個數據,因此暫不深究了。- 下面是當前goroutine的調用棧,列出了函數和所在文件的行數,這個行數對定位頗有幫助,以下:
32015 @ 0x42e15a 0x42e20e 0x40534b 0x4050e5 0x6d8559 0x6d831b 0x45abe1 # 0x6d8558 main.alloc2.func1+0xf8 /home/ubuntu/heap/leak_demo.go:53 # 0x6d831a main.alloc2+0x2a /home/ubuntu/heap/leak_demo.go:54
根據上面的提示,就能判斷32015個goroutine運行到leak_demo.go
的53行:
func alloc2(outCh chan<- int) { func() { defer fmt.Println("alloc-fm exit") // 分配內存,假用一下 buf := make([]byte, 1024*1024*10) _ = len(buf) fmt.Println("alloc done") outCh <- 0 // 53行 }() }
阻塞的緣由是outCh這個寫操做沒法完成,outCh是無緩衝的通道,而且因爲如下代碼是死代碼,因此goroutine始終沒有從outCh讀數據,形成outCh阻塞,進而形成無數個alloc2的goroutine阻塞,造成內存泄露:
if false { <-outCh }
方式二
url請求中設置debug=2:
http://ip:port/debug/pprof/goroutine?debug=2
![](http://static.javashuo.com/static/loading.gif)
第2種方式和第1種方式是互補的,它能夠看到每一個goroutine的信息:
goroutine 20 [chan send, 2 minutes]
:20是goroutine id,[]
中是當前goroutine的狀態,阻塞在寫channel,而且阻塞了2分鐘,長時間運行的系統,你能看到阻塞時間更長的狀況。- 同時,也能夠看到調用棧,看當前執行停到哪了:
leak_demo.go
的53行,
goroutine 20 [chan send, 2 minutes]: main.alloc2.func1(0xc42015e060) /home/ubuntu/heap/leak_demo.go:53 +0xf9 // 這 main.alloc2(0xc42015e060) /home/ubuntu/heap/leak_demo.go:54 +0x2b created by main.alloc1 /home/ubuntu/heap/leak_demo.go:42 +0x3f
命令行交互式方法
Web的方法是簡單粗暴,無需登陸服務器,瀏覽器打開看看就好了。但就像前面提的,沒有瀏覽器可訪問時,命令行交互式纔是最佳的方式,而且也是手到擒來,感受比Web同樣方便。
命令行交互式只有1種獲取goroutine profile的方法,不像Web網頁分debug=1
和debug=2
2中方式,並將profile文件保存到本地:
// 注意命令沒有`debug=1`,debug=1,加debug有些版本的go不支持 $ go tool pprof http://0.0.0.0:6060/debug/pprof/goroutine Fetching profile over HTTP from http://localhost:6061/debug/pprof/goroutine Saved profile in /home/ubuntu/pprof/pprof.leak_demo.goroutine.001.pb.gz // profile文件保存位置 File: leak_demo Type: goroutine Time: May 16, 2019 at 2:44pm (CST) Entering interactive mode (type "help" for commands, "o" for options) (pprof)
命令行只須要掌握3個命令就行了,上面介紹過了,詳細的倒回去看top, list, traces:
- top:顯示正運行到某個函數goroutine的數量
- traces:顯示全部goroutine的調用棧
- list:列出代碼詳細的信息。
咱們依然使用leak_demo.go
這個demo,
$ go tool pprof -base pprof.leak_demo.goroutine.001.pb.gz pprof.leak_demo.goroutine.002.pb.gz File: leak_demo Type: goroutine Time: May 16, 2019 at 2:44pm (CST) Entering interactive mode (type "help" for commands, "o" for options) (pprof) (pprof) (pprof) top Showing nodes accounting for 20312, 100% of 20312 total flat flat% sum% cum cum% 20312 100% 100% 20312 100% runtime.gopark 0 0% 100% 20312 100% main.alloc2 0 0% 100% 20312 100% main.alloc2.func1 0 0% 100% 20312 100% runtime.chansend 0 0% 100% 20312 100% runtime.chansend1 0 0% 100% 20312 100% runtime.goparkunlock (pprof) (pprof) traces File: leak_demo Type: goroutine Time: May 16, 2019 at 2:44pm (CST) -----------+------------------------------------------------------- 20312 runtime.gopark runtime.goparkunlock runtime.chansend runtime.chansend1 // channel發送 main.alloc2.func1 // alloc2中的匿名函數 main.alloc2 -----------+-------------------------------------------------------
top命令在怎麼肯定是goroutine泄露引起的內存泄露介紹過了,直接看traces命令,traces能列出002中比001中多的那些goroutine的調用棧,這裏只有1個調用棧,有20312個goroutine都執行這個調用路徑,能夠看到alloc2中的匿名函數alloc2.func1
調用了寫channel的操做,而後阻塞掛起了goroutine,使用list列出alloc2.func1
的代碼,顯示有20312個goroutine阻塞在53行:
(pprof) list main.alloc2.func1
Total: 20312 ROUTINE ======================== main.alloc2.func1 in /home/ubuntu/heap/leak_demo.go 0 20312 (flat, cum) 100% of Total . . 48: // 分配內存,假用一下 . . 49: buf := make([]byte, 1024*1024*10) . . 50: _ = len(buf) . . 51: fmt.Println("alloc done") . . 52: . 20312 53: outCh <- 0 // 看這 . . 54: }() . . 55:} . . 56:
友情提醒:使用list命令的前提是程序的源碼在當前機器,否則可無法列出源碼。服務器上,一般沒有源碼,那咱們咋辦呢?剛纔介紹了Web查看的方式,那裏會列出代碼行數,咱們可使用wget
下載網頁:
$ wget http://localhost:6060/debug/pprof/goroutine?debug=1
下載網頁後,使用編輯器打開文件,使用關鍵字main.alloc2.func1
進行搜索,找到與當前相同的調用棧,就能夠看到該goroutine阻塞在哪一行了,不要忘記使用debug=2
還能夠看到阻塞了多久和緣由,Web方式中已經介紹了,此處省略代碼幾十行。
總結
文章略長,但全是乾貨,感謝閱讀到這。然讀到着了,跟定很想掌握pprof,建議實踐一把,如今和你們溫習一把本文的主要內容。
goroutine泄露的本質
goroutine泄露的本質是channel阻塞,沒法繼續向下執行,致使此goroutine關聯的內存都沒法釋放,進一步形成內存泄露。
goroutine泄露的發現和定位
利用好go pprof獲取goroutine profile文件,而後利用3個命令top、traces、list定位內存泄露的緣由。
goroutine泄露的場景
泄露的場景不只限於如下兩類,但因channel相關的泄露是最多的。
-
channel的讀或者寫:
- 無緩衝channel的阻塞一般是寫操做由於沒有讀而阻塞
- 有緩衝的channel由於緩衝區滿了,寫操做阻塞
- 期待從channel讀數據,結果沒有goroutine寫
- select操做,select裏也是channel操做,若是全部case上的操做阻塞,goroutine也沒法繼續執行。
編碼goroutine泄露的建議
爲避免goroutine泄露形成內存泄露,啓動goroutine前要思考清楚:
- goroutine如何退出?
- 是否會有阻塞形成沒法退出?若是有,那麼這個路徑是否會建立大量的goroutine?
示例源碼
本文全部示例源碼,及歷史文章、代碼都存儲在Github,閱讀原文可直接跳轉,Github:https://github.com/Shitaibin/golang_step_by_step/tree/master/pprof 。
實踐 Tips
如下是一些從其它項目借鑑或者本身總結的實踐經驗,它們只是建議,而不是準則,實際項目中應該以性能分析數據來做爲優化的參考,避免過早優化。
- 對頻繁分配的對象,使用 sync.Pool 對象池減小分配時GC壓力
- 自動化的 DeepCopy 是很是耗時的,其中涉及到反射,內存分配,容器(如 map)擴展等,大概比手動拷貝慢一個數量級
- 用 atomic.Load/StoreXXX,atomic.Value, sync.Map 等代替 Mutex。(優先級遞減)
- 使用高效的第三方庫,如用fasthttp替代 net/http
- 在開發環境加上
-race
編譯選項進行競態檢查 - 在開發或線上環境開啓 net/http/pprof,方便實時pprof
- 將全部外部IO(網絡IO,磁盤IO)作成異步
推薦閱讀
這些既是參考資料也是推薦閱讀的文章,不容錯過。
【Go Blog關於pprof詳細介紹和Demo】 https://blog.golang.org/profi...
【Dave關於高性能Go程序的workshop】 https://dave.cheney.net/high-...
【煎魚pprof文章,很適合入門 Golang大殺器之性能剖析PProf】 https://segmentfault.com/a/11...
【SO上goroutine調用棧各字段的介紹】https://stackoverflow.com/a/3...
profiling-and-optimizing-go-web-applications