视频信息

Seven ways to Profile Go Applications
by Dave Cheney
at Golang UK Conf. 2016

方法一:time

shell 内置的 time

最简单的性能测试工具就是 shell 中内置的 time 命令,这是由 POSIX.2 (IEEE Std 1003.2-1992) 标准定义的,因此所有 Unix/Linux 都有这个内置命令。

$ time go fmt github.com/docker/machine
 
real 0m0.110s
user 0m0.056s
sys 0m0.040s

这是使用shell内置的 time来对 go fmt github.com/docker/machine 的命令进行性能分析。

这里一共有3项指标:

  • real:从程序开始到结束,实际度过的时间;
  • user:程序在用户态度过的时间;
  • sys:程序在内核态度过的时间。

一般情况下 real >= user + sys,因为系统还有其它进程。

GNU 实现的 time

除此以外,对于 Linux 系统,还有一套 GNU 的 time,位于 /usr/bin/time,需要用完整路径去调用,不过这个功能就更强大了。

vagrant@vagrant:~$ /usr/bin/time -v go fmt github.com/docker/machine
Command being timed: "go fmt github.com/docker/machine"
User time (seconds): 0.02
System time (seconds): 0.06
Percent of CPU this job got: 85%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.09
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 18556
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 9925
Voluntary context switches: 430
Involuntary context switches: 121
Swaps: 0
File system inputs: 0
File system outputs: 32
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0

可以看到这里的功能要强大多了,除了之前的信息外,还包括了:

  • CPU占用率;
  • 内存使用情况;
  • Page Fault 情况;
  • 进程切换情况;
  • 文件系统IO;
  • Socket 使用情况;
  • ……

*BSD、macOS 的 time

*BSD 也有自己实现的 time,功能稍逊,但也比 Shell 里的 time 强大。比如 macOS 中继承自 FreeBSD 的 time

$ /usr/bin/time -l go fmt github.com/docker/machine
0.70 real 0.05 user 0.40 sys
11710464 maximum resident set size
0 average shared memory size
0 average unshared data size
0 average unshared stack size
8579 page reclaims
2571 page faults
0 swaps
0 block input operations
0 block output operations
0 messages sent
0 messages received
3 signals received
1118 voluntary context switches
1702 involuntary context switches
$

这里有:

  • 内存使用情况
  • Page Fault 情况
  • IO 情况
  • 进程切换情况
  • Signal 情况
  • ……

go tool 中的 -toolexec 参数

当我们构建很慢的时候,如何才能知道为什么慢呢?go 工具链中支持 -x 命令,可以显示具体执行的每一条命令,这样我们就可以看到到底执行到哪里的时候慢了。

$ go build -x fmt
WORK=/var/folders/wc/9tzsn1hd7c38tvc54kctn4100000gn/T/go-build846067626
mkdir -p $WORK/runtime/internal/sys/_obj/
mkdir -p $WORK/runtime/internal/
cd /usr/local/Cellar/go/1.9.1/libexec/src/runtime/internal/sys
/usr/local/Cellar/go/1.9.1/libexec/pkg/tool/darwin_amd64/compile -o $WORK/runtime/internal/sys.a -trimpath $WORK -goversion go1.9.1 -p runtime/internal/sys -std -+ -complete -buildid 2749cc50ea3a4ebcf
...

但是如果构建时间很长,或者是计划在 CI 中运行,我们就不可能一直盯着了。当然,我们可以时候从输出中复制粘贴到命令行,前缀上 time,也可以知道每个命令的执行时间。不过这太繁琐了。

go tool 工具链中,还支持一个叫做 -toolexec 的参数,其值将作为工具链每一个命令的前缀来执行。

go build -toolexec=... github.com/pkg/profile
go test -toolexec=... net/http
...

换句话说,如果 -toolexec=time,那么假如有一个 go build xxx.go 的命令,就会变为 time go build xxx.go 来执行。

