優(yōu)化 Golang 服務(wù)來(lái)減少 40% 以上的 CPU
十年前,谷歌正在面臨一個(gè)由 C++ 編譯時(shí)間過(guò)長(zhǎng)所造成的嚴(yán)重瓶頸,并且需要一個(gè)全新的方式來(lái)解決這個(gè)問(wèn)題。谷歌的工程師們通過(guò)創(chuàng)造了一種新的被稱作 Go (又名 Golang)的語(yǔ)言來(lái)應(yīng)對(duì)挑戰(zhàn)。這個(gè)新語(yǔ)言 Go 帶來(lái)了 C++ 最好的部分(最主要的是它的性能和穩(wěn)定性),又與 Python 的速度相結(jié)合,使得 Go 能夠在實(shí)現(xiàn)并發(fā)的同時(shí)快速地使用多核心。
在 Coralogix(譯者注:一個(gè)提供全面日志分析的服務(wù)產(chǎn)品,官網(wǎng)[1]),我們?yōu)榱巳ソo我們的客戶提供關(guān)于他們?nèi)罩緦?shí)時(shí)的分析、警報(bào)和元數(shù)據(jù),要去解析他們的日志。在解析階段,我們需要非常快速地解析包含多個(gè)復(fù)雜規(guī)則的服務(wù)日志,這個(gè)目標(biāo)是促使我們決定使用 Golang 的原因之一。
這項(xiàng)新的服務(wù)現(xiàn)在就全天候的跑在生產(chǎn)階段,盡管我們看到了非常好的結(jié)果,但是它也需要跑在高性能的機(jī)器上。這項(xiàng) Go 的服務(wù)跑在一臺(tái) AWS m4.2xlarge 實(shí)例上 ,帶有 8 CPUs 和 36 GB 的配置,每天要解析幾十億的日志。
在這個(gè)階段一切都運(yùn)行正常,我們本可以自我感覺(jué)良好,但是那并不是我們?cè)?Coralogix 想要的表現(xiàn)。我們想要更多的特性,比如性能等等,或者使用更少的 AWS 實(shí)例。為了改進(jìn),我們首先需要理解瓶頸的本質(zhì)以及我們?nèi)绾文軌驕p少或者完全解決這些問(wèn)題。
我們決定在我們的服務(wù)上進(jìn)行一些分析,檢查一下到底是什么造成了 CPU 的高消耗,看看我們是否能夠優(yōu)化。
首先,我們將 Go 升級(jí)到最新的穩(wěn)定版本(這是軟件生命周期中的關(guān)鍵一步)。我們是用的 Go 1.12.4 版本,最新的是 1.13.8(目前最新版本已經(jīng) 1.14.x)。根據(jù) 文檔[2] ,Go 1.13 發(fā)行版在運(yùn)行時(shí)庫(kù)方面和一些其他主要利用內(nèi)存使用的組件方面已經(jīng)有了長(zhǎng)足的進(jìn)步??傊褂米钚碌姆€(wěn)定版本能幫助我們節(jié)省許多工作。
因此,內(nèi)存消耗由大約 800 MB 降低到了僅 180 MB。
第二,為了更好的理解我們的流程以及弄清楚我們應(yīng)該在哪花費(fèi)時(shí)間和資源,我們開(kāi)始去進(jìn)行分析。
分析不同的服務(wù)和程序語(yǔ)言可能看起來(lái)很復(fù)雜并且令人望而生畏,但是對(duì)于 Go 來(lái)說(shuō)它實(shí)際上十分容易,僅僅幾個(gè)命令就能夠描述清楚。Go 有一個(gè)專門的工具叫“pprof”,它通過(guò)監(jiān)聽(tīng)一個(gè)路由(默認(rèn)端口 6060)能夠應(yīng)用在你的 app 上,并且使用 Go 的包來(lái)管理 HTTP 連接:
import?_?"net/http/pprof"
接著在你的 main 函數(shù)中或者路由包下按照如下操作初始化:
go?func()?{
?log.Println(http.ListenAndServe("localhost:6060",?nil))
}()
現(xiàn)在你可以啟動(dòng)你的服務(wù)并且連接到:
http://localhost:6060/debug/pprof
Go 官方提供的完整文檔可以 在這[3] 找到。
pprof 的默認(rèn)配置是每 30 秒對(duì) CPU 的使用情況進(jìn)行采樣。有許多不同的選擇,也可以對(duì) CPU 的使用,堆的使用或者其他更多的使用情況進(jìn)行采樣。
我們主要關(guān)注 CPU 使用,因此在生產(chǎn)階段采取了一個(gè) 30 秒的性能分析,并且發(fā)現(xiàn)了你在下圖所看到的情況(提醒一下:這是在我們把 Go 版本升級(jí)并且將 Go 的內(nèi)部組件降到最低之后的結(jié)果):
Go profiling — Coralogix
正如你所看到的,我們發(fā)現(xiàn)了許多運(yùn)行時(shí)庫(kù)的活動(dòng):GC 幾乎使用了 29% 的 CPU(還僅僅只是消耗最多的前 20 個(gè)對(duì)象)。因?yàn)?Go 的 GC 非??觳⑶易隽司薮蟮膬?yōu)化,最好的實(shí)踐就是不要去改變或者修改它。因?yàn)槲覀兊膬?nèi)存消耗非常低(與我們先前的 Go 版本相比),所以主要的懷疑對(duì)象就變成了較高的對(duì)象分配率。
如果是那種情況的話,我們就能做兩件事情了:
調(diào)整 Go GC 活動(dòng),使其適合我們的服務(wù)行為,意味著 —— 延緩它的觸發(fā)以使 GC 變的不那么頻繁。這將使我們不得不補(bǔ)償更多的內(nèi)存使用。 找出我們代碼中那些分配了太多對(duì)象的函數(shù)、區(qū)段或者行。
觀察一下我們的實(shí)例類型,很明顯我們有大量的內(nèi)存可供使用,并且我們正在被機(jī)器的 CPU 數(shù)量所限制。因此我們僅僅需要調(diào)整一下比率。因?yàn)樵?Golang 的早期有一個(gè)大多數(shù)開(kāi)發(fā)者都不關(guān)注的數(shù)據(jù),叫 GOGC。這個(gè)數(shù)值默認(rèn)是 100,簡(jiǎn)單地告訴你的系統(tǒng)什么時(shí)候觸發(fā) GC。這個(gè)默認(rèn)值使得堆的大小在到達(dá)它初始態(tài)的兩倍時(shí)觸發(fā) GC。將這個(gè)數(shù)值改成一個(gè)更大的數(shù)將會(huì)延緩 GC 的觸發(fā),降低它的頻率。我們基準(zhǔn)測(cè)試了許多不同的數(shù),最終對(duì)于我們的目標(biāo)來(lái)說(shuō)最好的性能是在使用 GOGC = 2000 的時(shí)候。
這立刻增加了我們的內(nèi)存使用,從大約 200 MB 到 大約 2.7 GB(那還是由于我們的 Go 版本更新,在內(nèi)存消耗降低的情況下),另外也減少了我們 CPU 大約 10% 的使用。
這個(gè)接下來(lái)的截圖就展示了這些基準(zhǔn)測(cè)試的結(jié)果:
GOGC =2000 results — Coralogix benchmark
前面的四個(gè) CPU 的消耗函數(shù)就是我們的服務(wù)函數(shù),這十分有意義。全部的 GC 使用現(xiàn)在大約是 13%,是先前消耗的一半還少!
我們其實(shí)可以在這就停下來(lái)了,但是我們還是決定去揭露我們?cè)谀牟⑶覟槭裁磿?huì)分配這么多對(duì)象。很多時(shí)候,這么做有充分理由(比如在流式處理的情況下,我們?yōu)槊織l獲取的消息創(chuàng)建了許多新的對(duì)象,并且因?yàn)樗c下一條消息無(wú)關(guān),需要去移除它),但是在某些情況下有一種簡(jiǎn)單的方法可以去優(yōu)化并且動(dòng)態(tài)地減少對(duì)象的創(chuàng)建。
首先,讓我們運(yùn)行一個(gè)和之前同樣的命令,有一點(diǎn)小的改變,采用堆調(diào)試:
http://localhost:6060/debug/pprof/heap
為了查詢結(jié)果文件,你可以運(yùn)行如下命令在你的代碼目錄下來(lái)分析調(diào)試結(jié)果:
go?tool?pprof?-alloc_objects?
我們的截圖看起來(lái)像這樣:
除了第三行一切似乎都很合理,這是一個(gè)監(jiān)控函數(shù),在每個(gè) Carologix 規(guī)則解析階段的末尾向我們的 Promethes 調(diào)用者展示結(jié)果。為了獲取進(jìn)一步信息,我們運(yùn)行如下命令:
list?
例如:
list?reportRuleExecution
然后我們會(huì)獲得如下結(jié)果:
WithLabelValues 的兩個(gè)調(diào)用都是為了軟件度量的 Prometheus 函數(shù)(我們將這個(gè)留給產(chǎn)品去決定是否真正需要)。而且,我們可以看到第一行創(chuàng)建了大量的對(duì)象(由這個(gè)函數(shù)所創(chuàng)建的全部對(duì)象的 10%)。我們進(jìn)一步查看發(fā)現(xiàn)它是一個(gè)對(duì)于綁定到導(dǎo)出數(shù)據(jù)的消費(fèi)者 ID 從 int 到 string 的轉(zhuǎn)換,十分重要,但是考慮到實(shí)際情況,我們數(shù)據(jù)庫(kù)中消費(fèi)者的數(shù)量十分有限,我們不應(yīng)該采用 Prometheus 的方式來(lái)接收變量作為 string 類型。因此取代了每次創(chuàng)建一個(gè)新的 string 并且在函數(shù)末尾都拋棄的這種方法(浪費(fèi)分配還有 GC 的多余工作),我們?cè)趯?duì)象的分配階段定義了 map,配對(duì)了所有從 1 到 10 萬(wàn)的數(shù)字和一個(gè)需要執(zhí)行的 “get” 方法。
現(xiàn)在運(yùn)行一個(gè)新的性能分析會(huì)話來(lái)驗(yàn)證我們的論點(diǎn)并且它的對(duì)的(你可以看到這一部分并不會(huì)再分配對(duì)象了):

