登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go runtime trace 显示阻塞但代码无锁的分析思路

来源:17golang原创

时间:2026-10-16 21:47:50 236浏览 收藏

我第一次遇到这个问题,是在 trace 时间线里看到一批 goroutine 长时间处于等待状态,但代码搜索不到 mutex,也没有明显的 channel 锁。后来发现,“阻塞”只是现象,不是原因:goroutine 可能在等网络、系统调用、调度机会或另一个同步事件。排查的关键,是沿着 trace 的事件和等待原因建立一条证据链。

Go runtime trace 显示阻塞但代码无锁时,先区分 Network wait、Syscall wait、Scheduler latency 和同步等待;不要直接把所有等待都归因于锁。trace 适合回答“什么时候、谁在等、等什么”,热点和锁竞争的成本再交给对应的 pprof。

先确认 trace 看见的到底是什么

Go 的 execution trace 会记录 goroutine 创建、阻塞与唤醒、syscall 进入和退出、GC、堆变化以及处理器状态等事件。时间线上的一段空白,首先要回到 goroutine 状态和等待原因,而不是只盯着代码中有没有 sync.Mutex。

我通常先把现象分成三层:第一层是 goroutine 是否从运行态进入等待态;第二层是等待原因是否指向网络、syscall 或同步原语;第三层才是调度器是否让 runnable goroutine 等了太久。这样可以避免把“没有执行”误读为“被锁住”。

Go trace 文件、goroutine 状态与网络系统调用调度等待的静态关系说明图
图1:trace 事件分类说明图,观察等待原因与调度视角的边界;这是静态结构图,不是运行截图。

从网络与系统调用边界排查假锁

网络等待很容易伪装成锁等待。例如 HTTP 客户端在等待连接建立、读取响应体,或者服务端连接暂时没有数据时,goroutine 会让出执行权。此时 trace 里的等待对象和原因比“这段代码没有返回”更有信息量。系统调用等待也类似:文件、socket 或其他 syscall 可能让当前 goroutine 停下来,但并不代表存在互斥锁。

可以用最小化命令生成测试 trace,再用 go tool trace 打开。下面的命令只是采集入口,实际排查时应让测试覆盖一次可重复的网络或调度场景:

# 生成测试执行 trace,便于查看 goroutine 与网络事件的时间关系
go test -trace=trace.out ./path/to/package

# 打开 trace 查看器;不要把 trace 当作 CPU 热点报告
go tool trace trace.out

# 只提取网络阻塞视角,输出交给 pprof 做进一步查看
go tool trace -pprof=net trace.out > net.pprof
go tool pprof net.pprof

如果网络视角明显而同步视角不明显,优先检查连接池、超时、响应体读取和对端响应;如果 syscall 视角占主导,则把文件或 socket 的等待作为外部依赖处理。这里的结论是“等待发生在哪里”,不是“已经找到唯一根因”。

把调度延迟与业务等待分开

另一种常见误判是:goroutine 已经 runnable,但迟迟没有获得处理器,于是看起来像被阻塞。调度延迟要从 runnable、P/M 状态和 scheduler latency 的关系看;网络等待则是 goroutine 主动等待外部事件。二者的修复方向完全不同,前者可能涉及过度并发、长时间占用 CPU 或调度压力,后者则可能要回到网络和 syscall。

排查时建议固定一个短时间窗口,先看大量 goroutine 是否在同一资源上等待,再看 CPU 是否真的繁忙。如果 trace 只是告诉你“任务被串行化”,它可以提示共享资源成为瓶颈,但不能单凭这一点证明是 mutex;Go 官方也建议将 trace 用于时序和利用率,把热点问题交给 profiling 工具。

用最小 trace 和用户标记缩小范围

大 trace 往往让线索互相覆盖。我更倾向于缩短采集窗口,并在请求边界加入少量稳定的用户标记。runtime/trace 提供 Log、WithRegion 和 task 相关 API:region 表示同一个 goroutine 内的一段区间,task 则可以把跨 goroutine 的逻辑操作挂到同一个 context.Context 上。

package main

import (
	"context"
	"log"
	"os"
	"runtime/trace"
)

func capture(ctx context.Context, work func(context.Context)) error {
	// 单独写 trace 文件,便于把一次可复现请求与业务日志对应起来。
	f, err := os.Create("trace.out")
	if err != nil {
		return err
	}
	defer func() {
		// 关闭文件,确保 trace.Stop 完成后的数据能落盘。
		_ = f.Close()
	}()

	if err := trace.Start(f); err != nil {
		return err
	}
	defer trace.Stop()

	// region 标记同一 goroutine 内的阶段,Log 补充可检索的业务线索。
	trace.WithRegion(ctx, "load-profile", func() {
		trace.Log(ctx, "case", "network-read")
		work(ctx)
	})
	return nil
}

func main() {
	if err := capture(context.Background(), func(context.Context) {}); err != nil {
		log.Fatal(err)
	}
}

示例中的标记只用于把 trace 与问题场景对齐,不代表任何具体运行结果。标记名称要少而稳定,避免把用户输入、令牌或高基数字段写进 trace。

知道什么时候换 pprof

我的判断很简单:如果问题是“谁在什么时间等待、等待原因是什么”,继续看 trace;如果问题是“哪里消耗了 CPU、哪些同步原语产生了成本、哪些 goroutine 长期卡在同步点”,就换到 pprof。Go 的 diagnostics 文档明确区分 execution tracer 与 CPU、block、mutex 等 profile,而且不同诊断工具同时启用可能互相影响。

业务请求、runtime trace、用户标记与 block mutex CPU profile 的诊断边界说明图
图2:trace 与 pprof 的诊断边界说明图,按问题类型选择工具;这是静态结构图,不是运行截图。
看到的现象优先看什么不要先下的结论
等待原因包含 network连接、读写、超时和对端响应不是 mutex 的直接证据
runnable 但调度延迟高CPU 利用率、并发量和调度视角不是业务 I/O 变慢的直接证据
同步等待集中且可复现block 或 mutex profile不要只凭时间线猜锁名
CPU 热点明显CPU profiletrace 不适合替代热点分析

常见疑问

trace 里出现 blocking,就一定是锁吗?

不一定。goroutine 阻塞可以来自网络、syscall、channel、定时器或同步原语,先看事件类型和等待原因。

为什么 trace 看不到完整业务调用链?

runtime trace 记录的是运行时事件,不会自动替每个函数创建业务 span。需要用 region、task、log 或应用层 tracing 补齐业务语义。

trace 和 block profile 能同时开吗?

可以按需要采集,但不要把同时采集得到的结果当成完全无干扰的基线。先用一种诊断工具缩小问题,再用另一种工具确认成本。

归根结底,代码里没有锁并不等于没有等待;而 trace 里出现等待,也不等于已经证明锁竞争。把 trace 当作“事件关系图”,把 pprof 当作“成本定位器”,通常比反复搜索 Lock() 更快接近真正的网络、调度或同步问题。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>