0%

如何使用 pprof 和 trace 来诊断和修复性能问题

软件开发严重依赖调试技术,这对于有效处理性能问题至关重要。用户在遇到程序执行缓慢时会感到沮丧,这凸显了通过调试工具有效识别和解决潜在问题的重要性。

但是,由于软件的创建和实现过程中涉及庞大的代码库或复杂的系统,因此调试软件中的性能问题可能很困难。

在 Go 中,开发人员可以使用强大的内置工具来帮助诊断和修复性能问题。其中两个工具是 pproftrace 包。

pprof 包允许您分析和分析 Go 程序的执行,而该 trace 包允许您跟踪和可视化事件和 goroutine 活动。当这些工具一起使用时,可以帮我们快速定位 Go 程序中导致性能低下的代码。

了解性能问题

Go 程序或任何软件应用程序中的性能问题都会对用户体验产生重大影响。Go 程序中的性能问题可能由于多种原因而发生。在本节中,我们将介绍性能问题的一些最常见原因以及它们如何影响系统。

  • 低效算法:低效算法会对性能产生重大影响,尤其是在处理大型数据集时。这些算法会占用额外的 CPU 周期和内存资源,这可能会降低整个应用程序的速度。比如暴力搜索方法和无效的排序算法。
  • 阻塞操作:应用程序可能偶尔会等待 I/O 活动完成,例如在磁盘上读取或写入数据或连接到网络。在此过程中,可能会发生阻塞操作并导致执行延迟,从而导致性能下降。当应用程序被阻塞时,它无法执行其他有用的任务,从而导致整体性能下降。
  • 内存使用率过高:使用大量内存的 Go 应用可能会导致性能问题,尤其是在资源不足的系统上。如果应用程序消耗的内存多于系统的可用内存,则系统可能会开始交换到磁盘,从而大大降低应用程序的性能。如果应用程序不能有效地管理内存,从而导致内存泄漏和其他问题,也会发生这种情况。

goroutine 泄漏也会导致内存使用率过高。另外,一些中间价比如 Elasticsearch 等,则建议直接禁用 swap,因为 swap 会导致性能下降,取而代之的是给它足够大的内容。

性能低下的应用程序会导致用户体验差,从而导致用户流失。为了获得最佳体验,优化 Go 应用程序至关重要。

使用 pprof 诊断性能问题

pprof 是 Go 中的一个内置包,它为开发人员提供了一个分析工具,用于观测他们的 Go 程序如何使用 CPU 和内存。然后收集和分析来自此测量的数据。借助 pprof 软件包,开发人员可以轻松测量和识别消耗比正常情况更多的 CPU 内存的函数,以及分配最多内存的程序部分。

让我们假设一个转账 App 使用 Go,并且它具有允许用户使用二维码向朋友汇款的功能。更新该功能后,其开发人员注意到该应用程序的运行速度比平时慢得多,40% 的用户抱怨扫描二维码时延迟长达 15 秒,有时付款失败。为了正确分析问题,开发团队可以在用户扫描二维码时使用 pprof 生成 CPU 分析文件。通过分析文件,他们可能会发现哪些函数占用了过多的 CPU 内存或哪些算法效率低下。在发现问题并修复问题后,他们可以再次测试和使用 pprof ,以确保性能得到提高,体验更快、更无缝。

pprof 的 profile 类型

  1. CPU:用于分析程序的 CPU 使用情况。衡量函数如何消耗不同的 CPU 时间,从而更容易识别哪些函数消耗更多时间,这些就可能是潜在的瓶颈。
  2. Memory:用于分析程序的内存使用情况。衡量应用程序如何使用内存以及应用程序的哪些部分分配更多内存。
  3. Block(阻塞):显示程序阻塞的位置(例如 I/O 或同步原语),从而更容易识别并发低下的区域。
  4. Goroutine(协程):通过返回正在运行、阻塞和等待的状态的 Goroutine,可以轻松检测到并发低下的区域。
  5. Trace:捕获程序执行期间发生的事件的详细日志,例如 goroutine 创建和销毁、调度、网络活动和阻塞操作。它在详细分析应用程序的性能时非常有用。

分析 profile

我们下面以一个例子来讲解一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
"math/rand"
"os"
"runtime/pprof"
)

func main() {
// 创建一个保存 CPU 分析结果的文件
f, err := os.Create("profile.prof")
if err != nil {
panic(err)
}
defer f.Close()

// 开始采集 CPU 性能指标
if err := pprof.StartCPUProfile(f); err != nil {
panic(err)
}
defer pprof.StopCPUProfile()

// 模拟耗 CPU 的操作
for i := 0; i < 1000000; i++ {
n := rand.Intn(100)
_ = square(n)
}
}

