對於一些服務來說,性能是極其重要的一環,事關系統的吞吐、訪問的延遲,進而影響用戶的體驗。
寫性能測試在Go語言中是很便捷的,go自帶的標准工具鏈就有完善的支持,下面我們來從Go的內部和系統調用方面來詳細剖析一下Benchmark這塊兒。
Benchmark
Go做Benchmar只要在目錄下創建一個_test.go后綴的文件,然后添加下面函數:
func BenchmarkStringJoin1(b *testing.B) { b.ReportAllocs() input := []string{"Hello", "World"} for i := 0; i < b.N; i++ { result := strings.Join(input, " ") if result != "Hello World" { b.Error("Unexpected result: " + result) } } }
調用以下命令:
# go test -run=xxx -bench=. -benchtime="3s" -cpuprofile profile_cpu.out
該命令會跳過單元測試,執行所有benchmark,同時生成一個cpu性能描述文件.
這里有兩個注意點:
- -benchtime 可以控制benchmark的運行時間
- b.ReportAllocs() ,在report中包含內存分配信息,例如結果是:
BenchmarkStringJoin1-4 300000 4351 ns/op 32 B/op 2 allocs/op
-4表示4個CPU線程執行;300000表示總共執行了30萬次;4531ns/op,表示每次執行耗時4531納秒;32B/op表示每次執行分配了32字節內存;2 allocs/op表示每次執行分配了2次對象。
根據上面的信息,我們就能對熱點路徑進行內存對象分配的優化,例如針對上面的程序我們可以進行小小的優化:
func BenchmarkStringJoin2(b *testing.B) { b.ReportAllocs() input := []string{"Hello", "World"} join := func(strs []string, delim string) string { if len(strs) == 2 { return strs[0] + delim + strs[1]; } return ""; }; for i := 0; i < b.N; i++ { result := join(input, " ") if result != "Hello World" { b.Error("Unexpected result: " + result) } } }
新的Benchmark結果是:
BenchmarkStringJoin2-4 500000 2440 ns/op 16 B/op 1 allocs/op
可以看出來,在減少了內存分配后,性能提升了60%以上!
Cpu Profile
上一節的benchmark結果,我們只能看到函數的整體性能,但是如果該函數較為復雜呢?然后我們又想知道函數內部的耗時,這時就該Cpu Profile登場了。
Cpu profile是Go語言工具鏈中最閃耀的部分之一,掌握了它以及memory、block profile,那基本上就沒有你發現不了的性能瓶頸了。
之前的benchmark同時還生成了一個profile_cpu.out文件,這里我們執行下面的命令:
# go tool pprof app.test profile_cpu.out Entering interactive mode (type "help" for commands) (pprof) top10 8220ms of 10360ms total (79.34%) Dropped 63 nodes (cum <= 51.80ms) Showing top 10 nodes out of 54 (cum >= 160ms) flat flat% sum% cum cum% 2410ms 23.26% 23.26% 4960ms 47.88% runtime.concatstrings 2180ms 21.04% 44.31% 2680ms 25.87% runtime.mallocgc 1200ms 11.58% 55.89% 1200ms 11.58% runtime.memmove 530ms 5.12% 61.00% 530ms 5.12% runtime.memeqbody 530ms 5.12% 66.12% 2540ms 24.52% runtime.rawstringtmp 470ms 4.54% 70.66% 2420ms 23.36% strings.Join 390ms 3.76% 74.42% 2330ms 22.49% app.BenchmarkStringJoin3B 180ms 1.74% 76.16% 1970ms 19.02% runtime.rawstring 170ms 1.64% 77.80% 5130ms 49.52% runtime.concatstring3 160ms 1.54% 79.34% 160ms 1.54% runtime.eqstring
上面僅僅展示部分函數的信息,並沒有調用鏈路的性能分析,因此如果需要完整信息,我們要生成svg或者pdf圖。
# go tool pprof -svg profile_cpu.out > profile_cpu.svg
# go tool pprof -pdf profile_cpu.out > profile_cpu.pdf
下面是profile_cpu.pdf的圖:

可以看到圖里包含了多個benchmark的合集(之前的兩段benmark函數都在同一個文件中),但是我們只關心性能最差的那個benchmark,因此需要過濾:
go test -run=xxx -bench=BenchmarkStringJoin2B$ -cpuprofile profile_2b.out go test -run=xxx -bench=BenchmarkStringJoin2$ -cpuprofile profile_2.out go tool pprof -svg profile_2b.out > profile_2b.svg go tool pprof -svg profile_2.out > profile_2.svg

根據圖片展示,benchmark自身的函數(循環之外的函數)runtime.concatstrings觸發了內存對象的分配,造成了耗時,但是跟蹤到這里,我們已經無法繼續下去了,因此下面就需要flame graphs 了。
“A flame graph is a good way to drill down your benchmarks, finding your bottlenecks #golang” via @TitPetric

如果想詳細查看,你只要點擊這些矩形塊就好。

生成這些圖,我們需要 uber/go-torch這個庫,這個庫使用了https://github.com/brendangregg/FlameGraph,下面是一個自動下載依賴,然后生成frame graph的腳本,讀者可以根據需要,自己實現。
#!/bin/bash # install flamegraph scripts if [ ! -d "/opt/flamegraph" ]; then echo "Installing flamegraph (git clone)" git clone --depth=1 https://github.com/brendangregg/FlameGraph.git /opt/flamegraph fi # install go-torch using docker if [ ! -f "bin/go-torch" ]; then echo "Installing go-torch via docker" docker run --net=party --rm=true -it -v $(pwd)/bin:/go/bin golang go get github.com/uber/go-torch # or if you have go installed locally: go get github.com/uber/go-torch fi PATH="$PATH:/opt/flamegraph" bin/go-torch -b profile_cpu.out -f profile_cpu.torch.svg
至此,我們的benchmark之路就告一段落,但是上面所述的cpu profile不僅僅能用在benchmark中,還能直接在線debug生產環境的應用性能,具體的就不詳細展開,該系列后續文章會專門講解。
完整源碼
package main import "testing" import "strings" func BenchmarkStringJoin1(b *testing.B) { b.ReportAllocs() input := []string{"Hello", "World"} for i := 0; i < b.N; i++ { result := strings.Join(input, " ") if result != "Hello World" { b.Error("Unexpected result: " + result) } } } func BenchmarkStringJoin1B(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { input := []string{"Hello", "World"} result := strings.Join(input, " ") if result != "Hello World" { b.Error("Unexpected result: " + result) } } } func BenchmarkStringJoin2(b *testing.B) { b.ReportAllocs() input := []string{"Hello", "World"} join := func(strs []string, delim string) string { if len(strs) == 2 { return strs[0] + delim + strs[1]; } return ""; }; for i := 0; i < b.N; i++ { result := join(input, " ") if result != "Hello World" { b.Error("Unexpected result: " + result) } } } func BenchmarkStringJoin2B(b *testing.B) { b.ReportAllocs() join := func(strs []string, delim string) string { if len(strs) == 2 { return strs[0] + delim + strs[1]; } return ""; }; for i := 0; i < b.N; i++ { input := []string{"Hello", "World"} result := join(input, " ") if result != "Hello World" { b.Error("Unexpected result: " + result) } } } func BenchmarkStringJoin3(b *testing.B) { b.ReportAllocs() input := []string{"Hello", "World"} for i := 0; i < b.N; i++ { result := input[0] + " " + input[1]; if result != "Hello World" { b.Error("Unexpected result: " + result) } } } func BenchmarkStringJoin3B(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { input := []string{"Hello", "World"} result := input[0] + " " + input[1]; if result != "Hello World" { b.Error("Unexpected result: " + result) } } }
原文: http://www.jianshu.com/p/cb8a95cc66f0