简介
Go 生态系统提供了一套丰富的 API 和工具,用于诊断 Go 程序中的逻辑和性能问题。本页总结了可用的工具,并帮助 Go 用户根据其具体问题选择合适的工具。
诊断解决方案可分为以下几类:
- 分析:分析工具用于评估 Go 程序的复杂性和成本,例如其内存使用情况和频繁调用的函数,以识别程序中开销较大的部分。
- 跟踪:跟踪是一种通过插桩代码来分析整个调用或用户请求生命周期中延迟的方法。跟踪提供了系统中各组件对总延迟贡献程度的概览。跟踪可以跨越多个 Go 进程。
- 调试:调试允许我们暂停 Go 程序并检查其执行过程。可以通过调试来验证程序状态和流程。
- 运行时统计与事件:运行时统计信息和事件的收集与分析,提供了 Go 程序健康状况的高级概览。指标的激增或骤降有助于我们识别吞吐量、利用率和性能的变化。
注意:某些诊断工具可能会相互干扰。例如,精确的内存分析会扭曲 CPU 分析结果,而 goroutine(协程)阻塞分析会影响调度器跟踪。为获得更精确的信息,请单独使用这些工具。
分析
分析有助于识别代码中开销较大或调用频繁的部分。Go 运行时提供
分析数据,其格式符合 pprof 可视化工具 的要求。
可以在测试期间通过 go test 或通过
net/http/pprof 包提供的端点来收集分析数据。用户需要收集分析数据,并使用 pprof 工具来过滤和可视化主要的代码执行路径。
runtime/pprof 包提供的预定义分析类型:
- cpu:CPU 分析用于确定程序在主动消耗 CPU 周期时(而非休眠或等待 I/O 时)的时间花费点。
- heap:堆分析报告内存分配样本;用于监控当前和历史内存使用情况,并检查内存泄漏。
- threadcreate:线程创建分析报告导致创建新操作系统线程的程序部分。
- goroutine:Goroutine 分析报告所有当前 goroutine(协程)的栈追踪信息。
-
block:阻塞分析显示 goroutine(协程)在哪些同步原语(包括计时器 channel(通道))上阻塞等待。默认情况下不启用阻塞分析;使用
runtime.SetBlockProfileRate来启用它。 -
mutex:互斥锁分析报告锁争用情况。当你认为 CPU 利用率因互斥锁争用而未满载时,可使用此分析。默认情况下不启用互斥锁分析,参见
runtime.SetMutexProfileFraction以启用它。
还有哪些分析器可用于分析 Go 程序?
在 Linux 上,perf 工具 可用于分析 Go 程序。Perf 可以分析和展开 cgo/SWIG 代码及内核,因此有助于深入了解 原生/内核性能瓶颈。在 macOS 上, Instruments 套件可用于分析 Go 程序。
我可以对生产环境的服务进行分析吗?
可以。在生产环境中对程序进行分析是安全的,但启用 某些分析(例如 CPU 分析)会增加开销。你应该预期会看到性能下降。在生产环境中启用分析器之前,可以通过测量其开销来估计性能损失。
你可能希望定期对生产服务进行分析。 特别是在一个系统中存在某个进程的多个副本时,定期随机选择一个副本是一个安全的做法。 选择一个生产进程,每 Y 秒分析 X 秒,并保存结果以供可视化和分析;然后定期重复。结果可以手动和/或自动检查以发现问题。 收集分析可能会相互干扰, 因此建议一次只收集一种分析。
可视化分析数据的最佳方法有哪些?**
Go 工具使用 go tool pprof 提供分析数据的文本、图形和 callgrind 格式可视化。
阅读 分析 Go 程序 以查看其实际应用。
以文本形式列出开销最大的调用。
以图形形式可视化开销最大的调用。
Weblist 视图会在 HTML 页面中逐行显示源代码中开销最大的部分。在下面的示例中,runtime.concatstrings 函数耗费了 530 毫秒,列表中展示了每行代码的开销。
以 weblist 形式可视化开销最大的调用。
另一种可视化剖析数据的方式是使用火焰图。 火焰图允许你沿特定的调用路径移动,从而可以放大或缩小查看代码的特定部分。 上游的 pprof 工具支持火焰图。
火焰图提供可视化视图以定位开销最大的代码路径。
是否只能使用内置的剖析配置?
除了运行时提供的配置外,Go 用户还可以通过 pprof.Profile 创建自定义的剖析配置,并使用现有工具来检查它们。
能否在不同的路径和端口上提供剖析处理函数(/debug/pprof/...)?
可以。net/http/pprof 包默认将其处理函数注册到默认的 mux 上,但你也可以使用该包导出的处理函数自行注册。
例如,以下示例将在 :7777 端口的 /custom_debug_path/profile 路径上提供 pprof.Profile 处理函数:
package main
import (
"log"
"net/http"
"net/http/pprof"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/custom_debug_path/profile", pprof.Profile)
log.Fatal(http.ListenAndServe(":7777", mux))
}
追踪
追踪是一种通过检测代码来分析调用链整个生命周期中延迟的方法。Go 提供了 golang.org/x/net/trace 包作为每个 Go 节点的最小化追踪后端,并提供了一个带有简单仪表盘的最小化检测库。Go 还提供了一个执行追踪器,用于追踪一段时间内的运行时事件。
追踪使我们能够:
- 检测和分析 Go 进程中的应用程序延迟。
- 衡量一长串调用中特定调用的开销。
- 找出可利用和性能提升的方面。没有追踪数据,瓶颈并不总是显而易见的。
在单体系统中,从程序的构建块中收集诊断数据相对容易。所有模块都存在于一个进程内,并共享共同的资源来报告日志、错误和其他诊断信息。一旦你的系统超出单个进程的范围并开始变得分布式,跟踪一个从前端 Web 服务器开始、经过所有后端、最终将响应返回给用户的调用就会变得更加困难。这时,分布式追踪在检测和分析你的生产系统方面就发挥着重要作用。
分布式追踪是一种通过检测代码来分析用户请求整个生命周期中延迟的方法。当系统是分布式的,且传统的剖析和调试工具无法扩展时,你可能需要使用分布式追踪工具来分析用户请求和 RPC 的性能。
分布式追踪使我们能够:
- 检测和分析大型系统中的应用程序延迟。
- 跟踪用户请求生命周期内的所有 RPC,并查看仅在生产环境中可见的集成问题。
- 找出可以应用于我们系统的性能提升方案。在收集追踪数据之前,许多瓶颈并不明显。
Go 生态系统为各种追踪系统和后端无关的库提供了多种分布式追踪库。
有没有办法自动拦截每个函数调用并创建追踪跨度?
Go 没有提供自动拦截每个函数调用并创建追踪跨度的方法。你需要手动检测你的代码来创建、结束和标注跨度。
我应该如何在 Go 库中传播追踪头?
你可以在
context.Context
中传播追踪标识符和标签。
目前业界还没有规范的追踪键或通用的追踪头表示形式。每个追踪提供者负责在其 Go 库中提供传播工具。
还有哪些来自标准库或运行时的低层事件可以包含在追踪中?
标准库和运行时正尝试暴露多个额外的 API 来通知低层内部事件。例如,
httptrace.ClientTrace
提供了 API 来跟踪传出请求生命周期中的低层事件。
目前正在进行一项工作,以从运行时执行追踪器中获取低层运行时事件,并允许用户定义和记录他们自己的事件。
调试
调试是识别程序行为异常原因的过程。调试器允许我们理解程序的执行流程和当前状态。调试有多种方式;本节仅关注将调试器附加到程序以及核心转储调试。
Go 用户主要使用以下调试器:
- Delve: Delve 是 Go 编程语言的调试器。它支持 Go 的运行时概念和内置类型。Delve 致力于成为 Go 程序功能全面、稳定可靠的调试器。
- GDB: Go 通过标准 Go 编译器和 Gccgo 提供 GDB 支持。栈管理、线程和运行时包含的方面与 GDB 预期的执行模型存在足够大的差异,即使程序是用 gccgo 编译的,也可能混淆调试器。尽管 GDB 可用于调试 Go 程序,但它并非理想选择,可能会引起混淆。
调试器与 Go 程序配合效果如何?
gc 编译器会执行优化,例如函数内联和变量寄存器化。这些优化有时会使使用调试器进行调试变得更加困难。目前正在进行一项工作,旨在提高为优化二进制文件生成的 DWARF 信息的质量。在这些改进可用之前,我们建议在构建待调试的代码时禁用优化。以下命令构建一个不包含编译器优化的包:
$ go build -gcflags=all="-N -l"作为改进工作的一部分,Go 1.10 引入了一个新的编译器标志
-dwarflocationlists。该标志使编译器添加位置列表,以帮助调试器处理优化后的二进制文件。以下命令构建一个包含优化但带有 DWARF 位置列表的包:
$ go build -gcflags="-dwarflocationlists=true"
推荐的调试器用户界面是什么?
尽管 delve 和 gdb 都提供命令行界面,但大多数编辑器集成和 IDE 都提供特定于调试的用户界面。
能否对 Go 程序进行事后调试?
核心转储文件是一个包含运行进程内存转储及其进程状态的文件。它主要用于程序的事后调试以及理解程序仍在运行时的状态。这两种情况使得核心转储调试成为事后诊断和分析生产服务的良好辅助工具。可以从 Go 程序获取核心文件,并使用 delve 或 gdb 进行调试,具体步骤请参阅 核心转储调试 页面。
运行时统计和事件
运行时提供内部事件的统计数据和报告,供用户诊断运行时级别的性能和资源使用问题。
用户可以监控这些统计数据,以更好地了解 Go 程序的整体健康状况和性能。 一些经常监控的统计数据和状态:
runtime.ReadMemStats报告与堆分配和垃圾回收相关的指标。内存统计数据有助于监控进程消耗的内存资源量、进程是否能有效利用内存,以及捕获内存泄漏。debug.ReadGCStats读取垃圾回收的统计数据。 它有助于了解有多少资源花在了垃圾回收暂停上。 它还报告垃圾回收器暂停的时间线以及暂停时间百分位数。debug.Stack返回当前的栈跟踪。栈跟踪有助于查看当前运行着多少个 goroutine(协程), 它们在做什么,以及它们是否被阻塞。debug.WriteHeapDump暂停所有 goroutine(协程)的执行, 并将堆转储到文件。堆转储是 Go 进程在给定时间点的内存快照。它包含所有已分配的对象以及 goroutine(协程)、终结器等。runtime.NumGoroutine返回当前 goroutine(协程)的数量。 可以监控该值以查看是否充分利用了足够的 goroutine(协程),或检测 goroutine(协程)泄漏。
执行追踪器
Go 自带运行时执行追踪器,用于捕获广泛的运行时事件。调度、系统调用、垃圾回收、堆大小和其他事件由运行时收集,并可通过 go tool trace 进行可视化。执行追踪器是检测延迟和资源利用问题的工具。你可以检查 CPU 的利用情况,以及网络或系统调用是否是导致 goroutine(协程)被抢占的原因。
追踪器适用于:
- 理解你的 goroutine(协程)如何执行。
- 理解一些核心运行时事件,例如 GC 运行。
- 识别并行化程度低的执行。
然而,它不太适合识别热点,例如分析内存或 CPU 使用过多的原因。请先使用分析工具来解决这些问题。
上图中,go tool trace 可视化显示执行开始时情况良好,随后变为串行执行。这表明可能存在共享资源的锁争用,从而形成了瓶颈。
参阅 go tool trace
以收集和分析运行时跟踪。
GODEBUG
如果相应地设置了 GODEBUG 环境变量,运行时也会发出事件和信息。
- GODEBUG=gctrace=1 在每次垃圾回收时打印回收事件,总结回收的内存总量和暂停时长。
- GODEBUG=inittrace=1 打印已完成的包初始化工作的执行时间及内存分配信息摘要。
- GODEBUG=schedtrace=X 每 X 毫秒打印一次调度事件。
GODEBUG 环境变量可用于禁用标准库和运行时中指令集扩展的使用。
- GODEBUG=cpu.all=off 禁用所有可选的指令集扩展。
- GODEBUG=cpu.extension=off 禁用指定指令集扩展的指令。
extension 是指令集扩展的小写名称,例如 sse41 或 avx。