Go Ballast 讓記憶體控制更加絲滑
關於 Go GC 優化的手段你知道的有哪些?比較常見的是通過調整 GC 的步調,以調整 GC 的觸發頻率。
- 設定 GOGC
- 設定 debug.SetGCPercent()
這兩種方式的原理和效果都是一樣的,GOGC 預設值是 100,也就是下次 GC 觸發的 heap 的大小是這次 GC 之後的 heap 的一倍。
我們都知道 GO 的 GC 是標記 - 清除方式,當 GC 會觸發時全量遍歷變數進行標記,當標記結束後執行清除,把標記為白色的物件執行垃圾回收。值得注意的是,這裡的回收僅僅是標記記憶體可以返回給作業系統,並不是立即回收,這就是你看到 Go 應用 RSS 一直居高不下的原因。在整個垃圾回收過程中會暫停整個 Go 程式(STW),Go 垃圾回收的耗時還是主要取決於標記花費的時間的長短,清除過程是非常快的。
設定 GOGC 的弊端
1. GOGC 設定比率的方式不精確
設定 GOGC 基本上我們比較常用的 Go GC 調優的方式,大部分情況下其實我們並不需要調整 GOGC 就可以,一方面是不涉及記憶體密集型的程式本身對記憶體敏感程度太低,另外就是 GOGC 這種設定比率的方式不精確,我們很難精確的控制我們想要的觸發的垃圾回收的閾值。
2. GOGC 設定過小
GOGC 設定的非常小,會頻繁觸發 GC 導致太多無效的 CPU 浪費,反應到程式的表現就會特別明顯。舉個例子,對於 API 介面來說,導致的結果的就是介面週期性的耗時變化。這個時候你抓取 CPU profile 來看,大部分的耗時都集中在 GC 的相關處理上。
如上圖,這是一次 prometheus 的查詢操作,我們看到大部分的 CPU 都消耗在 GC 的操作上。這也是生產環境遇到的,由於 GOGC 設定的過小,導致過多的消耗都耗費在 GC 上。
3. 對某些程式本身佔用記憶體就低,容易觸發 GC
對 API 介面耗時比較敏感的業務,由於這種介面一般情況下記憶體佔用都比較低,因為 API 介面變數的生命週期都比較短,這個時候 GOGC 置預設值的時候,也可能也會遇到介面的週期性的耗時波動。這是為什麼呢?
因為這種介面本身佔用記憶體比較低,每次 GC 之後本身佔的記憶體比較低,如果按照上次 GC 後的 heap 的一倍的 GC 步調來設定 GOGC 的話,這個閾值其實是很容易就能夠觸發,於是就很容出現介面因為 GC 的觸發導致額外的消耗。
4. GOGC 設定很大,有的時候又容易觸發 OOM
那如何調整呢?是不是把 GOGC 設定的越大越好呢?這樣確實能夠降低 GC 的觸發頻率,但是這個值需要設定特別大才有效果,GOGC 一般需要設定 2000 左右。這樣帶來的問題,GOGC 設定的過大,如果這些介面突然接受到一大波流量,由於長時間無法觸發 GC 可能導致 OOM。
由此,GOGC 對於這樣的場景並不是很友好,那有沒有能夠精確控制記憶體,讓其在 10G 的倍數時準確控制 GC 呢?
GO 記憶體 ballast
這就需要 Go ballast 出場了。什麼是 Go ballast,其實很簡單就是初始化一個生命週期貫穿整個 Go 應用生命週期的超大 slice。
func main() {
ballast := make([]byte, 10*1024*1024*1024) // 10G
// do something
runtime.KeepAlive(ballast)
}
上面的程式碼就初始化了一個 ballast,利用 runtime.KeepAlive 來保證 ballast 不會被 GC 給回收掉。
利用這個特性,就能保證 GC 在 10G 的一倍時才能被觸發,這樣就能夠比較精準控制 GO GC 的觸發時機。
這裡你可能有一個疑問,這裡初始化一個 10G 的陣列,不就佔用了 10 G 的實體記憶體呢? 答案其實是不會的。
package main
import (
"runtime"
"math"
"time"
)
func main() {
ballast := make([]byte, 10*1024*1024*1024)
<-time.After(time.Duration(math.MaxInt64))
runtime.KeepAlive(ballast)
}
$ ps -eo pmem,comm,pid,maj_flt,min_flt,rss,vsz --sort -rss | numfmt --header --to=iec --field 5 | numfmt --header --from-unit=1024 --to=iec --field 6 | column -t | egrep "[t]est|[P]I"
%MEM COMMAND PID MAJFL MINFL RSS VSZ
0.1 test 12859 0 1.6K 344M 11530184
這個結果是在 CentOS Linux release 7.9 驗證的,我們看到佔用的 RSS 真實的實體記憶體只有 344M,但是 VSZ 虛擬記憶體確實有 10G 的佔用。
延伸一點,當懷疑我們的介面的耗時是由於 GC 的頻繁觸發引起的,我們需要怎麼確定呢?首先你會想到週期性的抓取 pprof 的來分析,這種方案其實也可以,但是太麻煩了。其實可以根據 GC 的觸發時間繪製這個曲線圖,GC 的觸發時間可以利用 runtime.Memstats 的 LastGC 來獲取。
生產環境驗證
- 綠線 調整前 GOGC = 30
- 黃線 調整後 GOGC 預設值,ballast = 50G
這張圖相同的流量壓力下,ballast 的表現明顯偏好
結論
本篇文章只是簡單的闡述了 Go ballast 的使用,不過 Go ballast 是官方比較認可的方案,具體可以參見 issue 23044。很多開源程式,如 tidb,cortex 都實現了 go ballast,如果你的程式飽受 GOGC 的問題影響或者週期性的耗時不穩定,不妨嘗試下 go ballast。
當然強烈推薦你看下twitch.tv 這篇文章,相信讓你會對 GOGC 以及 ballast 的運用理解的更加透徹。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- 效能優化 | Go Ballast 讓記憶體控制更加絲滑優化GoAST記憶體
- 牛逼!Vue3.5的useTemplateRef讓ref操作DOM更加絲滑Vue
- 學會這些CSS技巧讓你寫樣式更加絲滑CSS
- 為你的App增加WIFI認證檢測,讓使用者體驗更加絲滑APPWiFi
- 『CDN』讓你的網站訪問起來更加柔順絲滑網站
- Go:記憶體管理與記憶體清理Go記憶體
- AI讓你看片更絲滑AI
- 讓你的網頁更絲滑(一)網頁
- 讓你的網頁更絲滑(全)網頁
- GO 記憶體對齊Go記憶體
- Go記憶體逃逸分析Go記憶體
- Webpack DllPlugin 讓構建速度柔順絲滑WebPlugin
- go中的記憶體逃逸Go記憶體
- go記憶體分配器Go記憶體
- Go記憶體管理逃逸分析Go記憶體
- You-Get開源線上下載神器,搭配python更加絲滑(文中案例演示)Python
- 實戰Go記憶體洩露Go記憶體洩露
- 讓Vue專案更絲滑的幾個小技巧Vue
- 讓控制元件如此絲滑Scroller和VelocityTracker的API講解與實戰——Android高階UI控制元件APIAndroidUI
- 例子:酒店列表滑動記憶體增大的問題記憶體
- iOS 如何絲滑的側滑返回iOS
- 圖解Go語言記憶體分配圖解Go記憶體
- Pprof定位Go程式記憶體洩露Go記憶體洩露
- Go記憶體管理一文足矣Go記憶體
- Go記憶體分配和GC的理解Go記憶體GC
- docker的資源控制(CPU、記憶體、IO)Docker記憶體
- GO slice 切片-在記憶體中如何分配Go記憶體
- Linux下絲滑使用dockerLinuxDocker
- Redis記憶體——記憶體消耗(記憶體都去哪了?)Redis記憶體
- 讓你的ubuntu像windows一樣絲滑的小工具們UbuntuWindows
- Java事務註解:讓你的程式碼如絲般順滑Java
- Mac Mouse Fix 2.0.0 Beta 13 (讓你的mac滑鼠絲般順滑)Mac
- 讓API並行呼叫變得如絲般順滑的絕招API並行
- RabbitMQ持久化機制、記憶體磁碟控制(四)MQ持久化記憶體
- Go 記憶體洩漏?不是那麼簡單!Go記憶體
- 記憶體管理 記憶體管理概述記憶體
- 【記憶體管理】記憶體佈局記憶體
- 微信小程式效能優化方案——讓你的小程式如此絲滑微信小程式優化