使用 Stackdriver Profiler 分析生产性能

虽然客户端应用和前端 Web 开发者通常使用 Android Studio CPU 性能剖析器Chrome 中包含的性能剖析工具等工具提高代码性能,但后端服务的开发者几乎不易访问或广泛采用等效技术。无论开发者的代码是在 Google Cloud Platform 上还是在别处运行,Stackdriver Profiler 都能为开发者提供相同的功能。

该工具会从生产应用中收集 CPU 使用情况和内存分配信息。它会将这些信息归因于应用的源代码,从而帮助您识别应用中资源占用最多的部分,并阐明代码的性能特征。由于该工具采用的收集技术开销较低,因此适合在生产环境中持续使用。

在此 Codelab 中,您将学习如何为 Go 程序设置 Stackdriver Profiler,并熟悉该工具可带来的应用性能方面的数据分析。

学习内容

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

所需条件

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

您将如何使用本教程?

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

您如何评价自己在使用 Google Cloud Platform 方面的经验水平?

新手 中级 熟练

自定进度的环境设置

如果您还没有 Google 帐号(Gmail 或 Google Apps),则必须创建一个。登录 Google Cloud Platform Console (console.cloud.google.com) 并创建一个新项目:

2016-02-10 12:45:26 的屏幕截图.png

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

接下来,您需要在 Cloud Console 中启用结算功能,才能使用 Google Cloud 资源。

在此 Codelab 中运行仅花费几美元,但是如果您决定使用更多资源或继续让它们运行,费用可能更高(请参阅本文档末尾的“清理”部分)。

Google Cloud Platform 的新用户有资格获享 $300 免费试用

Google Cloud Shell

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

激活 Google Cloud Shell

在 GCP 控制台中,点击右上角工具栏上的 Cloud Shell 图标:

然后点击“启动 Cloud Shell”:

配置和连接到环境应该只需要片刻时间:

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

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

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

gcloud auth list

命令输出

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

命令输出

[core]
project = <PROJECT_ID>

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

gcloud config set project <PROJECT_ID>

命令输出

Updated property [core/project].

在 Cloud Console 中,点击左侧导航栏中的“Profiler”以转到 Profiler 界面:

或者,您可以使用 Cloud Console 搜索栏导航到 Profiler 界面:只需输入“Stackdriver Profiler”并选择找到的项即可。无论使用哪种方式,您都应该会看到 Profiler 界面和“No data to show”消息,如下所示。该项目是新项目,因此尚未收集到任何性能分析数据。

现在,该开始进行相关分析了!

我们将使用 GitHub 上提供的简单合成 Go 应用。在仍处于打开状态的 Cloud Shell 终端中(同时在 Profiler 界面中显示“没有可显示的数据”消息时),运行以下命令:

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

然后切换到 application 目录:

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

该目录包含“quq.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

在该程序运行时,性能分析代理会定期收集五种已配置的类型的配置文件。系统是随机选择合集(每种类型的平均速率为每分钟一个配置文件),因此收集每个类型最多可能需要 3 分钟。该程序会在您创建个人资料时通知您。这些消息由上述配置中的 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 界面。为此,请在时间间隔选择器中点击两次“现在”按钮:

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

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

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

我们已收集了部分数据,接下来我们将进一步分析这些数据。我们使用合成应用(GitHub 上提供了源代码)来模拟生产环境中各种不同类型的性能问题的常见行为。

CPU 密集型代码

选择 CPU 配置文件类型。界面加载后,您会在火焰图中看到 load 函数的四个叶块,这些块分别计入所有 CPU 消耗:

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

main.go

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

该函数通过以下四个调用路径从 busyloop() 间接调用:busyloop → {foo1, foo2} → {bar, baz} → load。函数框的宽度表示特定调用路径的相对费用。在这种情况下,全部 4 个路径的费用大致相同。在实际程序中,您需要专注于优化对效果而言最重要的调用路径。火焰图直观强调了带有较大方框的昂贵路径,使得这些路径易于识别。

您可以使用配置文件数据过滤器进一步优化显示效果。例如,尝试添加将“baz”指定为过滤条件字符串的“显示堆栈”过滤条件。您应该会看到类似以下屏幕截图的内容,其中仅显示了 load() 的四个调用路径中的两个。只有这两个路径可传递名称中包含字符串“baz;baz”的函数。如果您希望专注于某个大计划的一部分(例如,因为您只拥有其中的一部分),这种过滤就很有用。

内存密集型代码

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

火焰图上方的摘要表表明,应用中已使用的总内存量平均约为 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 函数占据:

在火焰图上方的摘要中,可以看到有 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 等待解锁的次数。它还会记录(在“Delay”指标下)阻塞的 goroutine 等待锁定的时间。在此示例中,有一个争用堆栈,锁定的总等待时间为 11.03 秒:

用于生成此配置文件的代码由 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)
        }
}

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

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

了解详情

许可

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