Golang內存優化實踐隨筆


最近做了許多有關Go內存優化的工作,總結了一些定位、調優方面的套路和經驗,於是,想通過這篇文章與大家分享討論。

發現問題

性能優化領域有一條總所周知的鐵律,即:不要過早地優化。編寫一個程序,首先應該保證其功能的正確性,以及諸如設計是否合理、需求等是否滿足,過早地優化只會引入不必要的復雜度以及設計不合理等各種問題。

那么何時才能開始優化呢?一句話,問題出現時。諸如程序出現頻繁OOM,CPU使用率異常偏高等情況。如今,在這微服務盛行的時代,公司內部都會擁有一套或簡單或復雜的監控系統,當系統給你發出相關告警時,你就要開始重視起來了。

問題定位

1. 查看內存曲線

首先,當程序發生OOM時,首先應該查看程序的內存使用量曲線,可以通過現有監控系統查看,或者prometheus之類的開源工具。

曲線一般都是呈上升趨勢,比如goroutine泄露的曲線一般是使用量緩慢上升直至OOM,而內存分配不合理往往時在高負載時快速攀升以致OOM。

2. 問題復現

這塊是可選項,但是最好能保證復現。如果能在本地或debug環境復現問題,這將非常有利於我們反復進行測試和驗證。

3. 使用pprof定位

Go官方工具提供了pporf來專門用以性能問題定位,首先得在程序中開啟pprof收集功能,這里假定問題程序已開啟pprof。(對這塊不夠了解的同學,建議通過這兩篇文章(1, 2)學習下pprof工具的基本用法)

接下來,我們復現問題場景,並及時獲取heap和groutine的采樣信息。

這里你可能想問,這樣就夠了嗎?

當然不是,只獲取一份樣本信息是不夠的。內存使用量是不斷變化的(通常是上升),因此我們需要的也是期間heap、gourtine信息的變化信息,而非瞬時值。一般來說,我們需要一份正常情況下的樣本信息,一份或多份內存升高期間的樣本信息。

數據收集完畢后,我們按照如下3個方面來排查定位。

排查goroutine泄露

使用命令go tool pprof --base g1.out g2.out ,比較goroutine信息來判斷是否有goroutine激增的情況。

進入交互界面后,輸入top命令,查看期間goroutine的變化。

同時可執行go tool pprof --base g2.out g3.out來驗證。我之前寫了的一篇實戰文章,記錄了goroutine泄露的排查過程。

排查內存使用量

使用命令go tool pprof --base h1.out h2.out,比較當前堆內存的使用量信息來判斷內存使用量。

進入交互界面后,輸入top命令,查看期間堆內存使用量的變化。

排查內存分配量

當上述排查方向都沒發現問題時,那就要查看期間是否有大量的內存申請了。

我們知道Go預言中的GC有三個觸發點:

  1. 兩分鍾沒有GC,就強制GC
  2. 用戶手動調用runtime.GC(一般只測試用)
  3. 程序當前使用的堆內存達到了runtime中動態計算的GC heap goal

若進程過於頻繁地申請大內存,那么就會把GC heap goal不斷提高,到超過內存限制后,就會OOM。

我們可以使用命令go tool pprof --alloc_space --base h1.out h2.out,通過比較前后內存分配量來找到是否有分配不合理的現象。(按照經驗來說,在熱點函數中申請內存易觸發這種情況)

進入交互界面后,輸入top命令,查看期間堆內存分配量的變化。

一般來說,通過上述3個方面的排查,我們基本就能定位出究竟是哪方面的問題導致內存激增了。我們也可以通過web命令,更為直觀地查看問題函數(方法)的完整調用鏈。

問題優化

定位到問題根因后,接下來就是優化階段了。這個階段需要對Go本身足夠熟悉,還得對問題程序的業務邏輯有所了解。

我梳理了一些常見的優化手段,僅供參考。實際場景還是得實際分析。

goroutine泄露

這種問題還是比較好修復的,需要顯式地保證goroutine能正確退出,而非以一些自以為的假設來保證。例如,通過傳遞context.Context對象來顯式退出

go func(ctx context.Context) {
  for {
    select {
      case <-ctx.Done():
    default:
      }
    ...
  }
}(ctx)

對象復用

在一些熱點代碼處,我們應該避免每次調用都申請新的內存,因為在極端情況下,內存分配速度可能會超過GC的速度,從而導致內存激增。這種情況下,我們可以采取復用對象的方式,例如我們可以使用sync.Pool來復用對象

var pool = sync.Pool{New: func() interface{} { return make([]byte, 4096) }}

func fn() {
	buf := pool.Get().([]byte) // takes from pool or calls New
	// do work
	pool.Put(buf) // returns buf to the pool
}

避免[]byte和string轉換

在Go中,使用string()[]byte()來實現[]byte和string的類型轉換,會額外申請一塊內存來復制。我們可以通過一些技巧來避免復制,例如*(*[]byte)(unsafe.Pointer(&s))來實現string轉[]byte

除此之外,還有很多優化方法,可以多看看dave cheney大神的這篇文章,寫得很全面。

優化驗證

最后一步,我們需要驗證優化的結果,畢竟你至少得說服自己,你的優化是的確有成效的

除了通過復現測試來驗證有效性外的,還可以編寫Benchmark測試用例來比較優化前后的內存分配情況(在Benchmark測試用例中加入一行b.ReportAllocs(),即可得到內存分配量信息)

總結

性能調優是一項必備但是較為困難的技能,不僅需要熟悉語言、操作系統等基本知識,還需要一定的經驗積累。

本文介紹了針對Go程序內存問題的發現、定位、優化以及驗證,希望能對你排查內存問題有所幫助(還有某些情況未能沒考慮到,歡迎評論區參與討論)。

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM