CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • Go性能调优 说明
  • 1. Go语言性能导论
  • 2 数据结构与算法
  • 3 理解并发
  • 4 Go语言中的等效标准模板库算法
  • 5 Go语言中的矩阵和向量计算
  • 6 编写易读的Go代码
  • 7 Go语言中的模板编程
  • 8 Go语言中的内存管理
  • 9 Go语言中的GPU并行化
  • 10 Go语言中的编译时评估
  • 11. 构建和部署Go代码
  • 12 Go代码性能分析
    • 理解性能分析
      • CPU性能分析的原因
      • 内存性能分析的原因
    • 探索检测方法
      • 使用go test进行性能分析
      • 在代码中手动插入性能分析代码
      • 对运行中的服务代码进行性能分析
    • CPU性能分析简介
    • 内存性能分析简介
    • 使用上游pprof扩展功能
    • 比较多个分析结果
    • 在pprof中解读火焰图
    • 在Go语言中检测内存泄漏
    • 总结
  • 13 跟踪Go代码
  • 14 集群与作业队列
  • 15 跨版本比较代码质量
目录

12 Go代码性能分析

# 12 Go代码性能分析

性能分析(Profiling)是一种用于测量计算机系统中资源使用情况的方法。进行性能分析通常是为了了解程序中的CPU或内存使用情况,以便针对执行时间、程序大小或可靠性进行优化。除了性能分析,在本章中,我们还将学习以下内容:

  • 如何使用pprof对Go语言中的请求进行性能分析
  • 如何比较多个性能分析结果
  • 如何解读生成的性能分析报告和火焰图(flame graphs)

进行性能分析有助于推断在代码中哪些地方可以进行改进,以及函数中各个部分在整个系统中所花费的时间占比情况。

# 理解性能分析

对Go代码进行性能分析是找出代码库中瓶颈的最佳方法之一。我们的计算机系统存在物理限制(例如CPU时钟速度、内存大小/速度、I/O读写速度以及网络吞吐量等),但是我们通常可以通过优化程序,更高效地利用硬件资源。使用性能分析工具对计算机程序进行分析后,会生成一份报告。这份报告通常被称为性能分析报告(profile),它能提供所运行程序的相关信息。了解程序的CPU和内存使用情况有诸多原因,以下列举几个例子:

# CPU性能分析的原因

  • 检查软件新版本中的性能改进情况
  • 确认每个任务的CPU使用量
  • 限制CPU使用以节省成本
  • 找出延迟产生的原因

# 内存性能分析的原因

  • 全局变量使用不当
  • 未完成的协程(Goroutines)
  • 反射使用不当
  • 大量字符串分配

接下来,我们将探讨检测方法。

# 探索检测方法

pprof工具提供了多种将性能分析集成到代码中的方法。Go语言的创造者希望确保在实现编写高性能程序所需的性能分析时,既简单又有效。我们可以在Go软件开发的多个阶段进行性能分析,即设计阶段、新函数创建阶段、测试阶段和生产阶段。

需要注意的是,性能分析确实会带来少量的性能损耗,因为在运行的二进制文件中需要持续收集更多的指标数据。许多公司(包括谷歌)认为这种权衡是可以接受的。为了持续编写高性能的代码,为CPU和内存性能分析增加5%的额外开销是值得的。

# 使用go test进行性能分析

你可以使用go test命令创建CPU和内存性能分析报告。如果你想比较多次测试运行的输出结果,这种方法会很有用。这些输出结果通常会存储在长期存储设备中,以便在更长的时间范围内进行比较。要对测试执行CPU和内存性能分析,可执行go test -cpuprofile /tmp/cpu.prof -memprofile /tmp/mem.prof -bench命令。

这将创建两个输出文件cpu.prof和mem.prof,它们都会存储在/tmp/文件夹中。后续可以使用本章“分析性能分析报告”部分介绍的技术来分析这些生成的性能分析报告。

# 在代码中手动插入性能分析代码

如果你想对代码中的特定部分进行性能分析,可以直接在该代码周围实现性能分析功能。如果你只想分析代码的一小部分,或者希望pprof的输出更小更简洁,又或者不想在已知的高开销代码部分周围添加性能分析代码而增加额外开销,这种方法会特别有用。针对代码库的不同部分进行CPU和内存性能分析,有不同的方法。

