我是如何在大型代码库上使用 pprof 探索 Go 中的内存泄漏


我是如何在大型代码库上使用 pprof 探索 Go 中的内存泄漏文章插图
在今年的大部分时间里 , 我一直在 Orbs 团队用 Go 语言做可扩展的区块链的基础设施开发 , 这是令人兴奋的一年 。 在 2018 年的时候 , 我们研究我们的区块链该选择哪种语言实现 。 因为我们知道 Go 拥有一个良好的社区和一个非常棒的工具集 , 所以我们选择了 Go 。
最近几周 , 我们进入了系统整合的最后阶段 。 与任何大型系统一样 , 可能会在后期阶段出现一些问题 , 包括性能问题 , 内存泄漏等 。 当整合系统时 , 我们找到了一个不错的方法 。 在本文中 , 我将介绍如何调查 Go 中的内存泄漏 , 详细说明寻找 , 理解和解决它的步骤 。
Golang 提供的工具集非常出色但也有其局限性 。 首先来看看这个问题 , 最大的一个问题是查询完整的 core dumps 能力有限 。 完整的 core dumps 是程序运行时的进程占用内存(或用户内存)的镜像 。
我们可以把内存映射想象成一棵树 , 遍历那棵树我们会得到不同的对象分配和关系 。 这意味着无论如何 根会持有内存而不被 GCing(垃圾回收)内存的原因 。 因为在 Go 中没有简单的方法来分析完整的 core dump , 所以很难找到一个没有被 GC 过的对象的根 。
在撰写本文时 , 我们无法在网上找到任何可以帮助我们的工具 。 由于存在 core dump 格式以及从 debug 包中导出该文件的简单方法 , 这可能是 Google 使用过的一种方法 。 网上搜索它看起来像是在 Golang pipeline 中创建了这样的 core dump 查看器 , 但看起来并不像有人在使用它 。 话虽如此 , 即使没有这样的解决方案 , 使用现有工具我们通常也可以找到根本原因 。
内存泄漏内存泄漏或内存压力可以以多种形式出现在整个系统中 。 通常我们将它们视为 bug , 但有时它们的根本原因可能是因为设计的问题 。
当我们在新的设计原则下构建我们的系统时 , 这些考虑并不重要 。 更重要的是以避免过早优化的方式构建系统 , 并使你能够在代码成熟后再优化它们 , 而不是从一开始就过度设计它 。 然而 , 一些常见内存压力的问题是:

  • 内存分配太多 , 数据表示不正确
  • 大量使用反射或字符串
  • 使用全局变量
  • 孤儿 , 没有结束的 Goroutines
在 Go 中 , 创建内存泄漏的最简单方法是定义全局变量 , 数组 , 然后将该数据添加到数组 。 这篇博客文章[1] 以一种不错的方式描述了这个例子 。
我为什么要写这篇文章呢?当我研究这个例子时 , 我发现了很多关于内存泄漏的方法 。 但是 , 相比较这个例子 , 我们的真实系统有超过 50 行代码和单个结构 。 在这种情况下 , 找到内存问题的来源比该示例描述的要复杂得多 。
Golang 为我们提供了一个神奇的工具叫 pprof 。 掌握此工具后 , 可以帮助调查并发现最有可能的内存问题 。 它的另一个用途是查找 CPU 问题 , 但我不会在这篇文章中介绍任何与 CPU 有关的内容 。
go tool pprof把这个工具的方方面面讲清楚需要不止一篇博客文章 。 我将花一点时间找出怎么使用这个工具去获取有用的东西 。 在这篇文章里 , 将集中在它的内存相关功能上 。
pprof 包创建一个 heap dump 文件 , 你可以在随后进行分析 / 可视化以下两种内存映射:
  • 当前的内存分配
  • 总(累积)内存分配
该工具可以比较快照 。 例如 , 可以让你比较显示现在和 30 秒前的差异 。 对于压力场景 , 这可以帮助你定位到代码中有问题的区域 。
pprof 画像pprof 的工作方式是使用画像 。
画像 (profile) 是一组显示导致特定事件实例的调用序列的堆栈追踪 , 例如内存分配 。
文件runtime/pprof/pprof.go[2] 包含画像的详细信息和实现 。
Go 有几个内置的画像供我们在常见情况下使用:
  • Goroutine - 当前所有 Goroutines 的堆栈跟踪
  • heap - 当前存活对象的内存分配的采样
  • allocs - 过去所有内存分配的采样
  • threadcreate - 导致创建新 OS 线程的堆栈跟踪信息
  • block - 导致同步原语阻塞的堆栈跟踪信息
  • mutex - 锁争用的持有者的堆栈跟踪信息
在查看内存问题时 , 我们将专注于堆画像 。 allocs 画像和它在关于数据收集方面是相同的 。 两者之间的区别在于 pprof 工具在启动时读取的方式不一样 。 allocs 画像将以显示自程序启动以来分配的总字节数(包括垃圾收集的字节)的模式启动 pprof 。 在尝试提高代码效率时 , 我们通常会使用该模式 。