1 Go语言性能测试
写性能测试在Go语言中是很便捷的,go自带的标准工具链就有完善的支持。
1.1 benchmark
写benchmark测试有如下约定:
- benchmark也是测试,因此也是以
_test.go
结尾的文件; - 需要
import testing
; - 测试方法以
Benchmark
开始,并且拥有一个*testing.B
参数。
*testing.B
参数提供了大多数和*testing.T
类似的方法,同时也额外增加了一些性能测试相关的方法。此外,还提供了一个整型成员N
,用来指定被检测操作的执行次数。
举个例子,下边是一个连接字符串的操作:
bench_test.go
:
package main
import "testing"
import "strings"
func BenchmarkStringJoin1(b *testing.B) {
input := []string{"Hello", "World"}
for i := 0; i < b.N; i++ {
result := strings.Join(input, " ")
if result != "Hello World" {
b.Error("Unexpected result: " + result)
}
}
}
然后执行benchmark测试:
$ go test -bench=. -run=NONE
-bench
可以根据正则表达式筛选要测试的方法,这里的.
表示匹配所有的测试方法;-run=NONE
表示忽略功能测试。
输出如下:
goos: linux
goarch: amd64
pkg: hello/bench_test
BenchmarkStringJoin1-8 50000000 36.6 ns/op
PASS
ok hello/bench_test 1.871s
BenchmarkStringJoin1-8
表示测试了哪个方法,8
标识GOMAXPROCS
的值,通常等于CPU物理线程数;50000000
即进行了50000000次测试,基准测试运行器开始并不知道这个操作耗时长短,所以开始的时候它使用一个比较小的N值,然后根据运行情况推算出合适的N值;36.6 ns/op
表示每个操作耗时36.6纳秒。
如果希望报告更多信息,可以通过增加参数实现,如通过如下命令展示内存分配信息:
$ go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: hello/bench_test
BenchmarkStringJoin1-8 30000000 37.2 ns/op 16 B/op 1 allocs/op
PASS
ok hello/bench_test 1.159s
16 B/op
表示每次操作需要16字节的内存;1 allocs/op
表示每次操作会进行一次内存分配。
当然,也可以在测试代码中指定(这样就不需要-benchmem
参数了):
func BenchmarkStringJoin1(b *testing.B) {
b.ReportAllocs() // *testing.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 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)
}
}
}
$ go test -bench=.
goos: linux
goarch: amd64
pkg: hello/bench_test
BenchmarkStringJoin1-8 50000000 36.5 ns/op 16 B/op 1 allocs/op
BenchmarkStringJoin2-8 100000000 20.2 ns/op 0 B/op 0 allocs/op
PASS
ok hello/bench_test 3.909s
可见,优化后性能有较大提升,内存使用成本也有明显降低。
1.2 profiling
更多时候,我们不仅希望了解执行时长和内存占用情况,而是希望进行性能剖析,寻找关键代码优化点。Go语言使用pprof工具来进行性能剖析。
Go通过对执行过程进行采样,来获取profiling数据。具体来说支持如下几种性能指标:
- CPU profiling:用于识别出执行过程中需要CPU最多的函数。在每个CPU上面执行的线程每个几毫秒进行定期的中断,并在每次中断过程中记录一个性能剖析事件,然后恢复执行。
- heap profiling:用于识别出负责分配最多内存的语句。
- block profiling:用于识别出阻塞协程最久的操作,比如系统调用、通道发送、接收数据和获取锁等,性能分析库会在goroutine被这些操作阻塞的时候记录一个事件。
命令如下:
$ go test -bench . -cpuprofile=cpu.out -blockprofile=block.out -memprofile=mem.out
这个时候该pprof出马了:
$ go tool pprof -text ./bench_test.test cpu.out
File: bench_test.test
Type: cpu
Time: Jul 26, 2018 at 10:17pm (CST)
Duration: 4.10s, Total samples = 3.94s (96.01%)
Showing nodes accounting for 3.85s, 97.72% of 3.94s total
Dropped 40 nodes (cum <= 0.02s)
flat flat% sum% cum cum%
1.63s 41.37% 41.37% 2.93s 74.37% runtime.concatstrings
0.55s 13.96% 55.33% 0.68s 17.26% runtime.mallocgc
0.34s 8.63% 63.96% 0.34s 8.63% runtime.memmove
0.33s 8.38% 72.34% 2.01s 51.02% hello/bench_test.BenchmarkStringJoin2
0.24s 6.09% 78.43% 3.17s 80.46% runtime.concatstring3
0.23s 5.84% 84.26% 0.96s 24.37% runtime.rawstringtmp
0.21s 5.33% 89.59% 1.82s 46.19% hello/bench_test.BenchmarkStringJoin1
0.12s 3.05% 92.64% 1.61s 40.86% strings.Join
0.05s 1.27% 93.91% 0.05s 1.27% runtime.memclrNoHeapPointers
0.05s 1.27% 95.18% 0.73s 18.53% runtime.rawstring
0.02s 0.51% 95.69% 0.02s 0.51% runtime.(*mspan).nextFreeIndex
... ...
可以看到占用CPU时间最多的函数。
还可以输出为图片格式(不过需要先安装GraphViz,如sudo apt install graphviz
):
go tool pprof -svg ./bench_test.test cpu.out > cpu.svg
此外,还可以使用uber/go-torch
这个库生成火焰图,这个库使用了FlameGraph。准备工作如下:
git clone --depth=1 https://github.com/brendangregg/FlameGraph.git ~/.flamegraph
export PATH=$PATH:~/.flamegraph
go get github.com/uber/go-torch
然后生成火焰图:
go-torch -b cpu.out -f cpu.torch.svg
2 fabric性能剖析
关于fabric性能剖析可以参考该文档:https://github.com/hyperledger-archives/fabric/wiki/Profiling-the-Hyperledger-Fabric#other-profiling
fabric性能剖析基于net/http/pprof包,使用这个包可以让pprof运行在一个web接口上。
fabric的peer内置有profile server,默认时运行在6060端口上的,并且默认关闭。可以通过将/etc/hyperledger/fabric/core.yaml
中的peer.profile.enabled
设置为true
来启用,或者设置环境变量CORE_PEER_PROFILE_ENABLED=true
。
这里我们借助caliper项目进行测试。
在caliper项目的测试环境的docker-compose.yaml文件中配置环境变量CORE_PEER_PROFILE_ENABLED=true
。仍然以smallbank
的测试为例,则在network/fabric/simplenetwork/docker-compose.yaml下增加一个environment变量。
然后启动性能测试,在Caliper项目根目录下执行:
node benchmark/smallbank/main.js
在环境准备好之后,测试开始之初,开启另一个终端,查看一下peer节点的IP(如docker inspect $(docker ps | grep " peer0.org1.example.com" | awk '{print $1}')
命令),然后执行如下命令,收集profiling数据:
# 默认采集30秒数据
go tool pprof http://<peer-ip>:6060/debug/pprof/profile
# 定义数据采集时长
go tool pprof http://<peer-ip>:6060/debug/pprof/profile
这个命令会默认采集30秒的数据,并进入交互模式,采集的数据默认的会在~/pprof
下创建名字是pprof.XXX.samples.cpu.NNN.pb.gz
的文件。
在交互模式下,分别键入web
指令、svg
指令、top10
指令等,可以生成报告或图片。
然后生成火焰图,更加直观:
go-torch -b pprof.peer.samples.cpu.001.pb.gz -f peer.cpu.torch.svg
可以发现,CPU大部分的工作都是在进行加解密和校验,如椭圆曲线算法。