這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Go程式記憶體流失的分析以及避免
2016-10-18
給系統打壓力,記憶體佔用上去了,停止打壓後,仍然降不下來,就可能是有泄漏。
對於無狀態的服務,串連上有請求過來,記憶體上去了。停了請求,但是記憶體仍然居高不下,等到串連斷開記憶體才降,則session可能存在不合理的引用,造成GC無法回收。
看記憶體佔用的時候不要光盯著top上面的數字,因為Go向系統申請的記憶體不使用後,也不會立刻歸還給系統。也就意味著停止打壓後記憶體佔用不立刻下降屬於正常行為,僅看這一項需要多花點時間觀察一會兒。最好是幾個資料一起關註:一個是程式佔用的系統記憶體,另一個是Go的堆記憶體,最後一個是實際使用到的記憶體。從系統申請的記憶體會在Go的記憶體池管理,整塊的記憶體頁,長時間不被訪問並滿足一定條件後,才歸還給作業系統。又因為有GC,堆記憶體也不能代表記憶體佔用,清理過之後剩下的,才是實際使用的記憶體。調用runtime.ReadMemStats
可以看到Go的記憶體使用量資訊; 或者啟用net/pprof後訪問 http://127.0.0.1:6060/debug/pprof/heap ,也可以看,其中HeapInuse是實際的記憶體使用量量,具有參考意義; 還可以帶上參數debug/pprof/heap?debug=2之類得到更細的資訊,
發現問題後,首先不要懷疑Go語言的GC有問題,一定要相信是自己的代碼寫的有問題。記憶體釋放不掉,不是泄漏的,而是代碼肯定有什麼地方還引用著那塊記憶體,導致GC無法釋放。怎麼查問題呢?Go語言有pprof這個神器。
經過在上一步看HeapInuse,確認過記憶體沒釋放之後,可以用 go tool pprof -inuse_space http://127.0.0.1:6060/debug/pprof/heap
查看是什麼地方佔用了記憶體,這裡可以大致看到是什麼函數占記憶體,根據代碼可以推測出一些資訊。一般來講,很有可能的就是goroutine leak,然后里面引用到的記憶體都釋放不掉。舉一個例子:
ch := make(chan T)go produce(ch) { // 生產者往ch裡寫資料 ch <- T{}}go consume(ch) { // 消費者從ch裡讀出資料 <-ch err := doSomeThing()}
消費者發生err並退出了,不再讀ch,導致生產者阻塞在ch <- T{}
上面,然後生產者的goroutine就泄漏了,裡面引用的記憶體永遠無法釋放。這是一個十分常見的情境,比如說開多個worker,worker處理好資料後寫channel,主線程讀channel,但是主線程處理過程中出錯退出,如果處理不當worker就可能泄漏的。
開啟net/pprof之後,通過 http://127.0.0.1:6060/debug/pprof/goroutine?debug=1 可以看到當前的所有goroutine棧,可以找到有哪些goroutine,當前執行到什麼位置,可以找到在哪裡goroutine泄漏了。
上面說了如何分析和定位Go程式的記憶體流失問題,接下來講一下如果避免寫會泄漏的代碼。
第一條原則是,絕對不能由消費者關channel,因為向關閉的channel寫資料會panic。正確的姿勢是生產者寫完所有資料後,關閉channel,消費者負責消費完channel裡面的全部資料:
func produce(ch chan<- T) { defer close(ch) // 生產者寫完資料關閉channel ch <- T{}}func consume(ch <-chan T) { for _ = range ch { // 消費者用for-range讀完裡面所有資料 }}ch := make(chan T)go produce(ch)consume(ch)
為什麼consume要讀完channel裡面所有資料?因為go produce()
可能有多個,這樣寫的代碼,在讀完ch可以確定所有produce的goroutine都退出了,不會泄漏。
第二條原則是,利用關閉channel來廣播取消動作。向關閉的channel讀資料永遠不會阻塞,這是進階的技巧。假設消費者拿到資料處理後有error發生,整個動作失敗,那麼需要有某種機制通知生產者停止並退出。
func produce(ch chan<- T, cancel chan struct{}) { select { case ch <- T{}: case <- cancel: // 用select同時監聽cancel動作 }}func consume(ch <-chan T, cancel chan struct{}) { v := <-ch err := doSomeThing(v) if err != nil { close(cancel) // 能夠通知所有produce退出 return }}for i:=0; i<10; i++ { go produce()}consume()
WaitGroup之類的可以配合著用,看自己喜歡的風格。基本上能處理好error情境下的資源釋放,問題就不大。哦,第零條原則是,對於並發的代碼心存敬畏之心,哪怕用Go,哪怕有channel這麼好用的東西!
context裡面的cancel比較值得參考和學習 ,其實沒什麼技巧,就是多看代碼多寫代碼,標準庫的代碼是極好的學習材料。