生產環境Go程序內存泄露,用pprof如何快速定位
內存泄漏可以在整個系統中以多種形式出現,除了在寫代碼上的疏忽,忘了關閉該關閉的資源外,更多的時候導致系統發生內存泄露原因可能是設計上決策不對、或者業務邏輯上的疏忽沒有考慮到一些邊界條件。
比如查數據庫時,有個查詢條件在一定情況下應用不到,導致程序被迫持有一個超大的結果集,這樣持續一段時間,執行相同任務的線程一多,就會造成內存泄露。
Golang 為我們提供了 pprof 工具。掌握之后,可以幫助排查程序的內存泄露問題,當然除了排查內存,它也能排查 CPU 占用過高,線程死鎖的這些問題,不過這篇文章我們會聚焦在怎么用 pprof 排查程序的內存泄露問題。
Go 開發的系統中,怎么 添加 pprof 進行采樣的步驟,在這里我就不再細說了,因為我之前的文章,對 pprof 的安裝和使用做了詳細的說明,文章鏈接我放在這里:
- Golang 程序性能分析(一)pprof 和 火焰圖分析https://mp.weixin.qq.com/s?__biz=MzUzNTY5MzU2MA==&mid=2247486618&idx=1&sn=bb5e76e011ba99ebc2ffb8f9d3c00b89&scene=21#wechat_redirect
- Golang程序性能分析(二)在Echo和Gin框架中使用pprofhttps://mp.weixin.qq.com/s?__biz=MzUzNTY5MzU2MA==&mid=2247486654&idx=1&sn=ea7171f58254dfecebc61cfbec7b64e5&scene=21#wechat_redirect
- Golang程序性能分析(三)用pprof分析gRPC服務的性能https://mp.weixin.qq.com/s?__biz=MzUzNTY5MzU2MA==&mid=2247486669&idx=1&sn=2be1d2cb1b85c8d0f59a314cafb4637f&scene=21#wechat_redirect
當然如果你想嘗試點更智能的,讓程序能自己監控自己,并在出現抖動的時候自己采樣,Dump 出導致內存、CPU的問題調用棧信息,可以看一下下面兩篇文章里我介紹的方法和實用的類庫。
- 學會這幾招讓 Go 程序自己監控自己https://mp.weixin.qq.com/s?__biz=MzUzNTY5MzU2MA==&mid=2247490745&idx=1&sn=6a04327f98a734fd50e509362fc04d48&scene=21#wechat_redirect
- Go 服務進行自動采樣性能分析的方案設計與實現https://mp.weixin.qq.com/s?__biz=MzUzNTY5MzU2MA==&mid=2247491713&idx=1&sn=3735d8f028823eaca6f42b28a9e7d817&scene=21#wechat_redirect
內存泄露該看哪個指標
pprof?工具集,提供了Go程序內部多種性能指標的采樣能力,我們常會用到的性能采樣指標有這些:
- profile:CPU采樣
- heap:堆中活躍對象的內存分配情況的采樣
- goroutine:當前所有goroutine的堆棧信息
- allocs: 會采樣自程序啟動所有對象的內存分配信息(包括已經被GC回收的內存)
- threadcreate:采樣導致創建新系統線程的堆棧信息
上面 heap 和 allocs 是兩個與內存相關的指標, allocs 指標會采樣自程序啟動所有對象的內存分配信息。一般是在想要分析哪些代碼能優化提高效率時,查看的指標。針對查看內存泄露問題的分析,使用則的是 heap 指標里的采樣信息。
Heap
pprof 的 heap 信息,是對堆中活躍對象的內存分配情況的采樣。Go 里邊哪些對象會被分配到堆上?一般概況就是,被多個函數引用的對象、全局變量、超過一定體積(32KB)的對象都會被分配到堆上,當然對于 Go 來說還會有其他的一些情況會讓對象逃逸到堆上。
具體哪些變量會被分配到堆上、以及內存逃逸的事兒,就不多說了,想看詳細情況的,看下面這兩篇文章。
- 圖解Go內存管理器的內存分配策略
- Go內存管理之代碼的逃逸分析
Heap 采樣
要使用 pprof 獲取 heap 指標的采樣信息,一種情況是使用 "net/http/pprof" 包
import (
"net/http/pprof"
)
func main() {
http.HandleFunc("/debug/pprof/heap", pprof.Index)
http.ListenAndServe(":80", nil)
}
然后通過 HTTP 請求的方式獲得
curl -sK -v https://example.com/debug/pprof/profile > heap.out
還有一種主要的方法是使用runtime.pprof 提供的方法,把采樣信息保存到文件。
pprof.Lookup("heap").WriteTo(profile_file, 0)
關于這兩個包的使用方式,以及怎么把信息采樣到文件,上面介紹自動采樣的文章里有詳細的介紹,這里就不再花過多篇幅了。
下面進入文章的正題, 拿到采樣文件后,怎么用 pprof 排查出代碼哪里導致了內存泄露。
用 pprof 找出內存泄露的地方
pprof 在采樣 heap 指標的信息時,使用的是 runtime.MemProfile 函數,該函數默認收集每個 512KB 已分配字節的分配信息。我們可以設置讓 runtime.MemProfile 收集所有對象的信息,不過這會對程序的性能造成影響。
當我們拿到采樣文件后,就可以通過 go tool pprof 將信息加載到一個交互模式的控制臺中。
> go tool pprof heap.out
進入,交互式控制臺后,一般會有如下的提示:
File: heap.out
Type: inuse_space
Time: Feb 1, 2022 at 10:11am (CST)
Entering interactive mode (type "help" for commands, "o" for options)
這里的 Type: inuse_space 指明了文件內采樣信息的類型, Type 可能的值有:
- inuse_space — 已分配但尚未釋放的內存空間
- inuse_objects——已分配但尚未釋放的對象數量
- alloc_space — 分配的內存總量(已釋放的也會統計)
- alloc_objects — 分配的對象總數(無論是否釋放)
接下來,介紹一個 pprof 交互式模式下的命令 top,也可以是 topN,比如 top10。這個跟Linux 系統的 top 命令類似,輸出 Top N 個最占用內存的函數。
(pprof) top10
Showing nodes accounting for 134.55MB, 92.16% of 145.99MB total
Dropped 60 nodes (cum <= 0.73MB)
Showing top 10 nodes out of 117
flat flat% sum% cum cum%
60.53MB 41.46% 41.46% 85.68MB 58.69% github.com/jinzhu/gorm.glob..func2
18.65MB 12.77% 54.24% 18.65MB 12.77% regexp.(*Regexp).Split
16.95MB 11.61% 65.84% 16.95MB 11.61% github.com/jinzhu/gorm.(*Scope).AddToVars
8.67MB 5.94% 71.78% 129.05MB 88.39% example.com/xxservice/dummy.GetLargeData
7.50MB 5.14% 82.63% 7.50MB 5.14% reflect.packEface
6.50MB 4.45% 87.08% 6.50MB 4.45% fmt.Sprintf
4MB 2.74% 89.82% 4MB 2.74% runtime.malg
1.91MB 1.31% 91.13% 1.91MB 1.31% strings.Replace
1.51MB 1.03% 92.16% 1.51MB 1.03% bytes.makeSlice
在這兩個里邊,最占用內存的前三是 gorm 庫的一個方法,gorm 是個 ORM 庫,但是導致它內存泄露的原因應該是后面一個有業務邏輯的代碼,dummy.GetLargeData 方法。
在 top 指令輸出的列表中,我們可以看到兩個值,flat 和 cum。
- flat:表示此函數分配、并由該函數持有的內存空間。
- cum:表示由這個函數或它調用堆棧下面的函數分配的內存總量。
此外 sum % 表示前面幾行輸出的 flat百分比之和, 比如上面第四行 sum% 列的值是, 71.78% 實際上就是它以及它上面三行輸出的 flat% 的總和。
定位到導致內存泄露的函數后,后面要做的優化問題就是,深入函數內部,看哪里使用不當或者有邏輯上的疏忽,比如我開頭舉得那個查詢條件在有些情況下應用不上的例子。
當然如果你想在函數內部再精確的定位到底是哪段代碼導致的內存溢出,也是有辦法的,這時候就需要用到 list 指令了。
list 指令可以列出函數內部,每一行代碼運行時分配的內存(如果分析CPU的采樣文件,則會顯示CPU使用時間)
(pprof) list dummy.GetLargeData
Total: 814.62MB
ROUTINE ======================== dummy.GetLargeData in /home/xxx/xxx/xxx.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 怎么排查程序的內存泄露做了個簡單的總結,當然如果你們公司有條件上持續采樣,或者我之前文章說的自動采樣方案的話,最好還是用上,讓機器幫我們做這些事情。?
不過不管是用什么辦法,最終只能是幫我們定位出來哪里造成了內存泄露,至于要怎么優化解決這個問題,還得具體情況具體分析,如果是一些業務邏輯實現上的問題,那就得跟團隊商量一下實現方式,可能還會涉及到產品上的一些改動。