使用 Cloud Profiler 分析生产性能

1. 概览

虽然客户端应用和前端 Web 开发者通常使用 Android Studio CPU 性能分析器Chrome 中包含的性能剖析工具等工具来提升其代码的性能,但等效技术还没有被后端服务开发者轻松使用或广泛采用。无论服务开发者的代码是在 Google Cloud Platform 还是其他位置运行,Cloud Profiler 都能为他们提供相同的功能。

95c034c70c9cac22

该工具可从生产应用中收集 CPU 使用情况和内存分配信息。它会将该信息归因于应用的源代码,从而帮助您确定消耗最多资源的应用程序部分,并且阐明代码的性能特征。该工具采用的收集技术开销较低,因此适合在生产环境中连续使用。

在此 Codelab 中,您将学习如何为 Go 程序设置 Cloud Profiler,并熟悉该工具可以提供哪些类型的应用性能分析数据。

学习内容

  • 如何使用 Cloud Profiler 配置 Go 程序以进行性能分析。
  • 如何使用 Cloud Profiler 收集、查看和分析性能数据。

所需条件

  • Google Cloud Platform 项目
  • 一个浏览器,例如 ChromeFirefox
  • 熟悉标准的 Linux 文本编辑器,例如 Vim、EMACs 或 Nano

您将如何使用本教程?

仅阅读教程内容 阅读并完成练习

您如何评价自己使用 Google Cloud Platform 的体验?

<ph type="x-smartling-placeholder"></ph> 新手 中级 熟练

2. 设置和要求

自定进度的环境设置

  1. 登录 Cloud 控制台,然后创建一个新项目或重复使用现有项目。 如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个

96a9c957bc475304

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

请记住项目 ID,它在所有 Google Cloud 项目中都是唯一的名称(上述名称已被占用,您无法使用,抱歉!)。它稍后将在此 Codelab 中被称为 PROJECT_ID

  1. 接下来,您需要在 Cloud 控制台中启用结算功能,才能使用 Google Cloud 资源。

运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。请务必按照“清理”部分部分,其中会指导您如何关停资源,以免产生超出本教程范围的结算费用。Google Cloud 的新用户符合参与 300 美元的免费试用计划的条件。

Google Cloud Shell

虽然 Google Cloud 可以通过笔记本电脑远程操作,但为了简化此 Codelab 中的设置,我们将使用 Google Cloud Shell,这是一个在云端运行的命令行环境。

激活 Cloud Shell

  1. 在 Cloud Console 中,点击激活 Cloud Shell4292cbf4971c9786

bce75f34b2c53987.png

如果您以前从未启动过 Cloud Shell,系统会显示一个中间屏幕(非首屏)来介绍 Cloud Shell。如果是这种情况,请点击继续(此后您将不会再看到此通知)。一次性屏幕如下所示:

70f315d7b402b476

预配和连接到 Cloud Shell 只需花几分钟时间。

fbe3a0674c982259.png

这个虚拟机装有您需要的所有开发工具。它提供了一个持久的 5GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证。只需使用一个浏览器或 Google Chromebook 即可完成本 Codelab 中的大部分(甚至全部)工作。

在连接到 Cloud Shell 后,您应该会看到自己已通过身份验证,并且相关项目已设置为您的项目 ID:

  1. 在 Cloud Shell 中运行以下命令以确认您已通过身份验证:
gcloud auth list

命令输出

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. 在 Cloud Shell 中运行以下命令,以确认 gcloud 命令了解您的项目:
gcloud config list project

命令输出

[core]
project = <PROJECT_ID>

如果不是上述结果,您可以使用以下命令进行设置:

gcloud config set project <PROJECT_ID>

命令输出

Updated property [core/project].

3. 导航到 Cloud Profiler

在 Cloud 控制台中,点击“Profiler”进入 Profiler 界面:

37ad0df7ddb2ad17.png

或者,您也可以使用 Cloud 控制台搜索栏导航到 Profiler 界面:只需输入“Cloud Profiler”即可然后选择所找到的项。无论采用哪种方式,您都应看到 Profiler 界面显示“No data to display”如下所示。该项目是新的,因此尚未收集任何分析数据。

