一、什么是 trace
trace 是 Go 语言的一个非常强大的性能分析工具,它用于追踪和记录 Go 程序的执行过程。与 CPU 和内存性能分析工具(如 pprof)不同,trace 侧重于在时间维度上分析程序的行为,帮助开发者理解程序执行中的时间分布、并发和调度等方面的细节。
二、如何使用 Go Trace
1、启用 trace
Go Trace 的启用非常简单。你只需要在程序中插入 runtime/trace 包,并在合适的地方启动和停止 trace。具体步骤如下:
package mainimport ("fmt""runtime/trace""os""time"
)func main() {// 创建一个文件来保存 trace 信息f, err := os.Create("trace.out")if err != nil {fmt.Println("could not create trace file:", err)return}// 启动 tracedefer f.Close()if err := trace.Start(f); err != nil {fmt.Println("could not start trace:", err)return}// 确保在函数退出时停止 tracedefer trace.Stop()// 模拟程序执行的任务simulateTask()
}func simulateTask() {// 模拟一段时间的工作fmt.Println("Task started")time.Sleep(2 * time.Second)fmt.Println("Task completed")
}
执行上述脚本后,会生成一个 trace.out 文件
2、分析trace文件
生成 trace 文件后,使用 go tool trace 命令来分析该文件。该命令会启动一个 web 界面,帮助你查看程序的详细执行过程。
go tool trace trace.out
执行该命令后,会启动一个本地的 Web 服务器
跳转打开之后
点击协程分析:
可以看到协程运行时间、同步阻塞时间、系统调用阻塞时间、调度延迟时间等。
如果发现某个数据不正常,可以点击看上面的链接,就可以定位到具体的阻塞代码了
当我们想要查看处理器的使用状况时,便可以使用处理器分析视图(View trace by Proc)。后面就是这个视图的样子。
借助处理器视图,我们可以了解到多核是否处于被充分使用,也能够知晓在某一特定时刻,处于不同状态的协程数量分别是多少。如果处于可调度(Runnable)状态的协程数量较多,可能意味着创建的协程数量过多,导致无法得到 CPU 的有效调度。而且我们还能够查看在那些占用处理器时间的各类事件当中,究竟哪些是由垃圾回收(GC)所占用的。倘若垃圾回收占用 CPU 的时间过多,这同样也意味着程序处于一种不健康的运行状态
以下是各个链接的说明:
2.1 View trace(常用)
是指查看完整的 trace 文件,观察程序的执行路径和执行过程,通常会包括以下信息:Goroutine 的生命周期、各种事件(如内存分配、垃圾回收)、调度、等待、阻塞等状态、Trace 数据通常通过 runtime/trace 包生成并保存为 .out 文件,之后通过 go tool trace 命令进行查看。
2.2 Goroutine analysis(常用)
这一部分显示了所有 Goroutine 的活动和状态。Go 的并发模型是基于 Goroutines(轻量级线程)实现的,而通过 Goroutine 分析,你可以:看到每个 Goroutine 执行的任务、分析 Goroutine 是否在阻塞、等待或过度创建等情况、观察 Goroutine 的调度、启动和终止等
2.3 Network blocking profile
这一部分分析网络相关的阻塞情况,通常是程序在进行网络操作时(例如 HTTP 请求、数据库连接等)发生的阻塞。它显示了因为网络操作而导致的延迟或卡住的情况。具体来说,可能的网络阻塞包括:
- 网络请求的等待时间(如 DNS 查询、HTTP 请求等待等)
- 网络连接建立和关闭的阻塞
- 数据传输时的等待
这种阻塞通常会影响应用程序的响应性和性能,因此网络阻塞分析对于优化程序的网络操作至关重要。
2.4 Synchronization blocking profile
Synchronization blocking profile 分析了程序中由于同步操作(如锁、信号量、条件变量等)而发生的阻塞情况。这类同步操作通常是为了保护共享资源,防止数据竞态问题。阻塞通常发生在以下场景:
竞争条件:多个 Goroutine 同时请求获取同一资源(例如,通过互斥锁 mutex)。
等待某些条件的发生(例如条件变量 Cond 的等待)。如果程序过度使用同步原语(例如 sync.Mutex 或 sync.WaitGroup),或者同步操作设计不当,可能会导致性能瓶颈。通过这部分分析,你可以了解在哪些同步点程序被阻塞,并优化同步的粒度和设计。
2.5 Syscall blocking profile
Syscall blocking profile 分析了程序在进行系统调用(如文件操作、网络 I/O、内存管理等)时发生的阻塞情况。Go 程序的系统调用通常是同步的,这意味着程序在等待 I/O 操作完成时会被阻塞。常见的系统调用包括:
- 网络 I/O 操作(如 net.Dial、http.Get 等)
- 文件 I/O 操作(如读写文件)
操作系统的其他资源访问(如进程创建、内存分配等)
如果应用程序的性能瓶颈出现在系统调用上,可能需要考虑优化 I/O 操作,减少阻塞的时间,或使用异步 I/O 模型。
2.6 Scheduler latency profile
Scheduler latency profile 分析了 Go 程序中调度器的延迟问题。Go 语言的调度器负责将 Goroutine 映射到操作系统线程,调度延迟可能会影响程序的响应时间和吞吐量。常见的调度延迟问题包括:
- Goroutine 长时间没有被调度执行
- 某些 Goroutine 被延迟调度,导致响应时间增加
- 调度器的上下文切换过于频繁,影响程序性能
通过这一部分的分析,开发者可以确定是否存在调度瓶颈,并相应地优化 Goroutine 的调度策略,例如调整 Goroutine 的优先级、减少不必要的上下文切换等。
2.7 User-defined tasks
User-defined tasks 是程序员通过 runtime/trace 的 WithRegion 和 Eventf 等接口定义的自定义任务。Go 的 Trace 工具允许开发者手动标记特定的任务或操作,以便后续分析。例如,你可能会将某个重要的业务逻辑部分(如数据库查询、复杂计算等)标记为任务,并查看它的执行情况和性能。
用户定义任务 对于标记和分析关键代码路径非常有用,帮助开发者聚焦于特定部分的性能瓶颈。
2.8 User-defined regions
User-defined regions 是程序员自定义的代码区域,通常用 WithRegion 函数来标记。类似于自定义任务,区域是对程序执行的特定代码块进行的标记,用于分析该区域的执行时长和性能。例如,你可以在一个数据库查询操作的开始和结束处插入 WithRegion,以查看该查询所占用的时间。
用户定义区域 让开发者能够细粒度地分析程序的执行,并且将重点放在程序中耗时较长的区域上。
2.9 Minimum mutator utilization
Minimum mutator utilization 是 Go 程序中与垃圾回收(GC)相关的一个度量,表示在 GC 运行期间,程序的“变异器”(Mutator)部分的工作效率。变异器是指执行用户代码的部分,它在 GC 期间可能会被暂停或延迟。如果变异器的利用率较低,意味着 GC 占用了太多的 CPU 时间,导致应用程序性能下降。
Mutator:指执行用户代码的 Goroutine,它在垃圾回收的过程中会被暂停。
利用率:表示在 GC 期间,变异器代码的执行时间相对于 GC 停顿时间的比例。
最低变异器利用率 是评估垃圾回收效率和程序性能的一个指标。如果该值过低,说明 GC 的停顿时间过长,可能需要优化内存管理或减少垃圾回收的频率。