Go 應(yīng)用性能優(yōu)化指北
假期重溫曹大的性能優(yōu)化文章,收獲還是很大的,值得所有 Gopher 精讀。無(wú)論是理論還是實(shí)踐,都能從中學(xué)到很多。
為什么要做優(yōu)化
這是一個(gè)速度決定一切的時(shí)代,我們的生活在不斷地?cái)?shù)字化,線下的流程依然在持續(xù)向線上轉(zhuǎn)移,轉(zhuǎn)移過程中,作為工程師,我們會(huì)碰到各種各樣的性能問題。
互聯(lián)網(wǎng)公司本質(zhì)是將用戶共通的行為流程進(jìn)行了集中化管理,通過中心化的信息交換達(dá)到效率提升的目的,同時(shí)用規(guī)模效應(yīng)降低了數(shù)據(jù)交換的成本。
用人話來(lái)講,公司希望的是用盡量少的機(jī)器成本來(lái)賺取盡量多的利潤(rùn)。利潤(rùn)的提升與業(yè)務(wù)邏輯本身相關(guān),與技術(shù)關(guān)系不大。而降低成本則是與業(yè)務(wù)無(wú)關(guān),純粹的技術(shù)話題。這里面最重要的主題就是“性能優(yōu)化”。
如果業(yè)務(wù)的后端服務(wù)規(guī)模足夠大,那么一個(gè)程序員通過優(yōu)化幫公司節(jié)省的成本,就可以負(fù)擔(dān)他十年的工資了。
優(yōu)化的前置知識(shí)
從資源視角出發(fā)來(lái)對(duì)一臺(tái)服務(wù)器進(jìn)行審視的話,CPU、內(nèi)存、磁盤與網(wǎng)絡(luò)是后端服務(wù)最需要關(guān)注的四種資源類型。
對(duì)于計(jì)算密集型的程序來(lái)說,優(yōu)化的主要精力會(huì)放在 CPU 上,要知道 CPU 基本的流水線概念,知道怎么樣在使用少的 CPU 資源的情況下,達(dá)到相同的計(jì)算目標(biāo)。
對(duì)于 IO 密集型的程序(后端服務(wù)一般都是 IO 密集型)來(lái)說,優(yōu)化可以是降低程序的服務(wù)延遲,也可以是提升系統(tǒng)整體的吞吐量。
IO 密集型應(yīng)用主要與磁盤、內(nèi)存、網(wǎng)絡(luò)打交道。因此我們需要知道一些基本的與磁盤、內(nèi)存、網(wǎng)絡(luò)相關(guān)的基本數(shù)據(jù)與常見概念:
要了解內(nèi)存的多級(jí)存儲(chǔ)結(jié)構(gòu):L1,L2,L3,主存。還要知道這些不同層級(jí)的存儲(chǔ)操作時(shí)的大致延遲:latency numbers every programmer should know[1]。 要知道基本的文件系統(tǒng)讀寫 syscall,批量 syscall,數(shù)據(jù)同步 syscall。 要熟悉項(xiàng)目中使用的網(wǎng)絡(luò)協(xié)議,至少要對(duì) TCP, HTTP 有所了解。
優(yōu)化越靠近應(yīng)用層效果越好
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.
我們?cè)趹?yīng)用層的邏輯優(yōu)化能夠幫助應(yīng)用提升幾十倍的性能,而最底層的優(yōu)化則只能提升幾個(gè)百分點(diǎn)。
這個(gè)很好理解,我們可以看到一個(gè) GTA Online 的新聞:rockstar thanks gta online player who fixed poor load times[2]。
簡(jiǎn)單來(lái)說,GTA online 的游戲啟動(dòng)過程讓玩家等待時(shí)間過于漫長(zhǎng),經(jīng)過各種工具分析,發(fā)現(xiàn)一個(gè) 10M 的文件加載就需要幾十秒,用戶 diy 進(jìn)行優(yōu)化之后,將加載時(shí)間減少 70%,并分享出來(lái):how I cut GTA Online loading times by 70%[3]。
這就是一個(gè)非常典型的案例,GTA 在商業(yè)上取得了巨大的成功,但不妨礙它局部的代碼是一坨屎。我們只要把這里的重復(fù)邏輯干掉,就可以完成三倍的優(yōu)化效果。同樣的案例,如果我們?nèi)?yōu)化磁盤的讀寫速度,則可能收效甚微。
優(yōu)化是與業(yè)務(wù)場(chǎng)景相關(guān)的
不同的業(yè)務(wù)場(chǎng)景優(yōu)化的側(cè)重也是不同的。
對(duì)于大多數(shù)無(wú)狀態(tài)業(yè)務(wù)模塊來(lái)說,內(nèi)存一般不是瓶頸,所以業(yè)務(wù) API 的優(yōu)化主要聚焦于延遲和吞吐。對(duì)于網(wǎng)關(guān)類的應(yīng)用,因?yàn)橛泻A康倪B接,除了延遲和吞吐,內(nèi)存占用可能就會(huì)成為一個(gè)關(guān)注的重點(diǎn)。對(duì)于存儲(chǔ)類應(yīng)用,內(nèi)存是個(gè)逃不掉的瓶頸點(diǎn)。
在關(guān)注一些性能優(yōu)化文章時(shí),我們也應(yīng)特別留意作者的業(yè)務(wù)場(chǎng)景。場(chǎng)景的側(cè)重可能會(huì)讓某些人去選擇使用更為 hack 的手段進(jìn)行優(yōu)化,而 hack 往往也就意味著 bug。如果你選擇了少有人走過的路,那你要面臨的也是少有人會(huì)碰到的 bug。解決起來(lái)令人頭疼。
優(yōu)化的工作流程
對(duì)于一個(gè)典型的 API 應(yīng)用來(lái)說,優(yōu)化工作基本遵從下面的工作流:
建立評(píng)估指標(biāo),例如固定 QPS 壓力下的延遲或內(nèi)存占用,或模塊在滿足 SLA 前提下的極限 QPS 通過自研、開源壓測(cè)工具進(jìn)行壓測(cè),直到模塊無(wú)法滿足預(yù)設(shè)性能要求:如大量超時(shí),QPS 不達(dá)預(yù)期,OOM 通過內(nèi)置 profile 工具尋找性能瓶頸 本地 benchmark 證明優(yōu)化效果 集成 patch 到業(yè)務(wù)模塊,回到 2
可以使用的工具
pprof
memory profiler
Go 內(nèi)置的內(nèi)存 profiler 可以讓我們對(duì)線上系統(tǒng)進(jìn)行內(nèi)存使用采樣,有四個(gè)相應(yīng)的指標(biāo):
inuse_objects:當(dāng)我們認(rèn)為內(nèi)存中的駐留對(duì)象過多時(shí),就會(huì)關(guān)注該指標(biāo) inuse_space:當(dāng)我們認(rèn)為應(yīng)用程序占據(jù)的 RSS 過大時(shí),會(huì)關(guān)注該指標(biāo) alloc_objects:當(dāng)應(yīng)用曾經(jīng)發(fā)生過歷史上的大量?jī)?nèi)存分配行為導(dǎo)致 CPU 或內(nèi)存使用大幅上升時(shí),可能關(guān)注該指標(biāo) alloc_space:當(dāng)應(yīng)用歷史上發(fā)生過內(nèi)存使用大量上升時(shí),會(huì)關(guān)注該指標(biāo)
網(wǎng)關(guān)類應(yīng)用因?yàn)楹A窟B接的關(guān)系,會(huì)導(dǎo)致進(jìn)程消耗大量?jī)?nèi)存,所以我們經(jīng)常看到相關(guān)的優(yōu)化文章,主要就是降低應(yīng)用的 inuse_space。
而兩個(gè)對(duì)象數(shù)指標(biāo)主要是為 GC 優(yōu)化提供依據(jù),當(dāng)我們進(jìn)行 GC 調(diào)優(yōu)時(shí),會(huì)同時(shí)關(guān)注應(yīng)用分配的對(duì)象數(shù)、正在使用的對(duì)象數(shù),以及 GC 的 CPU 占用的指標(biāo)。
GC 的 CPU 占用情況可以由內(nèi)置的 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 語(yǔ)言內(nèi)置的 CPU profiler 使用 setitimer 系統(tǒng)調(diào)用,操作系統(tǒng)會(huì)每秒 100 次向程序發(fā)送 SIGPROF 信號(hào)。在 Go 進(jìn)程中會(huì)選擇隨機(jī)的線程執(zhí)行 sigtrampgo 函數(shù)。該函數(shù)使用 sigprof 或 sigprofNonGo 來(lái)記錄線程當(dāng)前的棧。
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 語(yǔ)言內(nèi)置的 cpu profiler 是在性能領(lǐng)域比較常見的 On-CPU profiler,對(duì)于瓶頸主要在 CPU 消耗的應(yīng)用,我們使用內(nèi)置的 profiler 也就足夠了。
如果碰到的問題是應(yīng)用的 CPU 使用不高,但接口的延遲卻很大,那么就需要用上 Off-CPU profiler,遺憾的是官方的 profiler 并未提供該功能,我們需要借助社區(qū)的 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 是啟動(dòng)了一個(gè)后臺(tái)的 goroutine,每秒啟動(dòng) 99 次,調(diào)用 runtime.GoroutineProfile 來(lái)采集所有 gorooutine 的棧。
雖然看起來(lái)很美好:
func GoroutineProfile(p []StackRecord) (n int, ok bool) {
.....
stopTheWorld("profile")
for _, gp1 := range allgs {
......
}
if n <= len(p) {
// Save current goroutine.
........
systemstack(func() {
saveg(pc, sp, gp, &r[0])
})
// Save other goroutines.
for _, gp1 := range allgs {
if isOK(gp1) {
.......
saveg(^uintptr(0), ^uintptr(0), gp1, &r[0])
.......
}
}
}
startTheWorld()
return n, ok
}
但調(diào)用 GoroutineProfile 函數(shù)的開銷并不低,如果線上系統(tǒng)的 goroutine 上萬(wàn),每次采集 profile 都遍歷上萬(wàn)個(gè) goroutine 的成本實(shí)在是太高了。所以 fgprof 只適合在測(cè)試環(huán)境中使用。
trace
一般情況下我們是不需要使用 trace 來(lái)定位性能問題的,通過壓測(cè) + profile 就可以解決大部分問題,除非我們的問題與 runtime 本身的問題相關(guān)。
比如 STW 時(shí)間比預(yù)想中長(zhǎng),超過百毫秒,向官方反饋問題時(shí),才需要出具相關(guān)的 trace 文件。比如類似 long stw[4] 這樣的 issue。
采集 trace 對(duì)系統(tǒng)的性能影響還是比較大的,即使我們只是開啟 gctrace,把 gctrace 日志重定向到文件,對(duì)系統(tǒng)延遲也會(huì)有一定影響,因?yàn)?gctrace 的日志 print 是在 stw 期間來(lái)做的:gc trace 阻塞調(diào)度[5]。
perf
如果應(yīng)用沒有開啟 pprof,在線上應(yīng)急時(shí),我們也可以臨時(shí)使用 perf:

微觀性能優(yōu)化
編寫 library 時(shí)會(huì)關(guān)注關(guān)鍵函數(shù)的性能,這時(shí)可以脫離系統(tǒng)去探討性能優(yōu)化,Go 語(yǔ)言的 test 子命令集成了相關(guān)的功能,只要我們按照約定來(lái)寫 Benchmark 前綴的測(cè)試函數(shù),就可以實(shí)現(xiàn)函數(shù)級(jí)的基準(zhǔn)測(cè)試。我們以常見的二維數(shù)組遍歷為例:
package main
import "testing"
var x = make([][]int, 100)
func init() {
for i := 0; i < 100; i++ {
x[i] = make([]int, 100)
}
}
func traverseVertical() {
for i := 0; i < 100; i++ {
for j := 0; j < 100; j++ {
x[j][i] = 1
}
}
}
func traverseHorizontal() {
for i := 0; i < 100; i++ {
for j := 0; j < 100; j++ {
x[i][j] = 1
}
}
}
func BenchmarkHorizontal(b *testing.B) {
for i := 0; i < b.N; i++ {
traverseHorizontal()
}
}
func BenchmarkVertical(b *testing.B) {
for i := 0; i < b.N; i++ {
traverseVertical()
}
}
執(zhí)行 go test -bench=.
BenchmarkHorizontal-12 102368 10916 ns/op
BenchmarkVertical-12 66612 18197 ns/op
可見橫向遍歷數(shù)組要快得多,這提醒我們?cè)趯懘a時(shí)要考慮 CPU 的 cache 設(shè)計(jì)及局部性原理,以使程序能夠在相同的邏輯下獲得更好的性能。
除了 CPU 優(yōu)化,我們還經(jīng)常會(huì)碰到要優(yōu)化內(nèi)存分配的場(chǎng)景。只要帶上 -benchmem 的 flag 就可以實(shí)現(xiàn)了。
舉個(gè)例子,形如下面這樣的代碼:
logStr := "userid :" + userID + "; orderid:" + orderID
你覺得代碼寫的很難看,想要優(yōu)化一下可讀性,就改成了下列代碼:
logStr := fmt.Sprintf("userid: %v; orderid: %v", userID, orderID)
這樣的修改方式在某公司的系統(tǒng)中曾經(jīng)導(dǎo)致了 p2 事故,上線后接口的超時(shí)俱增至 SLA 承諾以上。
我們簡(jiǎn)單驗(yàn)證就可以發(fā)現(xiàn):
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
使用 + 進(jìn)行字符串拼接,不會(huì)在堆上產(chǎn)生額外對(duì)象。而使用 fmt 系列函數(shù),則會(huì)造成局部對(duì)象逃逸到堆上,這里是高頻路徑上有大量逃逸,所以導(dǎo)致線上服務(wù)的 GC 壓力加重,大量接口超時(shí)。
出于謹(jǐn)慎考慮,修改高并發(fā)接口時(shí),拿不準(zhǔn)的盡量都應(yīng)進(jìn)行簡(jiǎn)單的線下 benchmark 測(cè)試。
當(dāng)然,我們不能指望靠寫一大堆 benchmark 幫我們發(fā)現(xiàn)系統(tǒng)的瓶頸。
實(shí)際工作中還是要使用前文提到的優(yōu)化工作流來(lái)進(jìn)行系統(tǒng)性能優(yōu)化。也就是盡量從接口整體而非函數(shù)局部考慮去發(fā)現(xiàn)與解決瓶頸。
宏觀性能優(yōu)化
接口類的服務(wù),我們可以使用兩種方式對(duì)其進(jìn)行壓測(cè):
固定 QPS 壓測(cè):在每次系統(tǒng)有大的特性發(fā)布時(shí),都應(yīng)進(jìn)行固定 QPS 壓測(cè),與歷史版本進(jìn)行對(duì)比,需要關(guān)注的指標(biāo)包括,相同 QPS 下的系統(tǒng)的 CPU 使用情況,內(nèi)存占用情況(監(jiān)控中的 RSS 值),goroutine 數(shù),GC 觸發(fā)頻率和相關(guān)指標(biāo)(是否有較長(zhǎng)的 stw,mark 階段是否時(shí)間較長(zhǎng)等),平均延遲,p99 延遲。 極限 QPS 壓測(cè):極限 QPS 壓測(cè)一般只是為了 benchmark show,沒有太大意義。系統(tǒng)滿負(fù)荷時(shí),基本 p99 已經(jīng)超出正常用戶的忍受范圍了。
壓測(cè)過程中需要采集不同 QPS 下的 CPU profile,內(nèi)存 profile,記錄 goroutine 數(shù)。與歷史情況進(jìn)行 AB 對(duì)比。
Go 的 pprof 還提供了 --base 的 flag,能夠很直觀地幫我們發(fā)現(xiàn)不同版本之間的指標(biāo)差異:用 pprof 比較內(nèi)存使用差異[6]。
總之記住一點(diǎn),接口的性能一定是通過壓測(cè)來(lái)進(jìn)行優(yōu)化的,而不是通過硬啃代碼找瓶頸點(diǎn)。關(guān)鍵路徑的簡(jiǎn)單修改往往可以帶來(lái)巨大收益。如果只是啃代碼,很有可能將 1% 優(yōu)化到 0%,優(yōu)化了 100% 的局部性能,對(duì)接口整體影響微乎其微。
尋找性能瓶頸
在壓測(cè)時(shí),我們通過以下步驟來(lái)逐漸提升接口的整體性能:
使用固定 QPS 壓測(cè),以階梯形式逐漸增加壓測(cè) QPS,如 1000 -> 每分鐘增加 1000 QPS 壓測(cè)過程中觀察系統(tǒng)的延遲是否異常 觀察系統(tǒng)的 CPU 使用情況 如果 CPU 使用率在達(dá)到一定值之后不再上升,反而引起了延遲的劇烈波動(dòng),這時(shí)大概率是發(fā)生了阻塞,進(jìn)入 pprof 的 web 頁(yè)面,點(diǎn)擊 goroutine,查看 top 的 goroutine 數(shù),這時(shí)應(yīng)該有大量的 goroutine 阻塞在某處,比如 Semacquire 如果 CPU 上升較快,未達(dá)到預(yù)期吞吐就已經(jīng)過了高水位,則可以重點(diǎn)考察 CPU 使用是否合理,在 CPU 高水位進(jìn)行 profile 采樣,重點(diǎn)關(guān)注火焰圖中較寬的“平頂山”
一些優(yōu)化案例
gc mark 占用過多 CPU
在 Go 語(yǔ)言中 gc mark 占用的 CPU 主要和運(yùn)行時(shí)的對(duì)象數(shù)相關(guān),也就是我們需要看 inuse_objects。
定時(shí)任務(wù),或訪問流量不規(guī)律的應(yīng)用,需要關(guān)注 alloc_objects。
優(yōu)化主要是下面幾方面:
減少變量逃逸
盡量在棧上分配對(duì)象,關(guān)于逃逸的規(guī)則,可以查看 Go 編譯器代碼中的逃逸測(cè)試部分:

查看某個(gè) package 內(nèi)的逃逸情況,可以使用 build + 全路徑的方式,如:
go build -gcflags="-m -m" github.com/cch123/elasticsql
需要注意的是,逃逸分析的結(jié)果是會(huì)隨著版本變化的,所以去背誦網(wǎng)上逃逸相關(guān)的文章結(jié)論是沒有什么意義的。
使用 sync.Pool 復(fù)用堆上對(duì)象
sync.Pool 用出花兒的就是 fasthttp 了,可以看看我之前寫的這一篇:fasthttp 為什么快[7]。
最簡(jiǎn)單的復(fù)用就是復(fù)用各種 struct,slice,在復(fù)用時(shí) put 時(shí),需要判斷 size 是否已經(jīng)擴(kuò)容過頭,小心因?yàn)?sync.Pool 中存了大量的巨型對(duì)象導(dǎo)致進(jìn)程占用了大量?jī)?nèi)存。
調(diào)度占用過多 CPU
goroutine 頻繁創(chuàng)建與銷毀會(huì)給調(diào)度造成較大的負(fù)擔(dān),如果我們發(fā)現(xiàn) CPU 火焰圖中 schedule,findrunnable 占用了大量 CPU,那么可以考慮使用開源的 workerpool 來(lái)進(jìn)行改進(jìn),比較典型的 fasthttp worker pool[8]。
如果客戶端與服務(wù)端之間使用的是短連接,那么我們可以使用長(zhǎng)連接來(lái)減少連接創(chuàng)建的開銷,這里就包含了 goroutine 的創(chuàng)建與銷毀。
進(jìn)程占用大量?jī)?nèi)存
當(dāng)前大多數(shù)的業(yè)務(wù)后端服務(wù)是不太需要關(guān)注進(jìn)程消耗的內(nèi)存的。
我們經(jīng)??吹阶?Go 內(nèi)存占用優(yōu)化的是在網(wǎng)關(guān)(包括 mesh)、存儲(chǔ)系統(tǒng)這兩個(gè)場(chǎng)景。
對(duì)于網(wǎng)關(guān)類系統(tǒng)來(lái)說,Go 的內(nèi)存占用主要是因?yàn)?Go 獨(dú)特的抽象模型造成的,這個(gè)很好理解:

海量的連接加上海量的 goroutine,使網(wǎng)關(guān)和 mesh 成為 Go OOM 的重災(zāi)區(qū)。所以網(wǎng)關(guān)側(cè)的優(yōu)化一般就是優(yōu)化:
goroutine 占用的棧內(nèi)存 read buffer 和 write buffer 占用的內(nèi)存
很多項(xiàng)目都有相關(guān)的分享,這里就不再贅述了。
對(duì)于存儲(chǔ)類系統(tǒng)來(lái)說,內(nèi)存占用方面的不少努力也是在優(yōu)化各種 buffer,比如 dgraph 使用 cgo + jemalloc 來(lái)優(yōu)化他們的產(chǎn)品內(nèi)存占用[9]。
堆外內(nèi)存不會(huì)在 Go 的 GC 系統(tǒng)里進(jìn)行管轄,所以也不會(huì)影響到 Go 的 GC Heap Goal,所以不會(huì)因?yàn)榉峙浯罅繉?duì)象造成 Go 的 Heap Goal 被推高,系統(tǒng)整體占用的 RSS 也被推高。
鎖沖突嚴(yán)重,導(dǎo)致吞吐量瓶頸
我在 幾個(gè) Go 系統(tǒng)可能遇到的鎖問題[10] 中分享過實(shí)際的線上 case。
進(jìn)行鎖優(yōu)化的思路無(wú)非就一個(gè)“拆”和一個(gè)“縮”字:
拆:將鎖粒度進(jìn)行拆分,比如全局鎖,我能不能把鎖粒度拆分為連接粒度的鎖;如果是連接粒度的鎖,那我能不能拆分為請(qǐng)求粒度的鎖;在 logger fd 或 net fd 上加的鎖不太好拆,那么我們?cè)黾右恍┛蛻舳?,比如?1-> 100,降低鎖的沖突是不是就可以了。 縮:縮小鎖的臨界區(qū),業(yè)務(wù)允許的前提下,可以把 syscall 移到鎖外面;有時(shí)只是想要鎖 map 的讀寫邏輯,但是卻不小心鎖了連接讀寫的邏輯,或許簡(jiǎn)單地用 sync.Map 來(lái)代替 map Lock,defer Unlock 就能簡(jiǎn)單地縮小臨界區(qū)了。
timer 相關(guān)函數(shù)占用大量 CPU
同樣是在網(wǎng)關(guān)和海量連接的應(yīng)用中較常見,優(yōu)化手段:
使用時(shí)間輪/粗粒度的時(shí)間管理,精確到 ms 級(jí)一般就足夠了 升級(jí)到 Go 1.14+,享受官方的升級(jí)紅利
模擬真實(shí)工作負(fù)載
在前面的論述中,我們對(duì)問題進(jìn)行了簡(jiǎn)化。真實(shí)世界中的后端系統(tǒng)往往不只一個(gè)接口,壓測(cè)工具、平臺(tái)往往只支持單接口壓測(cè)。
公司的業(yè)務(wù)希望知道的是后端系統(tǒng)整體性能,即這些系統(tǒng)作為一個(gè)整體,在限定的資源條件下,能夠承載多少業(yè)務(wù)量(如并發(fā)創(chuàng)建訂單)而不崩潰。
雖然大家都在講微服務(wù),但單一服務(wù)往往也不只有單一功能,如果一個(gè)系統(tǒng)有 10 個(gè)接口(已經(jīng)算是很小的服務(wù)了),那么這個(gè)服務(wù)的真實(shí)負(fù)載是很難靠人肉去模擬的。
這也就是為什么互聯(lián)網(wǎng)公司普遍都需要做全鏈路壓測(cè)。像樣點(diǎn)的公司會(huì)定期進(jìn)行全鏈路壓測(cè)演練,以便知曉隨著系統(tǒng)快速迭代變化,系統(tǒng)整體是否出現(xiàn)了嚴(yán)重的性能衰退。
通過真實(shí)的工作負(fù)載,我們才能發(fā)現(xiàn)真實(shí)的線上性能問題。講全鏈路壓測(cè)的文章也很多,本文就不再贅述了。
當(dāng)前性能問題定位工具的局限性
本文中幾乎所有優(yōu)化手段都是通過 Benchmark 和壓測(cè)來(lái)進(jìn)行的,但真實(shí)世界的軟件還會(huì)有下列場(chǎng)景:
做 ToB 生意,我們的應(yīng)用是部署在客戶側(cè)(比如一些數(shù)據(jù)庫(kù)產(chǎn)品),客戶說我們的應(yīng)用會(huì) OOM,但是我們很難拿到 OOM 的現(xiàn)場(chǎng),不知道到底是哪些對(duì)象分配導(dǎo)致了 OOM 做大型平臺(tái),平臺(tái)上有各種不同類型的用戶編寫代碼,升級(jí)用戶代碼后,線上出現(xiàn)各種 CPU 毛刺和 OOM 問題
這些問題在壓測(cè)中是發(fā)現(xiàn)不了的,需要有更為靈活的工具和更為強(qiáng)大的平臺(tái),關(guān)于這些問題,可以看我的開源項(xiàng)目:mosn/holmes。
推薦閱讀:
cache contention[11]
every-programmer-should-know[12]
go-perfbook[13]
Systems Performance[14]
latency numbers every programmer should know: https://colin-scott.github.io/personal_website/research/interactive_latency.html
[2]rockstar thanks gta online player who fixed poor load times: https://www.pcgamer.com/rockstar-thanks-gta-online-player-who-fixed-poor-load-times-official-update-coming/
[3]how I cut GTA Online loading times by 70%: https://nee.lv/2021/02/28/How-I-cut-GTA-Online-loading-times-by-70/
[4]long stw: https://github.com/golang/go/issues/19378
[5]gc trace 阻塞調(diào)度: http://xiaorui.cc/archives/6232
[6]用 pprof 比較內(nèi)存使用差異: https://colobu.com/2019/08/20/use-pprof-to-compare-go-memory-usage/
[7]fasthttp 為什么快: https://xargin.com/why-fasthttp-is-fast-and-the-cost-of-it/
[8]fasthttp worker pool: https://github.com/valyala/fasthttp/blob/master/workerpool.go#L19
[9]內(nèi)存占用: https://dgraph.io/blog/post/manual-memory-management-golang-jemalloc/
[10]幾個(gè) Go 系統(tǒng)可能遇到的鎖問題: https://xargin.com/lock-contention-in-go/
[11]cache contention: https://web.eecs.umich.edu/~zmao/Papers/xu10mar.pdf
[12]every-programmer-should-know: https://github.com/mtdvio/every-programmer-should-know
[13]go-perfbook: https://github.com/dgryski/go-perfbook
[14]Systems Performance: https://www.amazon.com/Systems-Performance-Brendan-Gregg/dp/0136820158/ref=sr_1_1?dchild=1&keywords=systems+performance&qid=1617092159&sr=8-1
