真實環境下大內存 Go 服務性能優化一例
本文轉載自微信公眾號「董澤潤的技術筆記」,作者董澤潤 。轉載本文請聯系董澤潤的技術筆記公眾號。
本文是在上家的 case, 以前很多人在公開大會上拿該案例做分享,所以覺得有印象的同學勿噴,雖然冷飯,但是原創,而且干貨十足 ^_^
有時大家很不理解的現象,明明 call RPC 時設置了超時時間 timeout, 但是 Grafna 看到 P99 latency 很高,why ???
不要猶豫,要么是 timeout 設置不合理,比如只設置了單次 socket timeout, 并沒有設置 circuit breaker 外層超時。參考 你真的了解 timeout 嘛[1]
還有一種情況就是 GC 在搗亂,我們知道 Go GC 使用三色標記法,在 GC 壓力大時用戶態 goroutine 是要 assit 協助標記對象的,同時 GC STW 時間如果非常高,那么業務看起來 latency 就會得比 timeout 大很多
毛刺
該服務使用 go1.7, 需要加載海量的機器學習詞表,標準的 Go 大內存服務,優化前表現為 latency 非常高
可以看到最大的己經到了 2s
同時查看 GC PauseNS 也非常可怕,基本接近 1s, 服務處理不可用狀態
Pprof
如何開啟 pprof 這里就不寫了,網上有很多,大家可以自行查看
- go tool pprof bin/dupsdc http://127.0.0.1:6060/debug/pprof/profile
可以看到 runtime.greyobject, runtime.mallocgc, runtime.heapBitsForObject, runtime.scanobject, runtime.memmove 就些與 GC 相關的占據了 CPU 消耗的 TOP 6
- go tool pprof -inuse_objects http://127.0.0.1:6060/debug/pprof/heap
再查看下常駐對像個數,發現 1kw 常駐內存對像(現在來看很小了,不多),這些都是詞表加載的小對像
優化對像
詞表主要使用兩種類型,map[int64][]float32 和 map[string]int
讓我們看一下三色標記,本質是遞歸掃描所有的指針類型,遍歷確定有沒有被引用
那么問題來了,什么是指針類型呢???所有顯示 *T 以及內部有 pointer 的對像都是指針類型,比如 map[int64][]float32 因為值是 slice, 內部包含了指針,如果 map 有 1kw 個元素,那么 GC 也要遞歸所描所有 key/value
了解這些,優化方法就來了,把 map[int64][]float32 變成 map[int64][6]float32, 這里 slice 變成 6 個元素的數組,業務可以接受
同時把 map[string]int 里的 key 由 string 類型換成 int 枚舉值
優化效果
上線后優化效果很明顯
可以看到,常駐內存對像由 1kw 降低到 200w
同時 cpu pprof 也能看到,排名第一的是 syscall, GC 相關的己經降低很多
查看 Grafana 外圍 IO latency 降低非常明顯。整體優化效果不錯
例外
這里也有例外,比如 map 內部的實現,如果 key/value 值類型大小超過 128 字節,就會退化成指針
- // Builds a type representing a Bucket structure for
- // the given map type. This type is not visible to users -
- // we include only enough information to generate a correct GC
- // program for it.
- // Make sure this stays in sync with runtime/map.go.
- const (
- BUCKETSIZE = 8
- MAXKEYSIZE = 128
- MAXELEMSIZE = 128
- )
- // bmap makes the map bucket type given the type of the map.
- func bmap(t *types.Type) *types.Type {
- if t.MapType().Bucket != nil {
- return t.MapType().Bucket
- }
- bucket := types.New(TSTRUCT)
- keytype := t.Key()
- elemtype := t.Elem()
- dowidth(keytype)
- dowidth(elemtype)
- if keytype.Width > MAXKEYSIZE {
- keytype = types.NewPtr(keytype)
- }
- if elemtype.Width > MAXELEMSIZE {
- elemtype = types.NewPtr(elemtype)
- }
- field := make([]*types.Field, 0, 5)
- ......
- }
思考
Go 每個版本性能都會提升很多,go1.7 1kw 對像服務壓力非常大,但是我司現在 go1.15 2kw 對像未優化也毫無壓力
Go 在吞吐量方面優化非常顯著。還是那句話,本文只做為 GC 性能分析參考,不要提前優化
另外一方面也說明,Go 三色標記并不適合所有場景,本次分享的大詞表常駐內存就是一個典型:
很明顯的 old objects, 不需要 GC 每次都掃描,這里羨慕 java 的分代 GC