go tool trace 使用指南(Go 1.24)

一、trace 与 pprof 的本质区别

pprof 是聚合统计 :一万个样本按调用栈折叠成一张表,时间维度被抹掉。 trace 是全量事件流:每个 goroutine 的每次状态切换(创建/阻塞/唤醒/运行/让出/结束)、每次 syscall 进出、每次 GC 阶段、每次 P 启停,都带纳秒时间戳和调用栈记录在案。

ini 复制代码
pprof:  f() ──► 3200 个样本 ──► "f 占了 32% CPU"          (平均,无时间轴)
trace:  t=1000123ns G7 Runnable → Running (P2)
        t=1000987ns G7 Running → Syscall (read)
        t=1002455ns G7 Syscall → Runnable  (等了 1.5ms!)   (每个事件都有)

一句话:pprof 告诉你"哪里烧时间",trace 告诉你"每一毫秒发生了什么"。所以:

  • 稳态性能问题(CPU 高、分配多)→ 先 pprof;
  • 时序问题(偶发延迟、尾延迟、卡顿、调度不公平、GC 影响)→ trace。

图:trace 与 pprof 的本质区别:聚合统计 vs 全量事件流

二、三种采集方式

方式 1:HTTP 端点(线上常用)

bash 复制代码
curl -o trace.out "http://localhost:6060/debug/pprof/trace?seconds=5"
go tool trace trace.out
  • seconds 不传默认 1 秒 (1.24 源码 net/http/pprof/pprof.go:172-175,注意与 CPU profile 的默认 30 秒不同)。
  • 采集期间数据流式写回,configureWriteDeadline 会把 HTTP 写超时自动加长 seconds(pprof.go:123-131),不会因服务器的 WriteTimeout 截断。

方式 2:程序内显式 API(离线程序常用)

go 复制代码
package main

import (
	"os"
	"runtime/trace"
)

func main() {
	f, err := os.Create("trace.out")
	if err != nil { panic(err) }
	defer f.Close()

	if err := trace.Start(f); err != nil { panic(err) } // 已在采集则返回错误
	defer trace.Stop()                                  // Stop 会等所有数据写完才返回

	// ... 业务代码
}

1.24 公共 API 的 Start 实现(runtime/trace/trace.go:121-139)值得看一眼,它会:

  1. 调 runtime.StartTrace() 让 runtime 开始产数据;
  2. 另起一个 goroutine 死循环 runtime.ReadTrace() ,把分块数据搬给你的 io.Writer------这就是为什么"trace 数据是流式的,不是一次性快照"。

Stop() 只在全部数据落盘后 返回(trace.go:141-149),这就是"优雅退出不丢尾部数据"的保证。

方式 3:go test

bash 复制代码
go test -trace=trace.out ./...
go tool trace trace.out

想看单个 benchmark 的 trace 时,用方式 2 包在测试代码里也可以(Start/Stop 夹住被测段)。

图:trace 的三种采集方式

三、打开 viewer:首页地图

bash 复制代码
go tool trace trace.out
# 2026/11/01 11:20:00 Preparing trace for viewer...
# 2026/11/01 11:20:02 Opening browser (this may take a while)

它会在本机起一个 HTTP 服务并打开浏览器。首页(internal/trace/traceviewer/http.go)包含:

区块 内容
View trace 时间线主视图(下节精读),可按 thread / GC / 代(generation)等维度切换
Goroutine analysis 按创建栈分组的 goroutine 耗时分析(第五节)
Network blocking profile 网络等待的 pprof 图(/io)
Synchronization blocking profile channel/mutex 等同步等待(/block)
Syscall profile 系统调用阻塞(/syscall)
Scheduler latency profile 调度等待(就绪→运行的间隔)(/sched)
User-defined tasks / regions 你用 API 标注的任务/区间直方图
Garbage collection metrics GC 时长统计 + MMU 图(最小 mutator 利用率)

后四个 profile 均可 ?raw=1 下载成标准 pprof 文件,离线用 go tool pprof 打开------trace 也能产出 pprof 数据,这是两个工具的交汇点。

图:trace viewer 首页地图

四、时间线视图精读

"View trace" 打开的是双窗格时间线:

less 复制代码
┌─ 上窗格:宏观总览(整条 trace 压缩成一行,颜色块=事件密度) ──────────┐
│  ▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
└────────────────────────▲拖动选窗口────────────────────────┘
┌─ 下窗格:微观明细 ────────────────────────────────────────────┐
│ Threads    │ T1 ████████░░░░████████████                     │
│ Proc 0..N  │ P0 ██ G7 ███████ ██ G9 ████████████            │  ← 每行一个 P
│ Goroutines │ G7 ▁▁▂▁▁▁▁▁▁▂▁ (事件密度,点开看单个 G)        │
│ Heap       │     ╱╲    ╱╲   ╱╲  ← 堆大小曲线                │
│ OS Threads / Runtime │ ██STW██   █GC█  ← runtime 阶段        │
└──────────────────────────────────────────────────────────────┘