$ go build -toolexec="/usr/bin/time" cmd/compile/internal/gc
# runtime/internal/sys
0.09 real 0.01 user 0.02 sys
# runtime/internal/atomic
0.01 real 0.00 user 0.00 sys
# runtime/internal/atomic
0.02 real 0.00 user 0.00 sys
# runtime
1.60 real 1.90 user 0.12 sys
# runtime
0.00 real 0.00 user 0.00 sys
# runtime
0.02 real 0.01 user 0.00 sys
# runtime
0.01 real 0.00 user 0.00 sys
# runtime
0.00 real 0.00 user 0.00 sys
...

用好了,这就可以变得很强大,不仅仅是计时。比如,我们 go build 的时候我们可以在 Mac 或者 Linux 上进行交叉编译,但是 go test 的时候,我们希望则在手机设备上直接运行。另外,也可以用来校验输出结果的一致性 (toolstash

方法二:GODEBUG

/usr/bin/time 是外部工具,除此外,我们还可以使用 Go 内置的功能。Go 的 runtime 可以收集程序运行周期内的很多数据。当然,这些收集默认都是不启用的,你可以手动启用特定信息的收集。

比如,如果你关心垃圾收集,则可以启用 gctrace=1 标志。如:

$ env GODEBUG=gctrace=1 godoc -http=:8080
gc 1 @18446741350.644s 0%: 0.026+2.0+0.075 ms clock, 0.052+2.6/2.0/0+0.15 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
gc 2 @18446741350.664s 0%: 0.12+1.5+0.049 ms clock, 0.25+0.50/1.2/0+0.098 ms cpu, 4->4->1 MB, 5 MB goal, 4 P
gc 3 @18446741350.695s 0%: 0.024+1.1+0.059 ms clock, 0.072+1.3/0.96/0+0.17 ms cpu, 4->4->1 MB, 5 MB goal, 4P
gc 4 @18446741350.714s 0%: 0.036+1.8+0.092 ms clock, 0.11+1.4/1.7/0+0.27 ms cpu, 4->4->1 MB, 5 MB goal, 4 P
gc 5 @18446741350.746s 0%: 0.021+2.2+0.055 ms clock, 0.087+2.5/2.1/0+0.22 ms cpu, 4->4->1 MB, 5 MB goal, 4 P
gc 6 @18446741350.770s 0%: 0.013+4.5+0.12 ms clock, 0.053+1.3/3.9/0+0.50 ms cpu, 4->4->1 MB, 5 MB goal, 4 P
gc 7 @18446741350.800s 0%: 0.020+2.5+0.056 ms clock, 0.083+2.4/2.5/0+0.22 ms cpu, 4->4->2 MB, 5 MB goal, 4 P
gc 8 @18446741350.817s 0%: 0.030+3.2+0.053 ms clock, 0.12+2.8/3.0/0+0.21 ms cpu, 4->4->2 MB, 5 MB goal, 4 P
gc 9 @18446741350.845s 0%: 0.041+4.7+0.10 ms clock, 0.16+1.6/4.3/0+0.40 ms cpu, 4->4->2 MB, 5 MB goal, 4 P
gc 10 @18446741350.881s 0%: 0.018+3.7+0.070 ms clock, 0.072+2.5/3.6/0+0.28 ms cpu, 4->4->2 MB, 5 MB goal, 4
...

这样的话,垃圾收集的信息都会被输出出来,可以帮助 GC 排障。如果发现 GC 一直都在很忙碌的工作,那恐怕内存管理上有可以改进的地方。

插曲一:Profiler 是如何工作的?

Profiler 会启动你的程序,然后通过配置操作系统,来定期中断程序,然后进行采样。比如发送 SIGPROF 信号给被分析的进程,这样进程就会被暂停,然后切换到 Profiler 中进行分析。Profiler 则取得被分析的程序的每个线程的当前位置等信息进行统计,然后恢复程序继续执行。

性能分析注意事项