怎樣優(yōu)化一個Go服務(wù)以減少40%的CPU使用率?
通過優(yōu)化一個 Go 服務(wù),Coralogix 公司的工程師成功減少了 40% 的 CPU 使用率。
10 年前,谷歌遇到一個由 C++ 編譯時(shí)間過長造成的嚴(yán)重瓶頸,他們需要全新的解決方案。為應(yīng)對這一挑戰(zhàn),谷歌工程師創(chuàng)建了一種叫 Go(又名 Golang)的新編程語言。Go 語言借鑒了 C++ 的優(yōu)點(diǎn)(比如其性能和安全特性),同時(shí)結(jié)合了 Python 的開發(fā)速度,這讓其能快速使用多個核心,實(shí)現(xiàn)并發(fā)計(jì)算。
在 Coralogix,我們解析客戶日志來為它們提供相應(yīng)的實(shí)時(shí)分析、警報(bào)和元數(shù)據(jù)等。為做到這一點(diǎn),解析階段必須非常快,但解析階段卻又非常復(fù)雜,并需要為每一個日志行服務(wù)加載大量規(guī)則。這是我們覺得采用 Golang 的原因之一。
這項(xiàng)新服務(wù)全天候運(yùn)行在我們的生產(chǎn)環(huán)境中。雖然結(jié)果很不錯,但是它仍然需要運(yùn)行在高性能的機(jī)器上。這個 Go 服務(wù)運(yùn)行在一臺 8 核 CPU 和 36GB 內(nèi)存的 AWS m4.2xlarge 實(shí)例上,每天解析超過數(shù)百億的日志。這個階段,一切運(yùn)行正常,可以將就,但這不是我們的風(fēng)格,我們希望用更少的 AWS 實(shí)例來提供更多的功能,比如性能等。為實(shí)現(xiàn)這一目標(biāo),我們需要了解瓶頸的本質(zhì),以及如何能減少或完全消除它們。
1. 問題分析
我們決定在服務(wù)上運(yùn)行一些性能分析,并檢查是什么導(dǎo)致 CPU 的高消耗,看看是否可以做些優(yōu)化工作。
升級版本
首先,我們將 GO 升級到最新的穩(wěn)定版本(軟件生命周期中的關(guān)鍵一步)。此前,我們的版本是 1.12.4,現(xiàn)在用的是 1.13.8。
根據(jù)官方文檔,1.13 版本在 runtime library 和一些對內(nèi)存使用有很大影響的組件進(jìn)行了重大改進(jìn)。不管怎么說,使用最新的穩(wěn)定版本很有意義,并且為我們節(jié)省了很多工作。
https://golang.org/doc/devel/release.html
因此,內(nèi)存消耗也從 800MB 左右優(yōu)化到 180MB 左右。
分析開始
其次,為更好地理解我們的工作流程,并了解我們在哪里花費(fèi)時(shí)間和資源,我們開始進(jìn)行分析。
分析不同的服務(wù)和編程語言可能看起來非常復(fù)雜并令人生畏,但是在 Go 中,它實(shí)際上非常簡單,僅用幾個命令就可以實(shí)現(xiàn)。Go 有一個叫”pprof”的專門工具,可以通過監(jiān)聽路由(默認(rèn)端口為 6060)在應(yīng)用程序中啟用該工具,并使用 Go 包來管理 HTTP 連接
- import _ "net/http/pprof"
然后,在主函數(shù)或者路由包中啟用如下操作:
- go func() {
- log.Println(http.ListenAndServe("localhost:6060",nil))
- }()
現(xiàn)在我們可以啟動服務(wù)并連接到
- Http://localhost:6060/debug/pprof
完整的 Go 文檔可以參閱此處。
https://golang.org/pkg/net/http/pprof
pprof 的默認(rèn)配置是每隔 30 秒對 CPU 使用率進(jìn)行采樣。我們可以調(diào)整一些配置從而實(shí)現(xiàn)對 CPU 使用率、heap usage 等參數(shù)的采樣。我們主要關(guān)注的是 CPU 的使用情況,因此在生產(chǎn)階段中,我們采取一個 30 秒間隔的性能采樣,并發(fā)現(xiàn)下圖中的顯示內(nèi)容(注意:這是在我們升級了 Go 版本并將 Go 的內(nèi)部組件降到最低之后的結(jié)果):

