最近在項目中出現golang內存溢出的問題,master剛開始運行時只有10多M,運行幾天后,竟然達到了10多個G。而且到凌晨流量變少內存也沒有明顯降低,內存狀態呈現一種很不健康的曲線。


像這種情況肯定是golang內存溢出了,為此我持續排查了兩天,終于找到問題所在,特此記錄下。
準備工作
- 一臺較好的環境測試機,單臺運行無污染。
- 壓測工具,無論服務是http還是websocket服務,都必須準備好壓測工具模擬最真實的用戶場景。
- 將master引入net/http/pprof包,通過http訪問獲得goroutine、heap信息。
//引入pprof
import _"net/http/pprof"
//在main中加入
go func() {
log.Println(http.ListenAndServe("localhost:9999", nil))
}()
瀏覽器訪問: http://127.0.0.1:9999/debug/pprof/

獲取goroutine信息 http://10.13.132.91:9999/debug/pprof/goroutine?debug=2
獲取heap信息 http://10.13.132.91:9999/debug/pprof/heap?debug=2
使用golang tool進行統計分析,go tool pprof -inuse_space http://127.0.0.1:9999/debug/pprof/heap
。輸入top10可以看出前十占用內存情況,這里我是直接輸入png導出圖片來查看,以便以后比較。還有兩個參數可以選擇,-inuse_space顧名思義是正在使用的內存,-alloc_space是已經分配的內存,本次我是一直用-inuse_space進行分析。
開始進行分析
go是一門自己gc的語言,大概兩分鐘會gc一次。如果有內存泄漏,無非就是兩種情況。
- 有goroutine泄漏,goroutine“飛”了,zombie goroutine沒有結束,這個時候在這個goroutine上分配的內存對象將一直被這個僵尸goroutine引用著,進而導致gc無法回收這類對象,內存泄漏。
- 有一些全局(或者生命周期和程序本身運行周期一樣長的)的數據結構意外的掛住了本該釋放的對象,雖然goroutine已經退出了,但是這些對象并沒有從這類數據結構中刪除,導致對象一直被引用,無法被回收。
排除掉goroutine泄漏
首先,我利用壓測工具對server進行100個websocket連接,模擬用戶瀏覽行為,然后關閉連接。打開瀏覽器查看goroutine數量,發現新起的goroutine全部已經銷毀,沒有觀察到有泄漏的goroutine,因此排除此情況。
確定是全局變量無回收
排除goroutine泄漏,只能是由全局狀態變量引起的。再次用壓測工具進行壓測然后關閉,使用觀察內存情況。使用go tool pprof -inuse_space http://127.0.0.1:9999/debug/pprof/heap
輸入png
導出(在這種情況下,需要等程序gc完再導出,建議等10分鐘左右。)

發現問題所在
每次都會遺留這么大概0.5M的內存空間出來,就奇怪,明明整個goroutine退出為什么還有會內存占用?相應的全局變量也會刪除該地方的引用。等一下,全局變量,難道是刪除的時候沒做好配對導致沒有真正刪除該引用嗎?去查了下代碼,果然是沒有刪除引用導致的,至此問題解決。

這里面有個項目的坑,上報日志的key不是根據這個len(map)
計算出,導致上報日志的時候以為刪除了該key。
后記
為什么會花了兩天時間,看起來上述流程并不復雜。
實際上你要完全排除掉goroutine泄漏需要花較長的時間去對比的,查看哪些goroutine是新起來沒有關閉。
在使用-inuse_space或者-alloc_space分析,也是很糾結,這些看起來也并不完全與表現對應上。實際上用-inuse_space是較為直觀的,可以展現出程序真正在使用的(RSS)。Go 管理內存的方式可能與你以前使用的方式不太一樣。它會在一開始就保留一大塊 VIRT,而 RSS 與實際內存用量接近。RSS 和 VIRT 之間有什么區別呢?VIRT 或者虛擬地址空間大小是程序映射并可以訪問的內存數量。RSS 或者常駐大小是實際使用的內存數量。因此用-inuse_space導出在png圖上的統計中,與top上的res值是大致相同。

還有就是每次做壓測或者等待golang 完全gc都要耗費不少時間,這樣也會排查增加難度。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
您可能感興趣的文章:- Go pprof內存指標含義備忘錄及案例分析
- golang切片內存應用技巧詳解
- Go語言中的內存布局詳解
- go語言中切片與內存復制 memcpy 的實現操作