对特定代码块进行CPU使用情况的性能分析代码如下:

func foo() {
    pprof.StartCPUProfile()
    defer pprof.StopCPUProfile()
   ...
    // 代码
   ...
}
1
2
3
4
5
6
7

对特定代码块进行内存使用情况的性能分析代码如下:

func bar() {
    runtime.GC()
    defer pprof.WriteHeapProfile()
   ...
    // 代码
   ...
}
1
2
3
4
5
6
7

如果我们设计高效、迭代有成效,并使用下一节中的习惯用法来实现性能分析,有望无需手动在代码中插入这些片段。不过,知道这始终是一种进行代码性能分析和获取有意义输出的可行选择,也是很有帮助的。

# 对运行中的服务代码进行性能分析

在Go代码中,最常用的性能分析方法是在HTTP处理函数中启用分析器。这对于调试生产环境中的实时系统很有用。能够实时对生产系统进行性能分析,使你可以根据真实的生产数据做出决策,而不仅仅依赖于本地开发环境。

有时,只有当特定系统的数据量达到一定规模时,错误才会出现。一个能够有效处理1000个数据点的方法或函数,在其运行的底层硬件上,可能无法有效处理100万个数据点。在硬件环境变化的情况下,这一点尤为重要。无论你是在有其他干扰任务的Kubernetes环境中运行,使用规格未知的新物理硬件,还是使用新版本的代码或第三方库,了解这些变化对性能的影响,对于确保系统的可靠性和弹性至关重要。

能够从生产系统获取数据(在生产系统中,最终用户的数据量可能比你在本地使用的数据量大得多),有助于发现一些在本地迭代时可能从未注意到的性能问题,进而做出对最终用户有影响的性能改进。

如果我们想在HTTP处理函数中使用pprof库,可以导入_ "net/http/pprof"到主包中。

这样,你的HTTP处理函数就会注册用于性能分析的HTTP处理程序。请确保不要在公开暴露的HTTP服务器上执行此操作,因为公开程序的性能分析报告可能会暴露严重的安全漏洞。pprof包的索引展示了你使用该包时可用的路径。以下是pprof工具索引的截图:

img

我们可以查看公开的HTTP pprof路径及其描述。路径和相关描述如下表所示:

名称 HTTP路径 描述
allocs /debug/pprof/allocs 内存分配信息。
block /debug/pprof/block 有关协程阻塞等待位置的信息。这通常发生在同步原语上。
cmdline /debug/pprof/cmdline 二进制文件命令行调用的值。
goroutine /debug/pprof/goroutine 当前正在运行的协程的堆栈跟踪。
heap /debug/pprof/heap 内存分配采样(用于监控内存使用和泄漏情况)。
mutex /debug/pprof/mutex 竞争互斥锁的堆栈跟踪。
profile /debug/pprof/profile CPU性能分析报告。
symbol /debug/pprof/symbol 请求程序计数器。
threadcreate /debug/pprof/threadcreate 操作系统线程创建的堆栈跟踪。
trace /debug/pprof/trace 当前程序的跟踪信息。这将在第13章“Go代码追踪”中深入讨论。

在下一节中,我们将讨论CPU性能分析。

# CPU性能分析简介

让我们对一个简单的Go程序进行一些示例性能分析,以便了解分析器的工作原理。我们将创建一个包含几个睡眠参数的示例程序,以此查看不同函数调用的时间情况:

  1. 首先,实例化包并添加所有导入:
import (
    "fmt"
    "io"
    "net/http"
    _ "net/http/pprof"
    "time"
)
1
2
3
4
5
6
7
  1. 接下来,在主函数中,我们有一个HTTP处理函数,其中包含两个作为处理函数一部分被调用的睡眠函数:
func main() {
    Handler := func(w http.ResponseWriter, req *http.Request) {
        sleep(5)
        sleep(10)
        io.WriteString(w, "Memory Management Test")
    }
    http.HandleFunc("/", Handler)
    http.ListenAndServe(":1234", nil)
}
1
2
3
4
5
6
7
8
9

我们的sleep函数只是睡眠特定的毫秒数,并打印结果输出:

