1.背景
工作中,無意中發現vm系統日志裡近期出現過OOM,且系統主動kill了某一程序(稱為程序A)。 重新部署A的二進制檔案,執行top指令觀察到程序A瘋狂吃記憶體,記憶體占比肉眼可見的飙升。 檢視代碼邏輯,大緻為:for循環啟動多個goroutine,每個goroutine各自從prometheus或者es庫裡查詢指定index的内容,然後push到遠端tsdb中,每間隔一段時間進入下一次循環。
2.現象查詢
- 引入pprof工具分析記憶體洩露原因
_ import "net/http/pprof"
//在main函數裡添加監聽内容,端口視占用情況改動
go http.ListenAndServe(":8099", nil)
- 本地重新執行代碼,通路http://127.0.0.1:8099/debug/pprof, 代碼執行不到半分鐘,50w+的groutine數量,而且一直在增加。
記一次golang代碼記憶體洩露分析 - 準備點選進去檢視具體groutine内容時,mac已經卡到“不能自已”,隻能關閉程序,不過為查詢記憶體洩露指明了大緻方向。
3.問題分析
記憶體無法收回的原因基本确認為:groutine沒有及時退出,groutine數量堆積過多。
第一反應是:查詢es或者push到遠端tsdb過程中出現io wait,程式hang住在某一個接口調用過程中。 注釋其他代碼,隻留一個goroutine, 檢視groutine内部每一個接口調用的逾時時間,均正常,并不是因為接口調用導緻groutine無法退出。于是,檢視全局變量中用于sleep的時間值大于查詢es時間間隔,理論上進入下一個循環的時候,目前的groutine應該全部自動退出才對。
從pprof中看出,groutine數量一直在瘋狂增加, 既然接口調用沒有卡住,程式可以正常退出,原因可能是for循環出了問題,便在for循環内部列印日志,檢視goroutine内是否按照設定的時間間隔啟動groutine和列印日志。
結果發現循環的速度遠遠超出預期,檢視sleep内參數,發現少了機關,使用了預設值。
修改後代碼為:
重新驗證,goroutine數量符合預期,記憶體占用也比較小且長時間沒有明顯增加。
4.檢視源碼
sleep函數參數為time.Duration類型, Durarion的源碼注釋裡明确預設機關是納秒。是以設定interval為300時,實際上為300ns,和300s之間差距10的9次方,導緻goroutine數量瘋狂增加,記憶體被大量占用且持續增加。
總結
錯誤比較低級,實作的時候想當然以毫秒為機關。邏輯能力有待提高,需要更客觀冷靜的去對待每一個函數的實作和内置方法的使用。