d275a5f61ed31fb2.png

是时候进行分析了!

4. 对基准进行性能分析

我们将使用一个简单的合成 Go 应用(可在 GitHub 上获取)。在您仍处于打开状态(并且 Profiler 界面中仍显示“No data to display”消息)的 Cloud Shell 终端中,运行以下命令:

$ go get -u github.com/GoogleCloudPlatform/golang-samples/profiler/...

然后切换到应用目录:

$ cd ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/profiler/hotapp

该目录包含“main.go”文件,这是一个已启用性能分析代理的合成应用:

main.go

...
import (
        ...
        "cloud.google.com/go/profiler"
)
...
func main() {
        err := profiler.Start(profiler.Config{
                Service:        "hotapp-service",
                DebugLogging:   true,
                MutexProfiling: true,
        })
        if err != nil {
                log.Fatalf("failed to start the profiler: %v", err)
        }
        ...
}

默认情况下,性能剖析代理会收集 CPU、堆和线程性能剖析文件。此处的代码用于启用互斥量分析(也称为“争用”)配置文件的收集。

现在,运行程序:

$ go run main.go

在程序运行时,性能剖析代理会定期收集五种已配置类型的配置文件。收集是随时间随机分配的(每种类型的平均速率为每分钟一个配置文件),因此可能需要长达三分钟才能获取收集到的每种类型。该程序会在创建个人资料时通知您。这些消息通过上述配置中的 DebugLogging 标志启用;否则,代理会以静默方式运行:

$ go run main.go
2018/03/28 15:10:24 profiler has started
2018/03/28 15:10:57 successfully created profile THREADS
2018/03/28 15:10:57 start uploading profile
2018/03/28 15:11:19 successfully created profile CONTENTION
2018/03/28 15:11:30 start uploading profile
2018/03/28 15:11:40 successfully created profile CPU
2018/03/28 15:11:51 start uploading profile
2018/03/28 15:11:53 successfully created profile CONTENTION
2018/03/28 15:12:03 start uploading profile
2018/03/28 15:12:04 successfully created profile HEAP
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:04 successfully created profile THREADS
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:25 successfully created profile HEAP
2018/03/28 15:12:25 start uploading profile
2018/03/28 15:12:37 successfully created profile CPU
...

在首次收集配置文件之后,界面很快就会自行更新。此后它不会自动更新,因此要查看新数据,您需要手动刷新 Profiler 界面。为此,请点击时间间隔选择器中的“Now”按钮两次:

650051097b651b91

界面刷新后,您会看到如下内容:

47a763d4dc78b6e8

个人资料类型选择器显示了五种可用的个人资料类型:

b5d7b4b5051687c9.png

现在,我们来回顾一下每种配置文件类型和一些重要的界面功能,然后进行一些实验。在此阶段,您不再需要 Cloud Shell 终端,因此可以按 CTRL-C 并输入“exit”,退出该终端。

5. 分析 Profiler 数据

既然我们已经收集了一些数据,接下来我们来深入了解一下。我们使用的合成应用(可在 GitHub 上获取源代码)可模拟生产环境中不同种类性能问题的典型行为。

CPU 密集型代码

选择 CPU 配置文件类型。界面加载后,您会在火焰图中看到 load 函数的四个叶块,它们共同解释了所有 CPU 消耗:

fae661c9fe6c58df.png

此函数专门编写为通过运行紧密循环来消耗大量 CPU 周期:

main.go

func load() {
        for i := 0; i < (1 << 20); i++ {
        }
}

系统会通过以下四个调用路径从 busyloop() 间接调用此函数:busyloop → {foo1, foo2} → {bar, baz} → load。函数框的宽度表示特定调用路径的相对成本。在这种情况下,所有四条路径的费用大致相同。在实际的计划中,您需要专注于优化对效果而言最重要的来电路径。火焰图通过较大的框突出显示开销较高的路径,使这些路径易于识别。

