Go 內存泄漏排查兩例
我是一隻可愛的土撥鼠,專注於分享 Go 職場、招聘和求職,解 Gopher 之憂!歡迎關注我。
歡迎大家加入 Go 招聘交流羣,來這裏找志同道合的小夥伴!跟土撥鼠們一起交流學習。
例 1:Goroutine 泄漏
現象
NumGoroutine 指標持續上漲,且低峯期未下降,判斷出現了 Goroutine 泄漏現象。
排查
-
通過訪問線上服務 pprof 暴露出來的 HTTP 接口,拿到當前所有協程的堆棧信息;curl http://「ip:port」/debug/pprof/goroutine?debug=2
-
發現存在大量存活時間超過上千分鐘的 Goroutine,觀察堆棧疑似是 http 連接未釋放導致,通過對下圖 net.sockets.tcp.inuse(正在使用的 tcp socket 數量)指標的觀察進行了進一步的確認;
結論
http
下面以本次 case http 服務爲例,做簡單介紹:
-
上游服務作爲客戶端使用了 http1.1 並且將連接設置爲 keepalive;
-
本服務作爲服務端未設置 idletimeout 與 readtimeout;
當這兩種情況同時發生時,如果上游持有對本服務的連接不進行釋放,那麼服務端會一直維持這個連接的存在,不進行回收,進而導致協程泄漏;
client 上游客戶端可能爲 GO、Java 等,以下爲 GO 語言 http 客戶端的空閒連接超時設置;
thrift
server
Tips
需要注意的一點是,這個 Goroutine 泄漏問題不止在 http 下會發生,在 thrift、grpc 中也是同樣的道理,如果服務端不對連接設置 timeout,某些情況下就會被上游拖死。
Reference
一起 goroutine 泄漏問題的排查 [1]
例 2:內存居高不下
現象
內存使用量(mem.rss)居高不下,且低峯期未下降,懷疑發生了內存泄漏現象;
排查
-
剛開始懷疑時內存泄漏,但是抓取 pprof heap 圖觀察後,未發現泄露問題,且內存分配符合預期;
-
發現內存使用雖然居高不下,但未呈上漲趨勢,因此修改關鍵字爲 “go 內存佔用居高不下”,發現有相同問題;
結論
問題來自於 GO 在將內存歸還給操作系統時的內存釋放策略,詳情見官方 issues[2],以下做簡單介紹。
GO 內存釋放策略
(此節內容整理自 壓測後 go 服務內存暴漲 [3])
不同策略的釋放機制
-
MADV_DONTNEED:內核將會在合適的時機去釋放內存,但進程的 RSS(常駐內存)將會立即減少。如果再次申請內存,內核會重新分配一塊新的空間。
-
MADV_FREE:只能在 linux 內核版本 4.5 以上才能使用,此操作理論上只是打了一個標記位,只有在內核感覺到內存壓力的時候纔會將這些打標記的內存回收掉,分配給其他進程使用。這個策略下進程的 RSS 不會立即減少。
不同策略的實際差別
-
理論上 MADV_FREE 效率要高一些,通過在頁表中做標記的方式,延遲內存的分配和回收,可以提高內存管理的效率,畢竟內存的回收和分配都是會消耗系統性能的;
-
導致的 RSS 指標變化 MADV_DONTNEED 會導致進程 RSS 會有明顯的下降;MADV_FREE 會導致進程 RSS 平穩在高峯,不會得到立即釋放;
不同 GO 版本的釋放策略
-
在 GO1.12 之前,默認均選擇的 MADV_DONTNEED 策略進行內存回收;
-
在 GO1.12~GO1.15,官方默認選擇 MADV_FREE 策略進行內存回收;
-
在 GO1.16 及之後,又改回了 MADV_DONTNEED 策略進行回收內存。
在 GO1.12~GO1.15 且內核版本 4.5 以上,mem.rss 指標已經無法準確觀測服務內存佔用;
解決方法
-
不解決,對程序性能有利,但是會降低一些可觀測性;
-
以下任一方法可以解決,但會損失一定性能 把 export GODEBUG=madvdontneed=1 寫進服務 control.sh 腳本;
-
升級 GO 版本至 1.16 及以上;
參考資料
[1]
一起 goroutine 泄漏問題的排查: https://zhuanlan.zhihu.com/p/100740270
[2]
issues: https://github.com/golang/go/issues/42330
[3]
壓測後 go 服務內存暴漲: http://soiiy.com/go/17114.html
Go 招聘
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/bGnQnN84opogyeMRwIPSHA