Diagnostics
引言
Go 生态系统提供了一套丰富的 API 和工具,用于诊断 Go 程序中的逻辑和性能问题。本页将总结可用的工具,并帮助 Go 用户为他们遇到的具体问题选择合适的工具。
诊断解决方案可分为以下几类:
- 性能分析 (Profiling):性能分析工具分析 Go 程序的复杂性和成本,例如内存使用情况和频繁调用的函数,以识别 Go 程序中开销较大的部分。
- 追踪 (Tracing):追踪是一种仪器化代码的方式,用于分析调用或用户请求整个生命周期中的延迟。追踪可以提供一个系统整体延迟中每个组件贡献多少延迟的概览。追踪可以跨越多个 Go 进程。
- 调试 (Debugging):调试允许我们暂停 Go 程序并检查其执行情况。可以通过调试来验证程序的状态和流程。
- 运行时统计信息和事件:收集和分析运行时统计信息和事件可以提供 Go 程序健康状况的高级别概览。指标的峰值/谷值有助于我们识别吞吐量、利用率和性能的变化。
注意:一些诊断工具可能会相互干扰。例如,精确的内存分析会影响 CPU 分析,而 goroutine 阻塞分析会影响调度器追踪。为了获得更精确的信息,请单独使用工具。
性能分析
性能分析有助于识别代码中开销大或频繁调用的部分。Go 运行时以 pprof 可视化工具 期望的格式提供 性能分析数据。可以通过 go
test
或从 net/http/pprof 包提供的端点在测试期间收集性能分析数据。用户需要收集性能分析数据,并使用 pprof 工具来过滤和可视化最顶层的代码路径。
runtime/pprof
包提供的预定义分析项:
- cpu:CPU 分析确定程序在积极消耗 CPU 周期时(而不是在睡眠或等待 I/O 时)将时间花在了哪里。
- heap:堆分析报告内存分配样本;用于监控当前和历史内存使用情况,以及检查内存泄漏。
- threadcreate:线程创建分析报告导致创建新 OS 线程的程序部分。
- goroutine:Goroutine 分析报告所有当前 goroutine 的堆栈跟踪。
-
block:阻塞分析显示 goroutine 在等待同步原语(包括定时器通道)时发生阻塞的位置。阻塞分析默认不启用;使用
runtime.SetBlockProfileRate
来启用它。 -
mutex:互斥锁分析报告锁的争用情况。当您认为 CPU 未完全利用是由于互斥锁争用时,可以使用此分析。互斥锁分析默认不启用,请参阅
runtime.SetMutexProfileFraction
来启用它。
我还能使用哪些其他分析器来分析 Go 程序?
在 Linux 上,perf 工具 可用于分析 Go 程序。Perf 可以分析和展开 cgo/SWIG 代码和内核,因此可以帮助深入了解原生/内核性能瓶颈。在 macOS 上,可以使用 Instruments 套件来分析 Go 程序。
我可以分析我的生产服务吗?
可以。分析生产中的程序是安全的,但启用某些分析(例如 CPU 分析)会增加成本。您应该预期性能会有所下降。可以通过在生产环境中启用分析器之前测量其开销来估算性能损失。
您可能希望定期分析您的生产服务。特别是在具有单个进程的许多副本的系统中,定期选择一个随机副本是一个安全的选择。选择一个生产进程,每 Y 秒对其进行 X 秒的分析,然后保存结果以供可视化和分析;然后定期重复。结果可以手动和/或自动审查以查找问题。分析的收集可能会相互干扰,因此建议一次只收集一个分析。
可视化性能分析数据的最佳方法是什么?
Go 工具通过 go tool pprof
提供文本、图形和 callgrind 格式的可视化性能分析数据。阅读 Profiling Go programs 来查看它们的使用示例。
以文本形式列出开销最大的调用。
以图形方式可视化最昂贵的调用。
Weblist 视图以 HTML 页面的形式逐行显示源代码中最昂贵的部分。在下面的示例中,530ms 花费在 runtime.concatstrings
上,并显示了每行的开销。
以 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)) }
追踪 (Tracing)
追踪是一种仪器化代码的方式,用于分析调用链整个生命周期中的延迟。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 都提供了 CLI,但大多数编辑器集成和 IDE 都提供了特定于调试的用户界面。
是否可以对 Go 程序进行事后调试?
核心转储文件是包含正在运行的进程的内存转储及其进程状态的文件。它主要用于程序的内存转储调试,以及了解程序在运行时状态。这两种情况使得核心转储的调试成为事后分析和分析生产服务的良好诊断辅助工具。可以从 Go 程序获取核心文件并使用 delve 或 gdb 进行调试,请参阅 核心转储调试 页面以获取分步指南。
运行时统计信息和事件
运行时提供统计信息和内部事件的报告,供用户在运行时级别诊断性能和利用率问题。
用户可以监控这些统计信息,以更好地了解 Go 程序的整体健康状况和性能。一些经常监控的统计信息和状态:
runtime.ReadMemStats
报告与堆分配和垃圾回收相关的指标。内存统计信息对于监控进程消耗的内存资源量、进程是否能很好地利用内存以及捕获内存泄漏非常有用。debug.ReadGCStats
读取有关垃圾回收的统计信息。查看有多少资源花在了 GC 暂停上很有用。它还报告垃圾回收器暂停的时间线和暂停时间百分位数。debug.Stack
返回当前堆栈跟踪。堆栈跟踪有助于查看当前正在运行多少 goroutine、它们在做什么,以及它们是否被阻塞。debug.WriteHeapDump
暂停所有 goroutine 的执行,并允许您将堆转储到文件。堆转储是 Go 进程在给定时间点的内存快照。它包含所有已分配的对象以及 goroutine、finalizer 等。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 在每次 GC 时打印垃圾回收事件,总结收集的内存量和暂停时间。
- GODEBUG=inittrace=1 打印已完成的包初始化工作的执行时间和内存分配信息摘要。
- GODEBUG=schedtrace=X 每 X 毫秒打印调度事件。
GODEBUG 环境变量可用于禁用标准库和运行时中指令集扩展的使用。
- GODEBUG=cpu.all=off 禁用所有可选指令集扩展的使用。
- GODEBUG=cpu.extension=off 禁用来自指定指令集扩展的指令的使用。
extension 是指令集扩展的名称(小写),例如 sse41 或 avx。