您可以使用配置文件数据过滤器进一步缩小显示范围。例如,尝试添加“显示堆栈”指定“baz”的过滤条件用作过滤条件字符串。您应该会看到类似于以下屏幕截图的内容,其中仅显示了 load() 的四个调用路径中的两个。只有这两个路径会经过字符串“baz”的函数。当您想关注某个较大程序的子部分(例如,因为您只拥有该程序的一部分)时,这种过滤非常有用。

eb1d97491782b03f.png

内存密集型代码

现在切换到“堆”个人资料类型。请务必移除您在之前的实验中创建的所有过滤条件。现在,您应该看到一个火焰图,其中由 alloc 调用的 allocImpl 显示为应用中内存的主要使用方:

f6311c8c841d04c4.png

火焰图上方的摘要表表明,应用中已使用的总内存量平均约为 57.4 MiB,其中大部分由 allocImpl 函数分配。鉴于此函数的实现,这并不奇怪:

main.go

func allocImpl() {
        // Allocate 64 MiB in 64 KiB chunks
        for i := 0; i < 64*16; i++ {
                mem = append(mem, make([]byte, 64*1024))
        }
}

该函数会执行一次,将 64 MiB 以更小的分块分配,然后将指向这些区块的指针存储在全局变量中,以防止这些区块被垃圾回收。请注意,分析器中显示的内存量与 64 MiB 略有不同:Go 堆分析器是一种统计工具,因此测量开销较低,但字节准确。看到这样差异约为 10% 时,不必感到惊讶。

IO 密集型代码

如果您选择“线程”在性能剖析文件类型选择器中,显示屏将切换为火焰图,其中大部分宽度由 waitwaitImpl 函数占用:

ebd57fdff01dede9.png

在上面的火焰图摘要中,您可以看到有 100 个 goroutine 通过 wait 函数增大其调用堆栈。完全正确,因为启动这些等待的代码如下所示:

main.go

func main() {
        ...
        // Simulate some waiting goroutines.
        for i := 0; i < 100; i++ {
                go wait()
        }

此分析类型有助于了解程序在等待(如 I/O)时是否花费了任何意外时间。此类调用堆栈通常不会由 CPU 性能分析器进行采样,因为它们不会占用任何大量 CPU 时间。您通常需要使用“隐藏堆栈”具有线程配置文件的过滤器 - 例如,隐藏以调用 gopark, 结束的所有堆栈,因为这些堆栈通常是空闲 goroutine,没有等待 I/O 的堆栈更有趣。

线程配置文件类型也可以帮助识别程序中线程长时间等待由程序的其他部分拥有的互斥量的点,但对于这种情况,以下配置文件类型更为有用。

争用密集型代码

争用配置文件类型标识最“所需”锁定。此配置文件类型适用于 Go 程序,但必须通过指定“MutexProfiling: true”来明确启用添加到代理配置代码中该集合的工作原理是(在“争用”指标下)记录特定锁在被 goroutine A 解锁时,另一个 goroutine B 等待解锁被解锁的次数。它还会(在“延迟”指标下)记录被阻塞的 goroutine 等待锁定的时间。下例中有一个争用堆栈,且锁的总等待时间为 10.5 秒:

83f00dca4a0f768e

生成此配置文件的代码包含 4 个争夺互斥量的 goroutine:

main.go

func contention(d time.Duration) {
        contentionImpl(d)
}

func contentionImpl(d time.Duration) {
        for {
                mu.Lock()
                time.Sleep(d)
                mu.Unlock()
        }
}
...
func main() {
        ...
        for i := 0; i < 4; i++ {
                go contention(time.Duration(i) * 50 * time.Millisecond)
        }
}

6. 总结

在本实验中,您学习了如何配置 Go 程序以与 Cloud Profiler 搭配使用。您还学习了如何使用此工具收集、查看和分析效果数据。现在,您可以将新技能运用到您在 Google Cloud Platform 上运行的实际服务中。

7. 恭喜!

您已了解如何配置和使用 Cloud Profiler!

了解详情

许可

此作品已获得 Creative Commons Attribution 2.0 通用许可授权。