func square(n int) int {
return n * n
}

在上面的代码中:

  • main 函数生成一个介于 1 和 1000 之间的随机数,然后计算其平方根。
  • pprof.StartCPUProfile(f) 函数启动 CPU 分析,从而创建可以在以后分析的 profile 文件。
  • defer pprof.StopCPUProfile() 语句确保在程序结束时停止 CPU 分析,无论程序是正常终止还是由于错误。
  • 我们调用 rand.Intn(100) 1000000 次来模拟 CPU 密集型任务。

接下来,我们执行这个程序:

1
go run main.go

程序运行结束后,会生成一个名为 profile.pprof 的文件,这个文件包含了 CPU 分析的数据。我们可以使用 go tool pprof 命令来分析这个文件:

1
go tool pprof profile.prof

接下来,会输出如下内容,并进入了一个交互式的命令行:

1
2
3
4
5
Type: cpu
Time: Jan 15, 2024 at 5:17pm (CST)
Duration: 205.21ms, Total samples = 10ms ( 4.87%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

我们可以接着输入一些命令,来查看 profile 的数据:

比如,我们可以输入 top 来查看最耗 CPU 的函数:

1
2
3
4
5
6
7
8
(pprof) top
Showing nodes accounting for 10ms, 100% of 10ms total
flat flat% sum% cum cum%
10ms 100% 100% 10ms 100% math/rand.(*Rand).Intn
0 0% 100% 10ms 100% main.main
0 0% 100% 10ms 100% math/rand.Intn (inline)
0 0% 100% 10ms 100% runtime.main
(pprof)

分析内存

若要获取内存配置文件,请修改代码以使用函数 pprof.WriteHeapProfile() 将堆配置文件写入文件。在生成随机数并计算其平方后,您需要添加代码以将内存配置文件写入文件(mem.prof)。您还将添加一个 time.Sleep(5 * time.Second) 调用,以便有时间将内存配置文件写入文件。在下面找到代码的更新版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
"fmt"
"math/rand"
"os"
"runtime/pprof"
"time"
)

func main() {
// 创建一个保存 CPU 分析结果的文件
cpuProfileFile, err := os.Create("cpu.prof")
if err != nil {
panic(err)
}
defer cpuProfileFile.Close()

// 开始采集 CPU 性能指标
if err := pprof.StartCPUProfile(cpuProfileFile); err != nil {
panic(err)
}
defer pprof.StopCPUProfile()

// 模拟耗 CPU 的操作
for i := 0; i < 10; i++ {
n := rand.Intn(100)
s := square(n)
fmt.Printf("%d^2 = %d\n", n, s)
}

// 创建一个保存内存分析结果的文件
memProfileFile, err := os.Create("mem.prof")
if err != nil {
panic(err)
}
defer memProfileFile.Close()

// 将内存分析结果写入文件
if err := pprof.WriteHeapProfile(memProfileFile); err != nil {
panic(err)
}
fmt.Println("Memory profile written to mem.prof")

time.Sleep(5 * time.Second)
}

func square(n int) int {
return n * n
}

输出:

1
2
3
4
5
6
7
8
9
10
11
31^2 = 961
83^2 = 6889
88^2 = 7744
86^2 = 7396
14^2 = 196
99^2 = 9801
42^2 = 1764
29^2 = 841
86^2 = 7396
86^2 = 7396
Memory profile written to mem.prof

运行 go run main.go 后,将生成一个 mem.prof 文件。在交互式 shell 中,键入 top 以分析程序的内存使用情况。若要显示此交互式 shell,请运行以下命令:

1
go tool pprof mem.prof

要按 CPU 使用率显示排名靠前的函数,请键入 top 命令:

1
2
3
4
5
6
7
8
9
10
11
➜ go tool pprof mem.prof    
Type: inuse_space
Time: Jan 15, 2024 at 5:22pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 1.72MB, 100% of 1.72MB total
flat flat% sum% cum cum%
1.72MB 100% 100% 1.72MB 100% runtime/pprof.StartCPUProfile
0 0% 100% 1.72MB 100% main.main
0 0% 100% 1.72MB 100% runtime.main
(pprof)

从上面的示例中可以看出,pprof 可以让我们很清楚地知道哪些函数占用了大量的内存或者 CPU。因此,通过将 profile 纳入开发过程,可以很容易地主动识别和解决性能问题,从而实现更快、更高效的应用程序。

