我們使用golang編寫的線上服務,通常會設置一個golang runtime指標監(jiān)控,包括goroutine num、gc num、gc pause 等等。最近的一次上線,發(fā)現(xiàn) gc 相關的指標出現(xiàn)異常,gc num 和 gc pause 大幅升高。由于 golang 的 gc 是 stop the world 來做, gc 過多會搶占程序的正常執(zhí)行時間,甚至影響對外提供的服務,因此暫停了上線,準備先把 gc 的問題修復下。
出問題時的 gc 監(jiān)控如下:
其中藍色曲線表示 gc num,黃色曲線表示 gc pause(單位ms),兩個指標都是30s的累計數(shù)據(jù)。可以看到每30s的pause達到了秒級別。
登錄到線上機器,通過 go tool pprof --alloc_objects http://localhost:xxxx/debug/pprof/heap
命令,查看對象分配的采樣記錄。發(fā)現(xiàn)比較大頭的有reflect.Value
、encoding/json
、fmt.Sprint
。考慮到程序中為了更好地做抽象,使用了反射操作,而 reflect.Value
會將對象拷貝并分配到堆上,程序中的對象都是消息體,有的消息體會超大,因此會分配較多的堆內(nèi)存。對程序做了一版優(yōu)化,去掉這個反射邏輯,改為switch case
?,重新上線,發(fā)現(xiàn) gc 略有下降,但效果還是不夠。
?繼續(xù)做 profile,已經(jīng)沒有了 reflect.Value
,于是只能再從另外兩項入手。
這個程序是一個老程序的重構版,當時為了做diff測試,加了大量的日志,包括debug日志,甚至有些用來做diff的日志是marshal成json的。我們用的日志庫沒有做特殊處理,每條日志都會先調(diào)用 fmt.Sprint
,這個函數(shù)會把對象分配到堆上。針對上述情況,做了大量的日志刪減,gc 略有下降但效果不夠。
繼續(xù)做性能分析,發(fā)現(xiàn)gc大頭還是json相關操作。這個應用程序的主要功能就是處理json格式傳入的消息,因此除非從 json 庫著手改善,否則似乎解決不了問題。BTW,在處理的諸多消息中,有一類消息體占用字節(jié)數(shù)巨大,是其他消息的十倍以上。嘗試取消訂閱這類消息,發(fā)現(xiàn)gc立即好轉,恢復到正常水平。不過,這條路徑走不通。
分析本程序的特點,它基于消息觸發(fā)的模式,每次消息到來就會處理,處理就會有堆對象產(chǎn)生。golang 的 gc 時機是根據(jù)當前與上次的 heap size 的比例來決定,默認情況下是100,即新增一倍就會觸發(fā)。嘗試把這個比例調(diào)大 export GOGC=400
,試圖降低 gc 觸發(fā)頻率,發(fā)現(xiàn)效果還是不錯的,兩個指標均有明顯下降,其他指標無明顯異常,暫時先這樣解決,以后有余力再做程序?qū)用鎯?yōu)化。