读法三步:

  1. 先看堆曲线与 GC/STW 区块:毛刺时刻是否有 GC/STW 对齐?heap 是否锯齿异常?
  2. 看 P 行的颜色段:每个 P 上显示当前运行的 G 序号。大量 P 同时空白(白段)→ CPU 没吃满,去看 goroutine 状态;P 全满但延迟高 → CPU 瓶颈,回 pprof。
  3. 点开具体 goroutine :从 Goroutine analysis 或点击 P 上的 G 跳转,视图切换为"以该 G 为中心,只显示与它有阻塞/唤醒关系的 G"------顺着 unblock 关系可以人肉追出一条唤醒链(goroutines.go:171-176 页面上的 goroutine 链接即此)。

WASD/V 快捷键:W/S 缩放,A/D 平移------大 trace 必备。数字键切换窗格焦点。

图:时间线视图:上窗格宏观总览,下窗格微观明细

五、Goroutine analysis:六类耗时桶

按创建栈分组后,每个 goroutine 的生命周期被拆进互不重叠 的耗时桶(internal/trace/summary.go:122-136,1.24 实际类别):

桶 含义 异常时说明什么
Execution time 真正在 CPU 上跑 占比高 → CPU 瓶颈,转 pprof
Sched wait time Runnable → Running 的排队时间 高 → G 多 P 少/CPU 饱和/被抢占,GOMAXPROCS 或任务切分问题
Syscall execution time 系统调用自身执行 高 → IO/文件系统慢
Block time (syscall) 因系统调用阻塞(让出 P 后仍未就绪) 高 → 系统调用慢且频繁
Block time (原因) 同步阻塞,按原因细分:chan receive、chan send、select、sync.Mutex.Lock、sync.WaitGroup.Wait、GC assist... 直接指认等待点
Unknown time 以上都对不上的残余 通常很小;偏大说明有未被建模的状态(如 cgo)

表格中每个 goroutine 有一根堆叠条形图,颜色按桶占比------一眼扫出"整排都是 Sched wait"的分组,就是调度饥饿的群体。

分组页还有四个可下载的 pprof:Network wait、Sync block、Syscall、Scheduler wait(每组按创建栈过滤,goroutines.go:325-339)。

图:Goroutine analysis 的六类互不重叠耗时桶

六、四类延迟 profile 与 GC 页面

首页四个延迟 profile 的定义(traceviewer/http.go:187-190 原文):"goroutine 无法在逻辑处理器上运行的四种原因"------等网络、等同步原语、等系统调用、等可用 P。

用法与第 01 篇的 pprof 完全一致(它们就是 pprof 格式):

bash 复制代码
curl -o sched.profile "http://127.0.0.1:PORT/sched?raw=1"
go tool pprof -http=:8080 sched.profile

与 runtime 的 block/mutex profile 的区别 :trace 版把"就绪但没跑"单独拎出来(scheduler latency),而 runtime 的 block profile 只看同步原语。尾延迟问题 80% 的入口是 Scheduler latency profile------先看谁在排队,再回时间线找原因。

GC 页面 :GC 次数、每次 STW/mark 时长;MMU 图(Minimum Mutator Utilization)回答"任意 X 毫秒窗口内,应用代码至少能占到百分之多少 CPU"------GC 越凶,曲线越低。这是评估"GC 对延迟敏感服务影响"的量化工具。

图:四类延迟 profile = goroutine 无法在 P 上运行的四种原因

七、用户标注 API:task / region / log

三级标注,粒度从粗到细(全部在 runtime/trace 包,annotation.go):

7.1 Log:瞬时事件

go 复制代码
trace.Log(ctx, "orderID", orderID)                    // 总是记录
trace.Logf(ctx, "timing", "stage2 took %v", elapsed)   // 仅在开启采集时才格式化(省开销)

Logf 先查 IsEnabled() 才做 Sprintf(annotation.go:101-108)------高频路径用 Logf,低频用 Log。

7.2 Region:同 goroutine 内的时间区间

go 复制代码
// 闭包式(推荐,难漏 End)
trace.WithRegion(ctx, "extractCoffee", extractCoffee)

// 手动式:StartRegion 必须在同一个 goroutine 里 End,且区间必须正确嵌套
r := trace.StartRegion(ctx, "parse")
defer r.End()

两条硬规则(源码注释 annotation.go:146-148):同 goroutine 内必须嵌套 (后开的必须先关);未开启采集时 StartRegion 返回 noop 实例,零成本。

7.3 Task:跨 goroutine 的逻辑操作

go 复制代码
ctx, task := trace.NewTask(ctx, "makeCappuccino") // 挂进 context,顺着 ctx 传给所有参与者
defer task.End()                                   // 延迟 = NewTask 到第一次 End 的时长

go func() { // 子任务:ctx 里带着 task,自动关联
	trace.WithRegion(ctx, "steamMilk", steamMilk)
	milk <- true
}()
  • task 通过 context.Context 传播:任何拿着这个 ctx 的 goroutine 里的 region/log 都归属该 task。
  • viewer 的 "User-defined tasks" 页给出每类 task 的延迟直方图,点开能看到该 task 的完整事件序列(谁创建、在哪等、日志打了什么)------这就是"一次请求"级别的追踪。
  • NewTask 即使未开启采集也会分配 ID (annotation.go:45-48 的注释解释了原因:ctx 可能跨越采集的开关边界)。
  • taskType/regionType 的取值集合必须有限(源码注释明确要求 "a handful of unique")------别把订单号当 taskType,那是 Log 干的事。基数爆炸会拖垮 viewer。