在第一个示例中,我们了解了如何使用 pprof 工具创建 CPU 分析文件并对其进行分析。输出显示每个函数的调用次数,以及执行每个函数所花费的总时间。这使我们能够识别消耗最多 CPU 时间的函数,并可能对其进行优化。在第二个示例中,输出显示了每个函数的内存使用情况,包括分配的数量和分配的字节数。这使我们能够识别使用过多内存的函数,并可能对其进行优化以减少内存使用。

使用 trace 追踪

有时,我们需要有关程序如何运行的更多详细信息。在这种情况下,trace 包是一个非常强大和有用的工具。在本节中,我们将对其进行介绍。

trace 是一种工具,可让您收集有关程序运行方式的详细信息。它对于理解 goroutine 是如何创建和调度的、通道的使用方式以及网络请求的处理方式等内容非常有用。它提供了程序执行的时间线视图,可用于识别一段时间内的性能问题和其他类型的错误。

trace 可以收集有关程序运行时发生的各种事件的数据。这些事件包括:Goroutine 创建、销毁、阻塞、取消阻塞、网络活动和垃圾回收。每个 trace 事件都分配了一个时间戳和一个 goroutine ID,允许您查看事件的顺序以及它们之间的关系。

分析 trace 追踪数据

首先,我们将创建一个新的 go 文件,将其命名为 trace.go。若要生成跟踪数据,请导入 runtime/trace 包并在程序开始时调用 trace.Start 。若要停止跟踪收集,请在程序结束时调用 trace.Stop 。下面是它的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
"math/rand"
"os"
"runtime/pprof"
"runtime/trace"
)

func main() {
// 创建一个保存 CPU 分析结果的文件
f, err := os.Create("profile.prof")
if err != nil {
panic(err)
}
defer f.Close()

// 开始采集 CPU 性能指标
if err := pprof.StartCPUProfile(f); err != nil {
panic(err)
}
defer pprof.StopCPUProfile()

// 创建一个保存 trace 追踪结果的文件
traceFile, err := os.Create("trace.out")
if err != nil {
panic(err)
}
defer traceFile.Close()

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

// 模拟耗 CPU 的操作
for i := 0; i < 10; i++ {
n := rand.Intn(100)
_ = square(n)
}
}

func square(n int) int {
return n * n
}

运行以下命令以启动程序:

1
go run main.go

要分析跟踪数据,可以使用 go tool trace 命令,后跟跟踪文件的名称:

1
go tool trace trace.out 

这将启动基于 Web 的跟踪数据可视化,您可以使用它来了解程序的运行方式并识别性能问题。

您还可以查看有关各种 goroutine 以及各种进程如何运行的详细信息!Trace 是了解各种流事件、goroutine 分析等等的绝佳工具!

分析和修复性能问题

使用 pproftrace 收集性能数据后,下一步是分析数据并确定可能的性能问题。

要解释 pprof 的输出,首先需要了解可用的各种类型的分析数据。最常见的配置文件类型是 CPU 和内存配置文件,就像前面引用的示例一样。通过分析这些配置文件,可以识别消耗大量资源并可能成为潜在瓶颈的功能。 pprof 还可以生成其他类型的配置文件,例如互斥锁争用和阻塞配置文件,这有助于确定同步和阻塞问题。例如,较高的互斥锁争用率可能表明多个 goroutine 正在争用同一个锁,这可能导致阻塞和性能不佳。

如前所述,跟踪数据包含有关应用程序行为的更全面的数据,例如 goroutines、阻塞操作和网络流量。跟踪数据分析可用于检测延迟源和其他性能问题,例如网络延迟过长或选择了效率低下的算法。

一旦确定了性能问题,有几种方法可以优化性能。一种常见的策略是通过重用对象来减少内存分配,同时减少大型数据结构的使用。通过减少可分配的内存量和垃圾回收量,可以降低 CPU 使用率并提高整体程序性能。

另一种方法是使用异步 I/O 或非阻塞操作来减少阻塞操作,例如文件 I/O 或网络通信。这有助于减少程序等待 I/O 操作完成所花费的时间,并提高整体程序吞吐量。

此外,优化算法和数据结构可以显著提高性能。通过选择更有效的算法和数据结构,可以减少完成操作所需的 CPU 时间,并提高整体程序性能。

总结

优化 Go 应用程序中的性能以确保它们高效且有效地运行非常重要。通过这样做,我们可以改善用户体验,降低运行应用程序的成本,并提高代码的整体质量。我们可以使用 pproftrace 工具来分析 CPU 和内存使用情况,并识别 Go 应用程序中的瓶颈和其他问题。然后,我们可以根据这些工具的输出对代码进行有针对性的改进,例如减少内存分配、最小化阻塞操作和优化算法。分析工具(如 pproftrace )对于识别和解决性能问题至关重要。