Go语言内置分析工具

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

进一步阅读 #