客服熱線
186-8811-5347、186-7086-0265
官方郵箱
contactus@mingting.cn
添加微信
立即線上溝通
客服微信
詳情請咨詢客服
客服熱線
186-8811-5347、186-7086-0265
官方郵箱
contactus@mingting.cn
2022-05-15 來源:金山毒霸電腦優化作者:電腦技巧&問題
為什么要做優化
這是一個速度決定一切的時代,我們的生活在不斷地數字化,線下的流程依然在持續向線上轉移,轉移過程中,作為工程師,我們會碰到各種各樣的性能問題。
互聯網公司本質是將用戶共通的行為流程進行了集中化管理,通過中心化的信息交換達到效率提升的目的,同時用規模效應降低了數據交換的成本。
如果業務的后端服務規模足夠大,那么一個程序員通過優化幫公司節省的成本,就可以負擔他十年的工資了。
用人話來講,公司希望的是用盡量少的機器成本來賺取盡量多的利潤。利潤的提升與業務邏輯本身相關,與技術關系不大。而降低成本則是與業務無關,純粹的技術話題。這里面最重要的主題就是“性能優化”。
優化的前置知識
從資源視角出發來對一臺服務器進行審視的話,CPU、內存、磁盤與網絡是后端服務最需要關注的四種資源類型。
對于計算密集型的程序來說,優化的主要精力會放在?CPU?上,要知道?CPU?基本的流水線概念,知道怎么樣在使用少的?CPU?資源的情況下,達到相同的計算目標。
對于?IO?密集型的程序(后端服務一般都是?IO?密集型)來說,優化可以是降低程序的服務延遲,也可以是提升系統整體的吞吐量。
IO?密集型應用主要與磁盤、內存、網絡打交道。因此我們需要知道一些基本的與磁盤、內存、網絡相關的基本數據與常見概念:
要了解內存的多級存儲結構:L1,L2,L3,主存。還要知道這些不同層級的存儲操作時的大致延遲:latency?numbers?every?programmer?should?know[1]。
要知道基本的文件系統讀寫?syscall,批量?syscall,數據同步?syscall。
要熟悉項目中使用的網絡協議,至少要對?TCP,?HTTP?有所了解。
優化越靠近應用層效果越好
Performance?tuning?is?most?effective?when?done?closest?to?where?the?work?is?performed.?For?workloads?driven?by?applications,?this?means?within?the?application?itself.
我們在應用層的邏輯優化能夠幫助應用提升幾十倍的性能,而最底層的優化則只能提升幾個百分點。
這個很好理解,我們可以看到一個?GTA?Online?的新聞:rockstar?thanks?gta?online?player?who?fixed?poor?load?times。
簡單來說,GTA?online?的游戲啟動過程讓玩家等待時間過于漫長,經過各種工具分析,發現一個?10M?的文件加載就需要幾十秒,用戶?diy?進行優化之后,將加載時間減少?70%,并分享出來:how?I?cut?GTA?Online?loading?times?by?70%[3]。
這就是一個非常典型的案例,GTA?在商業上取得了巨大的成功,但不妨礙它局部的代碼是一坨屎。我們只要把這里的重復邏輯干掉,就可以完成三倍的優化效果。同樣的案例,如果我們去優化磁盤的讀寫速度,則可能收效甚微。
優化是與業務場景相關的
不同的業務場景優化的側重也是不同的。
對于大多數無狀態業務模塊來說,內存一般不是瓶頸,所以業務?API?的優化主要聚焦于延遲和吞吐。對于網關類的應用,因為有海量的連接,除了延遲和吞吐,內存占用可能就會成為一個關注的重點。對于存儲類應用,內存是個逃不掉的瓶頸點。
在關注一些性能優化文章時,我們也應特別留意作者的業務場景。場景的側重可能會讓某些人去選擇使用更為?hack?的手段進行優化,而?hack?往往也就意味著?bug。如果你選擇了少有人走過的路,那你要面臨的也是少有人會碰到的?bug。解決起來令人頭疼。
優化的工作流程
對于一個典型的?API?應用來說,優化工作基本遵從下面的工作流:
建立評估指標,例如固定?QPS?壓力下的延遲或內存占用,或模塊在滿足?SLA?前提下的極限?QPS
通過自研、開源壓測工具進行壓測,直到模塊無法滿足預設性能要求:如大量超時,QPS?不達預期,OOM
通過內置?profile?工具尋找性能瓶頸
本地?benchmark?證明優化效果
集成?patch?到業務模塊,回到?2
可以使用的工具
pprofmemory?profiler
Go?內置的內存?profiler?可以讓我們對線上系統進行內存使用采樣,有四個相應的指標:
inuse_objects:當我們認為內存中的駐留對象過多時,就會關注該指標
inuse_space:當我們認為應用程序占據的?RSS?過大時,會關注該指標
alloc_objects:當應用曾經發生過歷史上的大量內存分配行為導致?CPU?或內存使用大幅上升時,可能關注該指標
alloc_space:當應用歷史上發生過內存使用大量上升時,會關注該指標
網關類應用因為海量連接的關系,會導致進程消耗大量內存,所以我們經常看到相關的優化文章,主要就是降低應用的?inuse_space。
而兩個對象數指標主要是為?GC?優化提供依據,當我們進行?GC?調優時,會同時關注應用分配的對象數、正在使用的對象數,以及?GC?的?CPU?占用的指標。
GC?的?CPU?占用情況可以由內置的?CPU?profiler?得到。
cpu?profiler
The?builtin?Go?CPU?profiler?uses?the?setitimer(2)?system?call?to?ask?the?operating?system?to?be?sent?a?SIGPROF?signal?100?times?a?second.?Each?signal?stops?the?Go?process?and?gets?delivered?to?a?random?thread’s?sigtrampgo?function.?This?function?then?proceeds?to?call?sigprof?or?sigprofNonGo?to?record?the?thread’s?current?stack.
Go?語言內置的?CPU?profiler?使用?setitimer?系統調用,操作系統會每秒?100?次向程序發送?SIGPROF?信號。在?Go?進程中會選擇隨機的信號執行?sigtrampgo?函數。該函數使用?sigprof?或?sigprofNonGo?來記錄線程當前的棧。
Since?Go?uses?non-blocking?I/O,?Goroutines?that?wait?on?I/O?are?parked?and?not?running?on?any?threads.?Therefore?they?end?up?being?largely?invisible?to?Go’s?builtin?CPU?profiler.
Go?語言內置的?cpu?profiler?是在性能領域比較常見的?On-CPU?profiler,對于瓶頸主要在?CPU?消耗的應用,我們使用內置的?profiler?也就足夠了。
如果碰到的問題是應用的?CPU?使用不高,但接口的延遲卻很大,那么就需要用上?Off-CPU?profiler,遺憾的是官方的?profiler?并未提供該功能,我們需要借助社區的?fgprof。
fgprof
fgprof?is?implemented?as?a?background?goroutine?that?wakes?up?99?times?per?second?and?calls?runtime.GoroutineProfile.?This?returns?a?list?of?all?goroutines?regardless?of?their?current?On/Off?CPU?scheduling?status?and?their?call?stacks.
fgprof?是啟動了一個后臺的?goroutine,每秒啟動?99?次,調用?runtime.GoroutineProfile?來采集所有?gorooutine?的棧。
雖然看起來很美好:
funcGoroutineProfile(p?[]StackRecord)(n?int,?ok?bool)?{
.....
stopTheWorld(?"profile")
for_,?gp1?:=?rangeallgs?{
......
}
ifn?<=?len(p)?{
//?Save?current?goroutine.
........
systemstack(?func{
saveg(pc,?sp,?gp,?&r[?0])
})
//?Save?other?goroutines.
for_,?gp1?:=?rangeallgs?{
ifisOK(gp1)?{
.......
saveg(^?uintptr(?0),?^?uintptr(?0),?gp1,?&r[?0])
.......
}
}
}
startTheWorld
returnn,?ok
}
但調用?GoroutineProfile?函數的開銷并不低,如果線上系統的?goroutine?上萬,每次采集?profile?都遍歷上萬個?goroutine?的成本實在是太高了。所以?fgprof?只適合在測試環境中使用。
trace
一般情況下我們是不需要使用?trace?來定位性能問題的,通過壓測?+?profile?就可以解決大部分問題,除非我們的問題與?runtime?本身的問題相關。
比如?STW?時間比預想中長,超過百毫秒,向官方反饋問題時,才需要出具相關的?trace?文件。比如類似?long?stw[4]?這樣的?issue。
采集?trace?對系統的性能影響還是比較大的,即使我們只是開啟?gctrace,把?gctrace?日志重定向到文件,對系統延遲也會有一定影響,因為?gctrace?的日志?print?是在?stw?期間來做的:gc?trace?阻塞調度[5]。
perf
如果應用沒有開啟?pprof,在線上應急時,我們也可以臨時使用?perf:
perf?demo
微觀性能優化
編寫?library?時會關注關鍵函數的性能,這時可以脫離系統去探討性能優化,Go?語言的?test?子命令集成了相關的功能,只要我們按照約定來寫?Benchmark?前綴的測試函數,就可以實現函數級的基準測試。我們以常見的二維數組遍歷為例:
packagemain
import"testing"
varx?=?make([][]?int,?100)
funcinit{
fori?:=?0;?i?<?100;?i++?{
x[i]?=?make([]?int,?100)
}
}
functraverseVertical{
fori?:=?0;?i?<?100;?i++?{
forj?:=?0;?j?<?100;?j++?{
x[j][i]?=?1
}
}
}
functraverseHorizontal{
fori?:=?0;?i?<?100;?i++?{
forj?:=?0;?j?<?100;?j++?{
x[i][j]?=?1
}
}
}
funcBenchmarkHorizontal(b?*testing.B){
fori?:=?0;?i?<?b.N;?i++?{
traverseHorizontal
}
}
funcBenchmarkVertical(b?*testing.B){
fori?:=?0;?i?<?b.N;?i++?{
traverseVertical
}
}
執行?go?test?-bench=.
BenchmarkHorizontal-12?102368?10916?ns/op
BenchmarkVertical-12?66612?18197?ns/op
可見橫向遍歷數組要快得多,這提醒我們在寫代碼時要考慮?CPU?的?cache?設計及局部性原理,以使程序能夠在相同的邏輯下獲得更好的性能。
除了?CPU?優化,我們還經常會碰到要優化內存分配的場景。只要帶上?-benchmem?的?flag?就可以實現了。
舉個例子,形如下面這樣的代碼:
logStr?:=?"userid?:"+?userID?+?";?orderid:"+?orderID
你覺得代碼寫的很難看,想要優化一下可讀性,就改成了下列代碼:
logStr?:=?fmt.Sprintf(?"userid:?%v;?orderid:?%v",?userID,?orderID)
這樣的修改方式在某公司的系統中曾經導致了?p2?事故,上線后接口的超時俱增至?SLA?承諾以上。
我們簡單驗證就可以發現:
BenchmarkPrin-12?7168467?157?ns/op?64?B/op?3?allocs/op
BenchmarkPlus?-12?43278558?26.7?ns/op?0?B/op?0?allocs/op
使用?+?進行字符串拼接,不會在堆上產生額外對象。而使用?fmt?系列函數,則會造成局部對象逃逸到堆上,這里是高頻路徑上有大量逃逸,所以導致線上服務的?GC?壓力加重,大量接口超時。
出于謹慎考慮,修改高并發接口時,拿不準的盡量都應進行簡單的線下?benchmark?測試。
當然,我們不能指望靠寫一大堆?benchmark?幫我們發現系統的瓶頸。
實際工作中還是要使用前文提到的優化工作流來進行系統性能優化。也就是盡量從接口整體而非函數局部考慮去發現與解決瓶頸。
宏觀性能優化
接口類的服務,我們可以使用兩種方式對其進行壓測:
固定?QPS?壓測:在每次系統有大的特性發布時,都應進行固定?QPS?壓測,與歷史版本進行對比,需要關注的指標包括,相同?QPS?下的系統的?CPU?使用情況,內存占用情況(監控中的?RSS?值),goroutine?數,GC?觸發頻率和相關指標(是否有較長的?stw,mark?階段是否時間較長等),平均延遲,p99?延遲。
極限?QPS?壓測:極限?QPS?壓測一般只是為了?benchmark?show,沒有太大意義。系統滿負荷時,基本?p99?已經超出正常用戶的忍受范圍了。
壓測過程中需要采集不同?QPS?下的?CPU?profile,內存?profile,記錄?goroutine?數。與歷史情況進行?AB?對比。
Go?的?pprof?還提供了?--base?的?flag,能夠很直觀地幫我們發現不同版本之間的指標差異:用?pprof?比較內存使用差異[6]。
總之記住一點,接口的性能一定是通過壓測來進行優化的,而不是通過硬啃代碼找瓶頸點。關鍵路徑的簡單修改往往可以帶來巨大收益。如果只是啃代碼,很有可能將?1%?優化到?0%,優化了?100%?的局部性能,對接口整體影響微乎其微。
尋找性能瓶頸
在壓測時,我們通過以下步驟來逐漸提升接口的整體性能:
使用固定?QPS?壓測,以階梯形式逐漸增加壓測?QPS,如?1000?->?每分鐘增加?1000?QPS
壓測過程中觀察系統的延遲是否異常
觀察系統的?CPU?使用情況
如果?CPU?使用率在達到一定值之后不再上升,反而引起了延遲的劇烈波動,這時大概率是發生了阻塞,進入?pprof?的?web?頁面,點擊?goroutine,查看?top?的?goroutine?數,這時應該有大量的?goroutine?阻塞在某處,比如?Semacquire
如果?CPU?上升較快,未達到預期吞吐就已經過了高水位,則可以重點考察?CPU?使用是否合理,在?CPU?高水位進行?profile?采樣,重點關注火焰圖中較寬的“平頂山”
重復上述步驟,直至系統性能達到或超越我們設置的性能目標。
一些優化案例
gc?mark?占用過多?CPU
在?Go?語言中?gc?mark?占用的?CPU?主要和運行時的對象數相關,也就是我們需要看?inuse_objects。
定時任務,或訪問流量不規律的應用,需要關注?alloc_objects。
優化主要是下面幾方面:
減少變量逃逸
盡量在棧上分配對象,關于逃逸的規則,可以查看?Go?編譯器代碼中的逃逸測試部分:
查看某個?package?內的逃逸情況,可以使用?build?+?全路徑的方式,如:
go?build?-gcflags="-m?-m"?github.com/cch123/elasticsql
需要注意的是,逃逸分析的結果是會隨?著版本變化的,所以去背誦網上逃逸相關的文章結論是沒有什么意義的。
使用?sync.Pool?復用堆上對象
sync.Pool?用出花兒的就是?fasthttp?了,可以看看我之前寫的這一篇:fasthttp?為什么快[7]。
最簡單的復用就是復用各種?struct,slice,在復用時?put?時,需要判斷?size?是否已經擴容過頭,小心因為?sync.Pool?中存了大量的巨型對象導致進程占用了大量內存。
調度占用過多?CPU
goroutine?頻繁創建與銷毀會給調度造成較大的負擔,如果我們發現?CPU?火焰圖中?schedule,findrunnable?占用了大量?CPU,那么可以考慮使用開源的?workerpool?來進行改進,比較典型的?fasthttp?worker?pool[8]。
如果客戶端與服務端之間使用的是短連接,那么我們可以使用長連接來減少連接創建的開銷,這里就包含了?goroutine?的創建與銷毀。
進程占用大量內存
當前大多數的業務后端服務是不太需要關注進程消耗的內存的。
我們經常看到做?Go?內存占用優化的是在網關(包括?mesh)、存儲系統這兩個場景。
對于網關類系統來說,Go?的內存占用主要是因為?Go?獨特的抽象模型造成的,這個很好理解:
海量的連接加上海量的?goroutine,使網關和?mesh?成為?Go?OOM?的重災區。所以網關側的優化一般就是優化:
goroutine?占用的棧內存
read?buffer?和?write?buffer?占用的內存
很多項目都有相關的分享,這里就不再贅述了。
對于存儲類系統來說,內存占用方面的不少努力也是在優化各種?buffer,比如?dgraph?使用?cgo?+?jemalloc?來優化他們的產品內存占用[9]。
堆外內存不會在?Go?的?GC?系統里進行管轄,所以也不會影響到?Go?的?GC?Heap?Goal,所以不會因為分配大量對象造成?Go?的?Heap?Goal?被推高,系統整體占用的?RSS?也被推高。
鎖沖突嚴重,導致吞吐量瓶頸
我在?幾個?Go?系統可能遇到的鎖問題[10]?中分享過實際的線上?case。
進行鎖優化的思路無非就一個“拆”和一個“縮”字:
拆:將鎖粒度進行拆分,比如全局鎖,我能不能把鎖粒度拆分為連接粒度的鎖;如果是連接粒度的鎖,那我能不能拆分為請求粒度的鎖;在?logger?fd?或?net?fd?上加的鎖不太好拆,那么我們增加一些客戶端,比如從?1->?100,降低鎖的沖突是不是就可以了。
縮:縮小鎖的臨界區,業務允許的前提下,可以把?syscall?移到鎖外面;有時只是想要鎖?map?的讀寫邏輯,但是卻不小心鎖了連接讀寫的邏輯,或許簡單地用?sync.Map?來代替?map?Lock,defer?Unlock?就能簡單地縮小臨界區了。
timer?相關函數占用大量?CPU
同樣是在網關和海量連接的應用中較常見,優化手段:
使用時間輪/粗粒度的時間管理,精確到?ms?級一般就足夠了
升級到?Go?1.14+,享受官方的升級紅利
模擬真實工作負載
在前面的論述中,我們對問題進行了簡化。真實世界中的后端系統往往不只一個接口,壓測工具、平臺往往只支持單接口壓測。
公司的業務希望知道的是后端系統整體性能,即這些系統作為一個整體,在限定的資源條件下,能夠承載多少業務量(如并發創建訂單)而不崩潰。
雖然大家都在講微服務,但單一服務往往也不只有單一功能,如果一個系統有?10?個接口(已經算是很小的服務了),那么這個服務的真實負載是很難靠人肉去模擬的。
這也就是為什么互聯網公司普遍都需要做全鏈路壓測。像樣點的公司會定期進行全鏈路壓測演練,以便知曉隨著系統快速迭代變化,系統整體是否出現了嚴重的性能衰退。
通過真實的工作負載,我們才能發現真實的線上性能問題。講全鏈路壓測的文章也很多,本文就不再贅述了。
當前性能問題定位工具的局限性
本文中幾乎所有優化手段都是通過?Benchmark?和壓測來進行的,但真實世界的
做?ToB?生意,我們的應用是部署在客戶側(比如一些數據庫產品),客戶說我們的應用會?OOM,但是我們很難拿到?OOM?的現場,不知道到底是哪些對象分配導致了?OOM
做大型平臺,平臺上有各種不同類型的用戶編寫代碼,升級用戶代碼后,線上出現各種?CPU?毛刺和?OOM?問題
這些問題在壓測中是發現不了的,需要有更為靈活的工具和更為強大的平臺。
最后,小編給您推薦,金山毒霸“文檔修復大師”,支持.doc/.ppt/.xls等格式的文檔的修復,修復后再也不用為文檔的數據丟失而煩惱。
上一篇:磁盤怎樣檢測有沒有用過