這并不是一個(gè)顯著的改進(jìn),但是總體來(lái)說(shuō)為我們節(jié)省了另一個(gè) GC 的活動(dòng),說(shuō)的更具體一點(diǎn)就是節(jié)省了大約 1% 的 CPU。
最終的狀態(tài)就是下面的截圖:

最終結(jié)果
1) 內(nèi)存使用:大約 1.3 GB -> 大約 2.7 GB
2) CPU 使用:大約 2.55 avg 和 大約 5.05 峰值期 -> 大約 2.13 avg 和 大約 2.9 峰值期。
在我們 Golang 優(yōu)化前的 CPU:
在我們 Golang 優(yōu)化后的 CPU:
總體來(lái)說(shuō),我們可以看到主要的改進(jìn)是在每秒日志處理量增加時(shí)的高峰時(shí)間。這就意味著我們的基礎(chǔ)架構(gòu)不僅不需要再為了異常值進(jìn)行調(diào)整,而且變得更加穩(wěn)定了。
總結(jié)
通過(guò)對(duì)我們的 Go 解析服務(wù)進(jìn)行性能測(cè)試,我們能夠查明有問(wèn)題的地方,更好的理解我們的服務(wù)并且確定在哪里(如果有的話)投資時(shí)間進(jìn)行改進(jìn)。大多數(shù)性能分析工作都會(huì)以一些基礎(chǔ)數(shù)值或配置的調(diào)整,更合適你的使用情況并且最終展現(xiàn)更好的性能而結(jié)束。
via:https://medium.com/coralogix-engineering/optimizing-a-golang-service-to-reduce-over-40-cpu-366b67c67ef9
作者:Eliezer Yaacov[4]譯者:sh1luo[5]校對(duì):@unknwon[6]
本文由 GCTT[7] 原創(chuàng)編譯,Go 中文網(wǎng)[8] 榮譽(yù)推出
參考資料
官網(wǎng): https://coralogix.com/
[2]文檔: https://golang.org/doc/devel/release.html
[3]在這: https://golang.org/pkg/net/http/pprof
[4]Eliezer Yaacov: https://medium.com/@eliezerj8
[5]sh1luo: https://github.com/sh1luo
[6]@unknwon: https://github.com/unknwon
[7]GCTT: https://github.com/studygolang/GCTT
[8]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
站長(zhǎng) polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場(chǎng)和創(chuàng)業(yè)經(jīng)驗(yàn)
Go語(yǔ)言中文網(wǎng)
每天為你
分享 Go 知識(shí)
Go愛(ài)好者值得關(guān)注
