bigywater.tech
Go · 69

执行追踪 go tool trace:goroutine 在哪里等了多久

CPU profile 看不到 goroutine 在等什么。执行追踪记录每个 goroutine 的阻塞、唤醒、调度,可以回答“它在哪里等了多久”。这篇不用网页界面,用命令行把 trace 导成 pprof 和事件文本,并用程序自己的计时校验 trace 给出的数字。

314.33 ms
4 个 goroutine 争锁时,trace 的 sync 阻塞 profile 归到 Mutex.Lock 的等待
4690 vs 1906 ms
20 个 goroutine 排队:程序自己量到的排队时间对 sched profile 的总量(单次运行)
+22%
开着 trace 时,分配密集的小程序耗时增加

sync 阻塞 profile

实测 · 本机执行输出
  trace 文件 5592 字节
  Type: delay
  Showing nodes accounting for 537.23ms, 100% of 537.23ms total
        flat  flat%   sum%        cum   cum%
    314.33ms 58.51% 58.51%   314.33ms 58.51%  sync.(*Mutex).Lock
    122.83ms 22.86% 81.37%   122.83ms 22.86%  sync.(*WaitGroup).Wait
    100.08ms 18.63%   100%   100.08ms 18.63%  runtime.chanrecv1
           0     0%   100%   314.33ms 58.51%  main.holdLock
           0     0%   100%   122.83ms 22.86%  main.main
           0     0%   100%   314.33ms 58.51%  main.main.func2
           0     0%   100%   100.08ms 18.63%  runtime.(*wakeableSleep).sleep
           0     0%   100%   100.08ms 18.63%  runtime.traceStartReadCPU.func1

go tool trace -pprof=sync 把 goroutine 阻塞在同步原语上的时间按调用栈汇总。4 个 goroutine 争锁的例子里,Mutex.Lock 记了 314.33 ms、归到 main.holdLock;另外混进了运行时自己的 goroutine 的 100 ms(traceStartReadCPU),读这类结果要认得哪些是运行时内部的项。

sched profile 要小心

实测 · 本机执行输出
  程序自己量的:20 个 goroutine 从创建到第一次运行的等待之和 3559 ms;从创建到结束的总时间之和 5090 ms,减去它们各自的 20 ms 空转 = 4690 ms 的排队时间
  trace 文件 5157 字节
  Type: delay
  Showing nodes accounting for 1531.07ms, 80.32% of 1906.32ms total
  Dropped 7 nodes (cum <= 9.53ms)
  Showing top 8 nodes out of 9
        flat  flat%   sum%        cum   cum%
   1155.78ms 60.63% 60.63%  1155.78ms 60.63%  time.Now
    375.28ms 19.69% 80.32%   375.28ms 19.69%  runtime.wakeNetPoller
           0     0% 80.32%  1155.79ms 60.63%  main.main.func3
           0     0% 80.32%  1155.78ms 60.63%  main.spin
           0     0% 80.32%   375.28ms 19.69%  runtime.(*timer).maybeAdd
           0     0% 80.32%   375.28ms 19.69%  runtime.(*timer).modify
           0     0% 80.32%   375.28ms 19.69%  runtime.(*timer).reset
           0     0% 80.32%   375.28ms 19.69%  runtime.(*traceAdvancerState).start.func1

20 个 goroutine 在一个 P 上排队,程序自己量出来的排队时间是 4690 ms,而 -pprof=sched 的总量只有 1906 ms(另外几次是 804、1353 ms)。为什么偏小又不稳定,我没有查清。把它当作“哪个栈等得多”的相对排序用,不要当作精确的总时间。

用任务与区域标注业务

实测 · 本机执行输出
  go tool trace -d=parsed 总共 485 行事件;与标注相关的 9 行,前 8 行:
    M=8304148352 P=8 G=1 TaskBegin Time=395507253352320 ID=1 Parent=18446744073709551615 Type="请求"
    M=8304148352 P=8 G=1 RegionBegin Time=395507253353280 Task=1 Type="解析"
    M=6094778368 P=8 G=1 RegionEnd Time=395507265370048 Task=1 Type="解析"
    M=6094778368 P=8 G=1 RegionBegin Time=395507265371072 Task=1 Type="查询"
    M=6094778368 P=9 G=1 RegionEnd Time=395507277410496 Task=1 Type="查询"
    M=6094778368 P=9 G=1 RegionBegin Time=395507277411584 Task=1 Type="渲染"
    M=8304148352 P=9 G=1 RegionEnd Time=395507289457280 Task=1 Type="渲染"
    M=8304148352 P=9 G=1 Log Time=395507289460800 Task=1 Category="category" Message="done"
  区域 解析:12.02 ms(代码里是 time.Sleep(10ms))
  区域 查询:12.04 ms(代码里是 time.Sleep(10ms))
  区域 渲染:12.05 ms(代码里是 time.Sleep(10ms))

trace.NewTask / trace.WithRegion 把你关心的阶段写进 trace,go tool trace -d=parsed 里能直接读到 Begin/End 的纳秒时间戳。三个各睡 10 ms 的步骤读出来是 12.03、12.03、12.01 ms,和之前量到的 Sleep 多睡约 2 ms 一致。

代价

实测 · 本机执行输出
  plain  :耗时 最小 0.040 s / 中位 0.041 s / 最大 0.042 s
  traced :耗时 最小 0.048 s / 中位 0.050 s / 最大 0.050 s;trace 文件 0.9–0.9 MB

一个 200 个 goroutine、每个分配 400 次的小程序,开 trace 后中位耗时从 0.041 s 到 0.050 s,约慢 22%,生成 0.9 MB,折合每秒约 18 MB。真实服务的开销取决于事件密度,我只测了这一个例子。

四种命令行 profile

实测 · 本机执行输出
  execution tracer.
  # Tracing runtime activities
  The execution trace captures a wide range of execution events such as goroutine
  creation/blocking/unblocking, syscall enter/exit/block, GC-related events,
  changes of heap size, processor start/stop, etc. When CPU profiling is active,
  the execution tracer makes an effort to include those samples as well. A precise
  nanosecond-precision timestamp and a stack trace is captured for most events.
  The generated trace can be interpreted using `go tool trace`.
  - net: network blocking profile
  - sync: synchronization blocking profile
  - syscall: syscall blocking profile
  - sched: scheduler latency profile

-pprof= 支持 net、sync、syscall、sched 四种。本文只实验了 sync 与 sched;net 与 syscall 没有试。网页界面(火焰图式的时间线)需要浏览器,这里没有使用。

分步演示

执行追踪
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。程序分别用 runtime/trace 写出 trace 文件,再用 go tool trace -pprof=… 与 -d=parsed 读取;程序自己的排队计时在每个 goroutine 里用 time.Since(created) 记录;区域时长由 -d=parsed 里的 RegionBegin/RegionEnd 时间戳相减;开销对比各 5 次;读过的文档:go doc runtime/trace,go tool trace -h
    • 没有验证:网页界面(时间线/goroutine 分析)、-pprof=net 与 -pprof=syscall、sched profile 总量偏小的原因、trace 对真实服务的影响、飞行记录器(flight recorder)、Linux 上的结果
    京ICP备2026067502号-1京公网安备11010502063487号