Go 内置分析工具 #
这一章节将介绍Go 内置分析工具。通过这些工具我们可以分析、诊断、跟踪竞态,GMP调度,CPU耗用等问题。
go build #
go build
命令用来编译Go 程序。go build
重要的命令行选项有以下几个:
go build -n #
-n
选项用来显示编译过程中所有执行的命令,不会真正执行。通过该选项我们可以查看编译器,连接器如何工作的:
#
# _/home/vagrant/dive-into-go
#
mkdir -p $WORK/b001/
cat >$WORK/b001/importcfg << 'EOF' # internal
# import config
packagefile fmt=/usr/lib/go/pkg/linux_amd64/fmt.a
packagefile runtime=/usr/lib/go/pkg/linux_amd64/runtime.a
EOF
cd /home/vagrant/dive-into-go
/usr/lib/go/pkg/tool/linux_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -complete -buildid RcHLBQbXBa2gQVsMR6P0/RcHLBQbXBa2gQVsMR6P0 -goversion go1.14.13 -D _/home/vagrant/dive-into-go -importcfg $WORK/b001/importcfg -pack ./empty_string.go ./string.go
/usr/lib/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cat >$WORK/b001/importcfg.link << 'EOF' # internal
packagefile _/home/vagrant/dive-into-go=$WORK/b001/_pkg_.a
packagefile fmt=/usr/lib/go/pkg/linux_amd64/fmt.a
packagefile runtime=/usr/lib/go/pkg/linux_amd64/runtime.a
packagefile errors=/usr/lib/go/pkg/linux_amd64/errors.a
packagefile internal/fmtsort=/usr/lib/go/pkg/linux_amd64/internal/fmtsort.a
packagefile io=/usr/lib/go/pkg/linux_amd64/io.a
packagefile math=/usr/lib/go/pkg/linux_amd64/math.a
packagefile os=/usr/lib/go/pkg/linux_amd64/os.a
packagefile reflect=/usr/lib/go/pkg/linux_amd64/reflect.a
packagefile strconv=/usr/lib/go/pkg/linux_amd64/strconv.a
packagefile sync=/usr/lib/go/pkg/linux_amd64/sync.a
packagefile unicode/utf8=/usr/lib/go/pkg/linux_amd64/unicode/utf8.a
packagefile internal/bytealg=/usr/lib/go/pkg/linux_amd64/internal/bytealg.a
packagefile internal/cpu=/usr/lib/go/pkg/linux_amd64/internal/cpu.a
packagefile runtime/internal/atomic=/usr/lib/go/pkg/linux_amd64/runtime/internal/atomic.a
packagefile runtime/internal/math=/usr/lib/go/pkg/linux_amd64/runtime/internal/math.a
packagefile runtime/internal/sys=/usr/lib/go/pkg/linux_amd64/runtime/internal/sys.a
packagefile internal/reflectlite=/usr/lib/go/pkg/linux_amd64/internal/reflectlite.a
packagefile sort=/usr/lib/go/pkg/linux_amd64/sort.a
packagefile math/bits=/usr/lib/go/pkg/linux_amd64/math/bits.a
packagefile internal/oserror=/usr/lib/go/pkg/linux_amd64/internal/oserror.a
packagefile internal/poll=/usr/lib/go/pkg/linux_amd64/internal/poll.a
packagefile internal/syscall/execenv=/usr/lib/go/pkg/linux_amd64/internal/syscall/execenv.a
packagefile internal/syscall/unix=/usr/lib/go/pkg/linux_amd64/internal/syscall/unix.a
packagefile internal/testlog=/usr/lib/go/pkg/linux_amd64/internal/testlog.a
packagefile sync/atomic=/usr/lib/go/pkg/linux_amd64/sync/atomic.a
packagefile syscall=/usr/lib/go/pkg/linux_amd64/syscall.a
packagefile time=/usr/lib/go/pkg/linux_amd64/time.a
packagefile unicode=/usr/lib/go/pkg/linux_amd64/unicode.a
packagefile internal/race=/usr/lib/go/pkg/linux_amd64/internal/race.a
EOF
mkdir -p $WORK/b001/exe/
cd .
/usr/lib/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=nR64Q3qx-0ZdNI4_-qJS/RcHLBQbXBa2gQVsMR6P0/RcHLBQbXBa2gQVsMR6P0/nR64Q3qx-0ZdNI4_-qJS -extld=gcc $WORK/b001/_pkg_.a
/usr/lib/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out dive-into-go
go build -race #
-race
选项用来检查代码中是否存在竞态问题。-race
可以用在多个子命令中:
go test -race mypkg
go run -race mysrc.go
go build -race mycmd
go install -race mypkg
下面是来自Go语言官方博客的一个示例1,在该示例中演示了使用-race
选项检查代码中的竞态问题:
func main() {
start := time.Now()
var t *time.Timer
t = time.AfterFunc(randomDuration(), func() {
fmt.Println(time.Now().Sub(start))
t.Reset(randomDuration())
})
time.Sleep(5 * time.Second)
}
func randomDuration() time.Duration {
return time.Duration(rand.Int63n(1e9))
}
上面代码完成的功能是通过time.AfterFunc
创建定时器,该定时器会在randomDuration()
时候打印消息,此外还会通过Rest()
方法重置该定时器,以达到重复利用该定时器目的。
当我们使用-race
选项执行检查时候,可以发现上面代码是存在竞态问题的:
$ go run -race main.go
==================
WARNING: DATA RACE
Read by goroutine 5:
main.func·001()
race.go:14 +0x169
Previous write by goroutine 1:
main.main()
race.go:15 +0x174
Goroutine 5 (running) created at:
time.goFunc()
src/pkg/time/sleep.go:122 +0x56
timerproc()
src/pkg/runtime/ztime_linux_amd64.c:181 +0x189
==================
go build -gcflags #
-gcflags
选项用来设置编译器编译时参数,支持的参数有:
- -N选项指示禁止优化
- -l选项指示禁止内联
- -S选项指示打印出汇编代码
- -m选项指示打印出变量变量逃逸信息,
-m -m
可以打印出更丰富的变量逃逸信息
-gcflags
支持只在编译特定包时候才传递编译参数,此时的-gcflags
格式为包名=参数列表
。
go build -gcflags="-N -l -S" main.go // 打印出main.go对应的汇编代码
go build -gcflags="log=-N -l" main.go // 只对log包进行禁止优化,禁止内联操作
go tool compile #
go tool compile
命令用于汇编处理Go 程序文件。go tool compile
支持常见选项有:
- -N选项指示禁止优化
- -l选项指示禁止内联
- -S选项指示打印出汇编代码
- -m选项指示打印出变量内存逃逸信息
go tool compile -N -l -S main.go # 打印出main.go对应的汇编代码
GOOS=linux GOARCH=amd64 go tool compile -N -l -S main.go # 打印出针对特定系统和CPU架构的汇编代码
go tool nm #
go tool nm
命令用来查看Go 二进制文件中符号表信息。
go tool nm ./main | grep "runtime.zerobase"
go tool objdump #
go tool objdump
命令用来根据目标文件或二进制文件反编译出汇编代码。该命令支持两个选项:
- -S选项指示打印汇编代码
- -s选项指示搜索相关的汇编代码
go tool compile -N -l main.go # 生成main.o
go tool objdump main.o # 打印所有汇编代码
go tool objdump -s "main.(main|add)" ./test # objdump支持搜索特定字符串
go tool trace #
GODEBUG环境变量 #
GODEBUG
是控制运行时调试的变量,其参数以逗号分隔,格式为:name=val。GODEBUG
可以用来观察GMP调度和GC过程。
GMP调度 #
与GMP调度相关的两个参数:
-
schedtrace:设置 schedtrace=X 参数可以使运行时在每 X 毫秒输出一行调度器的摘要信息到标准 err 输出中。
-
scheddetail:设置 schedtrace=X 和 scheddetail=1 可以使运行时在每 X 毫秒输出一次详细的多行信息,信息内容主要包括调度程序、处理器、OS 线程 和 Goroutine 的状态。
我们以下面代码为例:
package main
import (
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 2000; i++ {
wg.Add(1)
go func() {
a := 0
for i := 0; i < 1e6; i++ {
a += 1
}
wg.Done()
}()
time.Sleep(100 * time.Millisecond)
}
wg.Wait()
}
执行以下代码获取GMP调度信息:
GODEBUG=schedtrace=1000 go run ./test.go
笔者本人电脑输出以下内容:
SCHED 0ms: gomaxprocs=8 idleprocs=6 threads=4 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0]
SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=3 spinningthreads=1 idlethreads=0 runqueue=0 [1 0 0 0 0 0 0 0]
SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=5 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0]
SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=5 spinningthreads=2 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0]
SCHED 1007ms: gomaxprocs=8 idleprocs=8 threads=16 spinningthreads=0 idlethreads=9 runqueue=0 [0 0 0 0 0 0 0 0]
SCHED 1000ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
SCHED 2018ms: gomaxprocs=8 idleprocs=8 threads=16 spinningthreads=0 idlethreads=9 runqueue=0 [0 0 0 0 0 0 0 0]
上面输出内容解释说明:
- SCHED XXms: SCHED是调度日志输出标志符。XXms是自程序启动之后到输出当前行时间
- gomaxprocs: P的数量,等于当前的 CPU 核心数,或者GOMAXPROCS环境变量的值
- idleprocs: 空闲P的数量,与gomaxprocs的差值即运行中P的数量
- threads: 线程数量,即M的数量
- spinningthreads:自旋状态线程的数量。当M没有找到可供其调度执行的 Goroutine 时,该线程并不会销毁,而是出于自旋状态
- idlethreads:空闲线程的数量
- runqueue:全局队列中G的数量
- [0 0 0 0 0 0 0 0]:表示P本地队列下G的数量,有几个P中括号里面就会有几个数字
GC #
与 GC 相关的参数是 gctrace,当设置为1时候,会输出GC相关信息到标准错误输出。使用方式示例如下:
GODEBUG=gctrace=1 go run main.go
GC 时候输出的内容格式如下:
gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # P
格式解释说明如下:
- gc #:GC 编号,每次 GC 时递增
- @#s:程序自启动以来的时间(单位秒)
- #%:程序自启动以来花费在 GC 上的时间百分比
- #+…+#:GC 各阶段花费的时间,分别为单个P的墙上时间和累计CPU时间
- #->#-># MB:分别表示 GC 启动时, GC 结束时, GC 活动时的堆大小
- #MB goal:下一次触发 GC 的内存占用阈值
- #P:当前使用的处理器P的数量
比如对于下面的输出内容,详细解释如下:
gc 100 @0.904s 11%: 0.043+2.8+0.029 ms clock, 0.34+3.4/5.4/13.6+0.23 ms cpu, 10->11->6 MB, 12 MB goal, 8 P
- gc 100:第 100 次 GC
- @0.904s:当前时间是程序启动后的0.904s
- 11%:程序启动后到现在共花费 11% 的时间在 GC 上
- 0.043+2.8+0.029 ms clock
- 0.043:表示单个 P 在 mark 阶段的 STW 时间
- 2.8:表示所有 P 的 concurrent mark(并发标记)所使用的时间
- 0.029:表示单个 P 的 markTermination 阶段的 STW 时间
- 0.34+3.4/5.4/0+0.23 ms cpu
- 0.34:表示整个进程在 mark 阶段 STW 停顿的时间,一共0.34秒,即 0.043 * 8
- 3.4/5.4/13.6:3.4 表示 mutator assist 占用的时间,5.4 表示 dedicated + fractional 占用的时间,13.6 表示 idle 占用的时间。这三块累计时间为22.4,即2.8 * 8
- 0.23 ms:0.23 表示整个进程在 markTermination 阶段 STW 时间,即0.029 * 8
- 10->11->6 MB
- 10:表示开始 mark 阶段前的 heap_live 大小
- 11:表示开始 markTermination 阶段前的 heap_live 大小
- 6:表示被标记对象的大小
- 12 MB goal:表示下一次触发 GC 回收的阈值是 12 MB
- 8 P:本次 GC 一共涉及8 个P
GOGC参数 #
Go语言GC相关的另外一个参数是GOGC。GOGC 用于控制GC的处发频率, 其值默认为100, 这意味着直到自上次垃圾回收后heap size已经增长了100%时GC才触发运行,live heap size每增长一倍,GC触发运行一次。若设定GOGC=200, 则live heap size 自上次垃圾回收后,增长2倍时,GC触发运行, 总之,其值越大则GC触发运行频率越低, 反之则越高。如果GOGC=off 则关闭GC。
# 表示当前应用占用的内存是上次GC时占用内存的两倍时,触发GC
export GOGC=100