Go 內(nèi)存泄漏排查實(shí)戰(zhàn)
現(xiàn)象
NumGoroutine 指標(biāo)持續(xù)上漲,且低峰期未下降,判斷出現(xiàn)了 Goroutine 泄漏現(xiàn)象。

排查
通過訪問線上服務(wù) pprof 暴露出來的 HTTP 接口,拿到當(dāng)前所有協(xié)程的堆棧信息;curl http://「ip:port」/debug/pprof/goroutine?debug=2

發(fā)現(xiàn)存在大量存活時(shí)間超過上千分鐘的 Goroutine,觀察堆棧疑似是 http 連接未釋放導(dǎo)致,通過對(duì)下圖 net.sockets.tcp.inuse(正在使用的tcp socket數(shù)量)指標(biāo)的觀察進(jìn)行了進(jìn)一步的確認(rèn);

結(jié)論
http
下面以本次 case http 服務(wù)為例,做簡(jiǎn)單介紹:
上游服務(wù)作為客戶端使用了 http1.1 并且將連接設(shè)置為 keepalive; 本服務(wù)作為服務(wù)端未設(shè)置 idletimeout 與 readtimeout;
當(dāng)這兩種情況同時(shí)發(fā)生時(shí),如果上游持有對(duì)本服務(wù)的連接不進(jìn)行釋放,那么服務(wù)端會(huì)一直維持這個(gè)連接的存在,不進(jìn)行回收,進(jìn)而導(dǎo)致協(xié)程泄漏;
client上游客戶端可能為 GO、Java 等,以下為 GO 語言 http 客戶端的空閑連接超時(shí)設(shè)置;
server
解決
建議啟動(dòng) http server 盡量用后者,前者雖然簡(jiǎn)單,但是服務(wù)不夠健壯;
thrift
server

Tips
需要注意的一點(diǎn)是,這個(gè) Goroutine 泄漏問題不止在 http 下會(huì)發(fā)生,在 thrift、grpc 中也是同樣的道理,如果服務(wù)端不對(duì)連接設(shè)置 timeout,某些情況下就會(huì)被上游拖死。
Reference
一起 goroutine 泄漏問題的排查[1]
例2:內(nèi)存居高不下
現(xiàn)象
內(nèi)存使用量(mem.rss)居高不下,且低峰期未下降,懷疑發(fā)生了內(nèi)存泄漏現(xiàn)象;
排查
剛開始懷疑時(shí)內(nèi)存泄漏,但是抓取 pprof heap 圖觀察后,未發(fā)現(xiàn)泄露問題,且內(nèi)存分配符合預(yù)期; 發(fā)現(xiàn)內(nèi)存使用雖然居高不下,但未呈上漲趨勢(shì),因此修改關(guān)鍵字為“go 內(nèi)存占用居高不下”,發(fā)現(xiàn)有相同問題;
結(jié)論
問題來自于 GO 在將內(nèi)存歸還給操作系統(tǒng)時(shí)的內(nèi)存釋放策略,詳情見官方 issues[2],以下做簡(jiǎn)單介紹。
GO 內(nèi)存釋放策略
(此節(jié)內(nèi)容整理自 壓測(cè)后go服務(wù)內(nèi)存暴漲[3])
不同策略的釋放機(jī)制
MADV_DONTNEED:內(nèi)核將會(huì)在合適的時(shí)機(jī)去釋放內(nèi)存,但進(jìn)程的 RSS(常駐內(nèi)存)將會(huì)立即減少。如果再次申請(qǐng)內(nèi)存,內(nèi)核會(huì)重新分配一塊新的空間。 MADV_FREE:只能在 linux 內(nèi)核版本 4.5 以上才能使用,此操作理論上只是打了一個(gè)標(biāo)記位,只有在內(nèi)核感覺到內(nèi)存壓力的時(shí)候才會(huì)將這些打標(biāo)記的內(nèi)存回收掉,分配給其他進(jìn)程使用。這個(gè)策略下進(jìn)程的 RSS 不會(huì)立即減少。
不同策略的實(shí)際差別
理論上 MADV_FREE 效率要高一些,通過在頁表中做標(biāo)記的方式,延遲內(nèi)存的分配和回收,可以提高內(nèi)存管理的效率,畢竟內(nèi)存的回收和分配都是會(huì)消耗系統(tǒng)性能的; 導(dǎo)致的 RSS 指標(biāo)變化 MADV_DONTNEED 會(huì)導(dǎo)致進(jìn)程 RSS 會(huì)有明顯的下降;MADV_FREE 會(huì)導(dǎo)致進(jìn)程 RSS 平穩(wěn)在高峰,不會(huì)得到立即釋放;
不同 GO 版本的釋放策略
在 GO1.12 之前,默認(rèn)均選擇的 MADV_DONTNEED 策略進(jìn)行內(nèi)存回收; 在 GO1.12~GO1.15,官方默認(rèn)選擇 MADV_FREE 策略進(jìn)行內(nèi)存回收; 在 GO1.16 及之后,又改回了 MADV_DONTNEED 策略進(jìn)行回收內(nèi)存。
在 GO1.12~GO1.15 且內(nèi)核版本 4.5 以上,mem.rss 指標(biāo)已經(jīng)無法準(zhǔn)確觀測(cè)服務(wù)內(nèi)存占用;
解決方法
不解決,對(duì)程序性能有利,但是會(huì)降低一些可觀測(cè)性; 以下任一方法可以解決,但會(huì)損失一定性能 把 export GODEBUG=madvdontneed=1 寫進(jìn)服務(wù) control.sh 腳本; 升級(jí) GO 版本至 1.16 及以上;
參考資料
一起 goroutine 泄漏問題的排查: https://zhuanlan.zhihu.com/p/100740270
[2]issues: https://github.com/golang/go/issues/42330
[3]壓測(cè)后go服務(wù)內(nèi)存暴漲: http://soiiy.com/go/17114.html
推薦閱讀