func sleep(sleepTime int) {
    time.Sleep(time.Duration(sleepTime) * time.Millisecond)
    fmt.Println("Slept for ", sleepTime, " Milliseconds")
}
1
2
3
4
  1. 当我们运行程序时,执行go run httpProfiling.go。要从这段特定代码生成性能分析报告,我们需要调用curl -s "localhost:1234/debug/pprof/profile?seconds=10" > out.dump。这将运行10秒钟的性能分析,并将结果输出到名为out.dump的文件中。默认情况下,pprof工具将运行30秒,并将二进制数据输出到标准输出(STDOUT)。我们要确保将测试时间限制在合理的范围内,并且需要重定向输出,以便能够在性能分析工具中获取有意义的内容进行查看。

  2. 接下来,为我们的函数生成测试负载。我们可以使用Apache Bench来完成此任务,并发10个请求,共生成5000个请求,使用ab -n 5000 -c 10 http://localhost:1234/进行设置。

  3. 一旦我们得到这个测试的输出,就可以查看out.dump文件,执行go tool pprof out.dump。这将进入性能分析器。这是C++性能分析器pprof的一个变体,该工具具有相当多的功能。

  4. 我们可以使用topN命令查看生成的性能分析报告中排名前N的样本,如以下截图所示:

img

在执行性能分析器(profiler)时,Go大约每秒会暂停程序100次。在此期间,它会记录Go协程(goroutine)栈上的程序计数器。我们还可以使用累积标志(-cum),按照当前性能分析采样中的累积值进行排序:

img

  1. 我们还能够以图形形式展示跟踪信息的可视化表示。确保安装了graphviz包后(它应该包含在你的包管理器中,或者也可以从http://www.graphviz.org/ ,通过简单输入网页命令进行下载),这将为我们生成的程序性能分析提供可视化展示:

img

性能分析图中的红色框表示对请求流影响最大的代码路径。查看这些框,正如我们所预期的,会发现示例程序的很大一部分时间花在睡眠以及向客户端回写响应上。我们可以通过在这种网页格式中输入想要查看的函数名,来查看特定函数的情况。例如,如果想要查看sleep函数的详细视图,只需输入(pprof) web sleep命令。 8. 然后我们会得到一个聚焦于sleep调用的SVG图像:

img

  1. 得到这些细分信息后,我们可能想要深入了解sleep函数实际执行的操作。我们可以在pprof中使用list命令,获取对sleep命令调用及其后续调用进行性能分析的输出。下面的截图展示了这一情况;为简洁起见,代码进行了缩短:

img

通过性能分析将我们所做的工作分解为可细分的部分,能让我们从利用率的角度,深入了解开发工作的改进方向。

在下一节中,我们将了解内存性能分析。

# 内存性能分析简介

我们可以对内存执行与上一节CPU测试类似的操作。让我们看看另一种使用测试功能进行性能分析的方法。以第2章“数据结构和算法”中创建的o-logn函数为例。我们可以利用已经为这个特定函数创建的基准测试,并在该测试中添加一些内存性能分析。我们可以执行go test -memprofile=heap.dump -bench命令。

我们会看到与第2章“数据结构和算法”类似的输出:

img

唯一的区别是,现在我们从这个测试中获得了堆(heap)的性能分析数据。如果使用性能分析器查看,我们将看到关于堆使用情况的数据,而不是CPU使用情况的数据。我们还能够看到程序中每个函数的内存分配情况。下面的图表展示了这一点:

img

这很有帮助,因为它使我们能够看到代码各部分生成的堆大小。我们还可以查看累积内存分配最多的部分:

img

随着程序变得越来越复杂,了解内存使用状态变得越来越重要。在下一节中,我们将讨论如何使用上游的pprof扩展我们的性能分析能力。

# 使用上游pprof扩展功能

如果我们希望默认就能使用更多功能,可以使用上游的pprof二进制文件来扩展性能分析的视图:

  1. 我们可以通过执行go get github.com/google/pprof获取它。pprof工具的调用方式有好几种。我们可以使用报告生成方法,以请求的格式(目前支持.dot、.svg、.web、.png、.jpg、.gif和.pdf格式)生成文件。我们也可以像上一节进行CPU和内存性能分析那样,使用交互式终端格式。最后一种也是最常用的方法是使用HTTP服务器。这种方法需要启动一个HTTP服务器,以易于理解的格式展示许多相关输出。
  2. 通过go get获取二进制文件后,我们可以通过Web界面调用它,查看之前生成的输出:pprof -http=:1234 profile.dump。
  3. 然后,我们可以访问新出现的用户界面(UI),查看默认pprof工具中没有的功能。这个工具的一些主要亮点如下:
    • 一个支持正则表达式(regex)搜索的表单字段,有助于搜索所需的性能分析元素。
    • 一个下拉视图菜单,方便查看可用的不同性能分析工具。
    • 一个示例下拉菜单,用于显示性能分析中的样本。
    • 一个优化过滤器,用于隐藏/显示请求流的不同部分。

拥有这些用于性能分析的工具,有助于使性能分析过程更加高效。如果我们想要查看名称中包含fmt的任何调用的运行时间,可以使用带有正则表达式过滤器的样本视图,它将突出显示fmt调用,如下截图所示:

img

能够根据这些值进行过滤,有助于缩小性能不佳函数的排查范围。

# 比较多个分析结果

性能分析一个非常实用的功能是,你可以对不同的分析结果进行比较。如果我们对同一个程序进行了两次不同的测量,就能够判断所做的更改是否对系统产生了积极影响。下面我们对HTTP睡眠计时函数做一些扩展:

  1. 先添加一些额外的导入:
package main

import (
    "fmt"
    "net/http"
    _ "net/http/pprof"
    "strconv"
    "time"
)
1
2
3
4
5
6
7
8
9
  1. 接下来,修改处理程序,使其能够接受一个表示时间的查询字符串参数:
func main() {
    Handler := func(w http.ResponseWriter, r *http.Request) {
        sleepDuration := r.URL.Query().Get("time")
        sleepDurationInt, err := strconv.Atoi(sleepDuration)
        if err != nil {
            fmt.Println("Incorrect value passed as a query string for time")
            return
        }
        sleep(sleepDurationInt)
        fmt.Fprintf(w, "Slept for %v Milliseconds", sleepDuration)
    }
    http.HandleFunc("/", Handler)
    http.ListenAndServe(":1234", nil)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 保持sleep函数不变:
func sleep(sleepTime int) {
    time.Sleep(time.Duration(sleepTime) * time.Millisecond)
    fmt.Println("Slept for ", sleepTime, " Milliseconds")
}
1
2
3
4
  1. 现在有了这个额外功能,我们可以通过向HTTP处理程序传递查询参数,针对不同的计时情况获取多个分析结果:

    • 运行新的计时性能分析工具:
    go run timedHttpProfiling.go
    
    1
    • 在另一个终端中,启动性能分析工具:
    curl -s "localhost:1234/debug/pprof/profile?seconds=20" > 5-millisecond-profile.dump
    
    1
    • 然后对新资源发起多次请求:
    ab -n 10000 -c 10 http://localhost:1234/?time=5
    
    1
    • 接着收集第二个分析结果:
    curl -s "localhost:1234/debug/pprof/profile?seconds=20" > 10-millisecond-profile.dump
    
    1
    • 再对新资源发起第二次请求,生成第二个分析结果:
    ab -n 10000 -c 10 http://localhost:1234/?time=10
    
    1
  2. 现在我们有了两个不同的分析结果,分别存储在5-millisecond-profile.dump和10-millisecond-profile.dump中。我们可以使用之前的工具来比较它们,设置一个基础分析结果和一个次要分析结果。如下截图展示了这一过程:

img

比较分析结果能让我们了解所做的更改对系统的影响。在下一节中,我们将介绍火焰图(flame graph)。

# 在pprof中解读火焰图

上游pprof包中最有用的工具之一就是火焰图。火焰图是一种固定速率采样的可视化图表,有助于确定分析结果中的热点代码路径。随着程序越来越复杂,分析结果也会越来越大。通常很难确切知道是哪个代码路径占用了最多的CPU资源,或者用我常说的话来讲,哪个是“短板”。

火焰图最初是由Netflix的Brendan Gregg开发的,用于解决MySQL的CPU利用率问题。这种可视化工具的出现帮助了许多程序员和系统管理员确定程序中延迟的来源。pprof二进制文件生成的是一种冰柱式(火焰向下)的火焰图。在火焰图中,数据按特定框架进行可视化展示:

  • x轴表示请求中的所有样本集合。
  • y轴表示栈中的帧数,通常也称为栈深度。
  • 方框的宽度表示特定函数调用所占用的总CPU时间。

将这三个元素结合起来可视化展示,有助于确定程序的哪个部分引入了最多的延迟。你可以访问pprof分析结果的火焰图部分,地址是http://localhost:8080/ui/flamegraph 。以下图片展示了一个火焰图示例:

img

如果查看第2章“数据结构与算法”中的冒泡排序(bubbleSort)示例,我们可以看到在测试中占用CPU时间的不同部分的详细情况。在交互式网页模式下,我们可以将鼠标悬停在每个样本上,查看它们的持续时间和执行时间百分比。

在下一节中,我们将了解如何在Go语言中检测内存泄漏。

# 在Go语言中检测内存泄漏

正如在第8章“Go语言中的内存管理”的“内存对象分配”部分所讨论的,我们有很多工具可以用来查看当前正在执行的程序的内存统计信息。在本章中,我们还将学习使用pprof工具进行性能分析。Go语言中一种较为常见的内存泄漏情况是无限制地创建协程(goroutines)。当你使一个无缓冲通道过载,或者有一个高度并发的抽象不断生成新的协程但这些协程又无法结束时,就经常会出现这种情况。协程占用的资源非常少,系统通常可以生成大量的协程,但在生产环境中排查程序问题时,最终会发现它们存在一个上限,找到这个上限往往很麻烦。

在下面的示例中,我们将研究一个存在泄漏问题的无缓冲通道抽象:

  1. 首先初始化包并导入必要的依赖项:
package main

import (
    "fmt"
    "net/http"
    _ "net/http/pprof"
    "runtime"
    "time"
)
1
2
3
4
5
6
7
8
9
  1. 在main函数中,处理HTTP监听并为leakyAbstraction函数提供服务。通过HTTP提供服务,这样可以更方便地观察协程数量的增长:
func main() {
    http.HandleFunc("/leak", leakyAbstraction)
    http.ListenAndServe("localhost:6060", nil)
}
1
2
3
4
  1. 在leakyAbstraction函数中,首先初始化一个无缓冲的字符串通道。然后在一个无限循环中,将协程的数量写入HTTP响应,并将wait()函数的结果写入通道:
func leakyAbstraction(w http.ResponseWriter, r *http.Request) {
    ch := make(chan string)

    for {
        fmt.Fprintln(w, "Number of Goroutines: ", runtime.NumGoroutine())
        go func() { ch <- wait() }()
    }
}
1
2
3
4
5
6
7
8
  1. wait()函数会睡眠5微秒并返回一个字符串:
func wait() string {
    time.Sleep(5 * time.Microsecond)
    return "Hello Gophers!"
}
1
2
3
4

这些函数共同作用,会不断生成协程,直到运行时无法再创建新协程并崩溃。我们可以通过执行以下命令运行服务器来测试:

go run memoryLeak.go
1

服务器运行后,在另一个终端窗口中,我们可以使用以下命令向服务器发起请求:

curl localhost:6060/leak
1

curl命令会打印生成的协程数量,直到服务器被终止:

img

请注意,根据系统配置的不同,这个请求可能需要一些时间。这是正常的,它展示了程序中可用的协程数量。

使用本章所学的技术,我们可以借助pprof进一步调试类似这样的内存问题,但理解潜在的问题将有助于我们避免内存问题。

这个示例是为了明确展示内存泄漏问题而编写的,但如果想让这个可执行程序避免协程泄漏,我们需要修改两处:

  • 无限循环很可能需要设置一个边界。
  • 可以添加一个带缓冲的通道,以确保有能力处理通过该通道进入的所有生成的协程。

# 总结

在本章中,我们学习了性能分析相关内容,包括什么是分析结果,以及如何使用pprof生成分析结果。你还学习了如何使用不同的方法分析分析结果、如何比较不同的分析结果,以及如何读取火焰图来评估性能。在生产环境中具备这些能力,将有助于你维持系统稳定性、提升性能,并为终端用户带来更好的体验。在下一章中,我们将讨论另一种分析代码的方法——跟踪(tracing)。

上次更新: 2025/04/08, 19:40:35
11. 构建和部署Go代码
13 跟踪Go代码

← 11. 构建和部署Go代码 13 跟踪Go代码→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式