图:用户标注 API:task / region / log 三级

八、实战手册:四类问题的 trace 侧排查

8.1 偶发延迟毛刺(P99 高)

  1. 毛刺时刻前后抓 5~10 秒 trace;
  2. 时间线对齐毛刺时刻,看是否有 STW / GC mark 区块重叠;
  3. 看 runnable 队列(Goroutines 行突然变高)→ 调度饥饿;
  4. Scheduler latency profile 找排队大户 → 回时间线点开它,看它是被谁阻塞后唤醒晚了。

8.2 吞吐上不去但 CPU 闲

时间线上 P 大量空白 + goroutine 全在 Block time (chan receive) → 管道串行化瓶颈;Block time (sync.Mutex.Lock) → 锁竞争(配合第 01 篇 mutex profile 定位持锁者)。

8.3 GC 压力评估

GC 页看 STW/mark 时长与频率;MMU 图看 100ms 窗口利用率;heap 曲线锯齿的斜率 = 分配速率,斜率陡 → 回 pprof 的 allocs 灭火。

8.4 系统调用 / cgo 慢

Goroutine analysis 的 Syscall 两桶异常大;时间线上看 G 的 Running → Syscall 段长度。cgo 调用在 1.24 trace 里同样以 syscall 阻塞形态出现(且常伴随线程不归还 P 的现象)。

九、开销、格式与版本注意事项

  • 开销 :Go 1.21 全面重写 tracer(frame pointer 回溯 + 流式落盘),大多数服务开销在 1~2% CPU 以内;1.22 起高并发下的伸缩性进一步改善。可以常开短窗,但别当 7×24 全量记录用------文件大小与事件数成正比,繁忙服务每分钟可达数百 MB。
  • 采集时长建议:10~30 秒封顶,够看问题了。
  • 格式版本 :1.21+ 是新格式 go 1.22 trace 版本头;go tool trace 向下兼容旧格式(读旧文件走 internal/trace/oldtrace.go 的兼容读路径),但旧 Go 的 viewer 读不了新格式。
  • 1.24 还没有飞行记录仪 :trace.FlightRecorder(常驻环形缓冲、事后取最近片段)是 Go 1.25 加入的;1.24 上等价做法是定时短窗采集(如每 5 分钟抓 10 秒滚动覆盖)。
  • trace 与 pprof 同开:完全支持,而且 CPU 采样会被合入 trace(时间线上能看到样本)

图:开销 / 格式 / 版本注意事项(以 1.24 为基准)

十、常见坑 Top 8

# 现象 原因 → 解法
1 trace.out 是空的/残缺 trace.Stop() 没等完就退出了 → 用 defer,HTTP 端点等请求自然结束
2 viewer 打不开、浏览器卡死 trace 太大(几 GB)→ 缩短采集;用 Goroutine analysis 页代替全量时间线
3 大量 "Unknown time" 存在 cgo/长系统调用等未建模状态 → 结合 /syscall profile 看
4 task/region 页面巨卡 taskType 基数爆炸 → 类型用有限集合,动态值走 trace.Log
5 StartRegion 的 End 崩溃/数据乱 跨 goroutine 调 End 或未嵌套 → 换 WithRegion 闭包式
6 goroutine 在时间线上找不到 新版按事件密度渲染 → 用 Goroutine analysis 里的 /trace?goid=N 直达
7 旧 Go 版本看新 trace 报错 新旧格式不兼容 → 用匹配版本的 go tool trace
8 采集时服务抖动 秒级参数太大或磁盘慢 → 秒数降到 5,输出写到本地盘再拷走
相关推荐
一条Go12 分钟前
能力套餐:菜单、权限、开通范围怎么拆成三层而不打架
后端·github·saas
dpharness12 分钟前
我给 DSH 装记忆插件,踩了五个坑,其中一个让我丢了一条记录
后端
就像楞次定律12 分钟前
【Linux 学习记之二】C++ 构建体系与底层链接机制的深度透视(2)
linux·后端
烬羽14 分钟前
LangChain v1 中间件实战:不碰主流程,也能给 Agent 装上"外挂"
后端·langchain·agent
泡海椒18 分钟前
jquick-pdf 生成征信报表(相关矩阵版):靛蓝主题报告里动态图与说明面板并排的实测笔记
后端
IT枫斗者枫哥21 分钟前
Java 退款请求超时,先查原单还是重试?
java·后端
特立独行的猫A25 分钟前
我用仓颉写了个多平台发文工具:一份 Markdown,一条命令发六个平台
java·后端·python
HUANGZHENXUAN12327 分钟前
代码解析(二):detect_video.py —— 一个纯函数 + 一段主循环
后端
爱勇宝1 小时前
《道德经》第 13 章:别让评价系统控制你的心
前端·后端·产品