Go profiling
如你所見,我們發(fā)現(xiàn)很多與運(yùn)行時(shí)庫(runtime package )相關(guān)的活動,其中需要特別指出是 GC(垃圾收集):幾乎 29% 的 CPU 被 GC 使用,這還只是消耗最多的前 20 個對象。由于 Go 的 GC 已經(jīng)非常快并做了很大優(yōu)化,最好的實(shí)踐就是不要去改變或修改它。由于我們的內(nèi)存消耗非常低(與前一個 Go 版本相比),所以主要問題變成了高對象分配率。
如果是這樣的話,我們可以做兩件事情:
- 調(diào)整 Go GC 活動來適應(yīng)我們的服務(wù)行為,也就是說,我們需要延遲 GC 的觸發(fā)來減少其運(yùn)行頻率。作為代價(jià),我們將不得不消耗更多的內(nèi)存。
- 找出代碼中分配了太多對象的函數(shù)、區(qū)域或行。
觀察一下實(shí)例類型,我們有大量的空閑內(nèi)存,而 CPU 數(shù)量則被機(jī)器類型所限制。因此我們需要調(diào)整這個比率。從 Golang 早期開始,就有一個閥值(flag)被大多數(shù)開發(fā)人員所忽視:GOGC。該閥值的缺省值為 100,它的主要工作就是告訴系統(tǒng)何時(shí)觸發(fā) GC。當(dāng)堆達(dá)到其初始大小的 100% 時(shí),默認(rèn)值將觸發(fā) GC 進(jìn)程。將默認(rèn)值更改為更高數(shù)字則延遲 GC 觸發(fā),反之,將更快地觸發(fā) GC。我們開始針對不同的數(shù)值進(jìn)行基準(zhǔn)測試,最終發(fā)現(xiàn)當(dāng) GOGC=2000 時(shí),我們能獲得最佳性能。
這將我們的內(nèi)存使用量從 200 MB 立刻增加到 2.7 GB(這還是在我們更新 Go 版本減少了內(nèi)存消耗之后),并將我們的 CPU 使用率降低 10%。
下面的截圖展示了這些基準(zhǔn)測試的結(jié)果:

Gogc=2000 的結(jié)果
CPU 使用率排名前 4 的函數(shù)變成了我們的服務(wù)函數(shù),這才說得過去。現(xiàn)在,總的 GC 使用量變成了約 13%,比之前的一半還少。
繼續(xù)深入
我們本可以就此打住,但我們還是決定繼續(xù)去調(diào)查分配這么多對象的位置以及原因。很多時(shí)候,分配對象都有一個很好的理由(例如在流處理的情況下,我們?yōu)槊織l消息都創(chuàng)建了很多新對象,因?yàn)樗c下一條消息無關(guān),需要移除它),但在某些情況下,有一種簡單的方法可以優(yōu)化并極大地減少對象創(chuàng)建。
首先,讓我們運(yùn)行一個和之前相同的命令,只做一個小小的變動,采用 heap dump:
- Http://localhost:6060/debug/pprof/heap
為了查詢結(jié)果文件,我們可以在代碼文件目錄中運(yùn)行如下命令來分析調(diào)試結(jié)果:
- go tool pprof -allocobjects <HEAP.PROFILE.FILE>
我們的截圖看起來是這樣的:

除第三行外,一切似乎都很合理。第三行是一個監(jiān)控函數(shù),在每個 Coralogix 規(guī)則解析階段的末尾向我們的 Promethes 導(dǎo)出者(exporter)輸出報(bào)告。為獲取進(jìn)一步的信息,我們運(yùn)行以下命令:
- list <FunctionName>
例如:
- list reportRuleExecution
然后,我們得到以下結(jié)果:

這兩個對 WithLabelValues 的調(diào)用其實(shí)是針對度量的 Prometheus 調(diào)用(我們讓產(chǎn)品來決定是否真的需要它)。此外,我們看到第一行創(chuàng)建了大量對象(占該函數(shù)總分配對象的 10%)。通過進(jìn)一步研究,我們發(fā)現(xiàn)這是一個將客戶 ID 從 int 轉(zhuǎn)換為 string 的過程。該過程非常重要,但考慮到數(shù)據(jù)庫中的客戶數(shù)量有限,我們不應(yīng)該為迎合 Prometheus 而將變量作為字符串接收。
因此,我們沒有在每次創(chuàng)建一個新字符串并在函數(shù)結(jié)束時(shí)丟棄它(浪費(fèi)了分配時(shí)間和 GC 的更多工作),而是在對象初始化時(shí)定義了一個映射,映射了從 1 到 10 萬之間的所有數(shù)字以及一個相對應(yīng)的”get”操作。
我們運(yùn)行了一個新的性能分析會話來驗(yàn)證上述論點(diǎn),結(jié)果證明它是正確的(我們可以看到這部分不再分配對象了):

這并不是一個非常大的改動,但總的來說,這為我們節(jié)省了另一個 GC 活動,更具體地說,大約 1% 的 CPU 使用率。
最終狀態(tài)顯示在下面的截圖中:

2. 最終結(jié)果
內(nèi)存使用:~1.3GB -> ~2.7GB
CPU 使用:~2.55 均值和~5.05 峰值 -> ~2.13 均值和~2.9 峰值。
Golang 優(yōu)化之前的 CPU 使用率:

Golang 優(yōu)化之后的 CPU 使用率:

總的來說,我們的改進(jìn)主要體現(xiàn)在峰值時(shí)間段,每秒處理的日志數(shù)量提升了。這意味著我們的基礎(chǔ)設(shè)施不再需要針對異常值進(jìn)行調(diào)優(yōu),而且變得更加穩(wěn)定。
3. 總結(jié)
通過分析 Go 解析服務(wù),我們能定位出問題區(qū)域,更好地理解我們的服務(wù),并決定在哪里(如果有的話)投入時(shí)間來做改進(jìn)工作。大多數(shù)性能分析工作最終都會參照用戶使用情況對閥值或配置進(jìn)行調(diào)優(yōu),從而獲得更好的性能。
























