最近在項(xiàng)目中出現(xiàn)golang內(nèi)存溢出的問(wèn)題,master剛開(kāi)始運(yùn)行時(shí)只有10多M,運(yùn)行幾天后,竟然達(dá)到了10多個(gè)G。而且到凌晨流量變少內(nèi)存也沒(méi)有明顯降低,內(nèi)存狀態(tài)呈現(xiàn)一種很不健康的曲線。
像這種情況肯定是golang內(nèi)存溢出了,為此我持續(xù)排查了兩天,終于找到問(wèn)題所在,特此記錄下。
準(zhǔn)備工作
- 一臺(tái)較好的環(huán)境測(cè)試機(jī),單臺(tái)運(yùn)行無(wú)污染。
- 壓測(cè)工具,無(wú)論服務(wù)是http還是websocket服務(wù),都必須準(zhǔn)備好壓測(cè)工具模擬最真實(shí)的用戶場(chǎng)景。
- 將master引入net/http/pprof包,通過(guò)http訪問(wèn)獲得goroutine、heap信息。
//引入pprof
import _"net/http/pprof"
//在main中加入
go func() {
log.Println(http.ListenAndServe("localhost:9999", nil))
}()
瀏覽器訪問(wèn): 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進(jìn)行統(tǒng)計(jì)分析,go tool pprof -inuse_space http://127.0.0.1:9999/debug/pprof/heap
。輸入top10可以看出前十占用內(nèi)存情況,這里我是直接輸入png導(dǎo)出圖片來(lái)查看,以便以后比較。還有兩個(gè)參數(shù)可以選擇,-inuse_space顧名思義是正在使用的內(nèi)存,-alloc_space是已經(jīng)分配的內(nèi)存,本次我是一直用-inuse_space進(jìn)行分析。
開(kāi)始進(jìn)行分析
go是一門自己gc的語(yǔ)言,大概兩分鐘會(huì)gc一次。如果有內(nèi)存泄漏,無(wú)非就是兩種情況。
- 有g(shù)oroutine泄漏,goroutine“飛”了,zombie goroutine沒(méi)有結(jié)束,這個(gè)時(shí)候在這個(gè)goroutine上分配的內(nèi)存對(duì)象將一直被這個(gè)僵尸goroutine引用著,進(jìn)而導(dǎo)致gc無(wú)法回收這類對(duì)象,內(nèi)存泄漏。
- 有一些全局(或者生命周期和程序本身運(yùn)行周期一樣長(zhǎng)的)的數(shù)據(jù)結(jié)構(gòu)意外的掛住了本該釋放的對(duì)象,雖然goroutine已經(jīng)退出了,但是這些對(duì)象并沒(méi)有從這類數(shù)據(jù)結(jié)構(gòu)中刪除,導(dǎo)致對(duì)象一直被引用,無(wú)法被回收。
排除掉goroutine泄漏
首先,我利用壓測(cè)工具對(duì)server進(jìn)行100個(gè)websocket連接,模擬用戶瀏覽行為,然后關(guān)閉連接。打開(kāi)瀏覽器查看goroutine數(shù)量,發(fā)現(xiàn)新起的goroutine全部已經(jīng)銷毀,沒(méi)有觀察到有泄漏的goroutine,因此排除此情況。
確定是全局變量無(wú)回收
排除goroutine泄漏,只能是由全局狀態(tài)變量引起的。再次用壓測(cè)工具進(jìn)行壓測(cè)然后關(guān)閉,使用觀察內(nèi)存情況。使用go tool pprof -inuse_space http://127.0.0.1:9999/debug/pprof/heap
輸入png
導(dǎo)出(在這種情況下,需要等程序gc完再導(dǎo)出,建議等10分鐘左右。)
發(fā)現(xiàn)問(wèn)題所在
每次都會(huì)遺留這么大概0.5M的內(nèi)存空間出來(lái),就奇怪,明明整個(gè)goroutine退出為什么還有會(huì)內(nèi)存占用?相應(yīng)的全局變量也會(huì)刪除該地方的引用。等一下,全局變量,難道是刪除的時(shí)候沒(méi)做好配對(duì)導(dǎo)致沒(méi)有真正刪除該引用嗎?去查了下代碼,果然是沒(méi)有刪除引用導(dǎo)致的,至此問(wèn)題解決。
這里面有個(gè)項(xiàng)目的坑,上報(bào)日志的key不是根據(jù)這個(gè)len(map)
計(jì)算出,導(dǎo)致上報(bào)日志的時(shí)候以為刪除了該key。
后記
為什么會(huì)花了兩天時(shí)間,看起來(lái)上述流程并不復(fù)雜。
實(shí)際上你要完全排除掉goroutine泄漏需要花較長(zhǎng)的時(shí)間去對(duì)比的,查看哪些goroutine是新起來(lái)沒(méi)有關(guān)閉。
在使用-inuse_space或者-alloc_space分析,也是很糾結(jié),這些看起來(lái)也并不完全與表現(xiàn)對(duì)應(yīng)上。實(shí)際上用-inuse_space是較為直觀的,可以展現(xiàn)出程序真正在使用的(RSS)。Go 管理內(nèi)存的方式可能與你以前使用的方式不太一樣。它會(huì)在一開(kāi)始就保留一大塊 VIRT,而 RSS 與實(shí)際內(nèi)存用量接近。RSS 和 VIRT 之間有什么區(qū)別呢?VIRT 或者虛擬地址空間大小是程序映射并可以訪問(wèn)的內(nèi)存數(shù)量。RSS 或者常駐大小是實(shí)際使用的內(nèi)存數(shù)量。因此用-inuse_space導(dǎo)出在png圖上的統(tǒng)計(jì)中,與top上的res值是大致相同。
還有就是每次做壓測(cè)或者等待golang 完全gc都要耗費(fèi)不少時(shí)間,這樣也會(huì)排查增加難度。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
您可能感興趣的文章:- Go pprof內(nèi)存指標(biāo)含義備忘錄及案例分析
- golang切片內(nèi)存應(yīng)用技巧詳解
- Go語(yǔ)言中的內(nèi)存布局詳解
- go語(yǔ)言中切片與內(nèi)存復(fù)制 memcpy 的實(shí)現(xiàn)操作