[译] 我是如何在大型代码库上使用 pprof 调查 Go 中的内存泄漏

在今年的大部分时间里,我一直在 Orbs 团队用 Go 语言作可扩展的区块链的基础设施开发,这是使人兴奋的一年。在 2018 年的时候,咱们研究咱们的区块链该选择哪一种语言实现。由于咱们知道 Go 拥有一个良好的社区和一个很是棒的工具集,因此咱们选择了 Go。node

最近几周,咱们进入了系统整合的最后阶段。与任何大型系统同样,可能会在后期阶段出现一些问题,包括性能问题,内存泄漏等。当整合系统时,咱们找到了一个不错的方法。在本文中,我将介绍如何调查 Go 中的内存泄漏,详细说明寻找,理解和解决它的步骤。git

Golang 提供的工具集很是出色但也有其局限性。首先来看看这个问题,最大的一个问题是查询完整的 core dumps 能力有限。完整的 core dumps 是程序运行时的进程占用内存(或用户内存)的镜像。github

咱们能够把内存映射想象成一棵树,遍历那棵树咱们会获得不一样的对象分配和关系。这意味着不管如何 根会持有内存而不被 GCing(垃圾回收)内存的缘由。由于在 Go 中没有简单的方法来分析完整的 core dump,因此很难找到一个没有被 GC 过的对象的根。golang

在撰写本文时,咱们没法在网上找到任何能够帮助咱们的工具。因为存在 core dump 格式以及从 debug 包中导出该文件的简单方法,这多是 Google 使用过的一种方法。网上搜索它看起来像是在 Golang pipeline 中建立了这样的 core dump 查看器,但看起来并不像有人在使用它。话虽如此,即便没有这样的解决方案,使用现有工具咱们一般也能够找到根本缘由。web

内存泄漏

内存泄漏或内存压力能够以多种形式出如今整个系统中。一般咱们将它们视为 bug,但有时它们的根本缘由多是由于设计的问题。正则表达式

当咱们在新的设计原则下构建咱们的系统时,这些考虑并不重要。更重要的是以避免过早优化的方式构建系统,并使你可以在代码成熟后再优化它们,而不是从一开始就过分设计它。然而,一些常见内存压力的问题是:shell

  • 内存分配太多,数据表示不正确
  • 大量使用反射或字符串
  • 使用全局变量
  • 孤儿,没有结束的 goroutines

在 Go 中,建立内存泄漏的最简单方法是定义全局变量,数组,而后将该数据添加到数组。这篇博客文章以一种不错的方式描述了这个例子。后端

我为何要写这篇文章呢?当我研究这个例子时,我发现了不少关于内存泄漏的方法。可是,相比较这个例子,咱们的真实系统有超过 50 行代码和单个结构。在这种状况下,找到内存问题的来源比该示例描述的要复杂得多。数组

Golang 为咱们提供了一个神奇的工具叫 pprof。掌握此工具后,能够帮助调查并发现最有可能的内存问题。它的另外一个用途是查找 CPU 问题,但我不会在这篇文章中介绍任何与 CPU 有关的内容。浏览器

go tool pprof

把这个工具的方方面面讲清楚须要不止一篇博客文章。我将花一点时间找出怎么使用这个工具去获取有用的东西。在这篇文章里,将集中在它的内存相关功能上。

pprof 包建立一个 heap dump 文件,你能够在随后进行分析/可视化如下两种内存映射:

  • 当前的内存分配
  • 总(累积)内存分配

该工具能够比较快照。例如,可让你比较显示如今和 30 秒前的差别。对于压力场景,这能够帮助你定位到代码中有问题的区域。

pprof 画像

pprof 的工做方式是使用画像。

画像是一组显示致使特定事件实例的调用顺序堆栈的追踪,例如内存分配。

文件runtime/pprof/pprof.go包含画像的详细信息和实现。

Go 有几个内置的画像供咱们在常见状况下使用:

  • goroutine - 全部当前 goroutines 的堆栈跟踪
  • heap - 活动对象的内存分配的样本
  • allocs - 过去全部内存分配的样本
  • threadcreate - 致使建立新 OS 线程的堆栈跟踪
  • block - 致使阻塞同步原语的堆栈跟踪
  • mutex - 争用互斥锁持有者的堆栈跟踪

在查看内存问题时,咱们将专一于堆画像。 allocs 画像和它在关于数据收集方面是相同的。二者之间的区别在于 pprof 工具在启动时读取的方式不同。 allocs 画像将以显示自程序启动以来分配的总字节数(包括垃圾收集的字节)的模式启动 pprof。在尝试提升代码效率时,咱们一般会使用该模式。

简而言之,这是 OS(操做系统)存储咱们代码中对象占用内存的地方。这块内存随后会被“垃圾回收”,或者在非垃圾回收语言中手动释放。

堆不是惟一发生内存分配的地方,一些内存也在栈中分配。栈主要是短周期的内存。在 Go 中,栈一般用于在函数闭包内发生的赋值。 Go 使用栈的另外一个地方是编译器“知道”在运行时须要多少内存(例如固定大小的数组)。有一种方法可使 Go 编译器将栈“转义”到堆中输出分析,但我不会在这篇文章中谈到它。

堆数据须要“释放”和垃圾回收,而栈数据则不须要。这意味着使用栈效率更高。

这是分配不一样位置的内存的简要说明。还有更多内容,但这不在本文的讨论范围以内。

使用 pprof 获取堆数据

获取数据主要有两种方式。第一种一般是把代码加入到测试或分支中,包括导入runtime/pprof,而后调用pprof.WriteHeapProfile(some_file)来写入堆信息。

请注意,WriteHeapProfile是用于运行的语法糖:

// lookup takes a profile name
pprof.Lookup("heap").WriteTo(some_file, 0)
复制代码

根据文档,WriteHeapProfile能够向后兼容。其他类型的画像没有这样的便捷方式,必须使用Lookup()函数来获取其画像数据。

第二个更有意思,是经过 HTTP(基于 Web 的 endpoints)来启用。这容许你从正在运行的 e2e/test 环境中的容器中去提取数据,甚至从“生产”环境中提取数据。这是 Go 运行时和工具集所擅长的部分。整个包文档能够在这里找到,太长不看版,只须要你将它添加到代码中:

import (
  "net/http"
  _ "net/http/pprof"
)
...
func main() {
  ...
  http.ListenAndServe("localhost:8080", nil)
}
复制代码

导入net/http/pprof 的“反作用”是在/debug/pprof的 web 服务器根目录下会注册 pprof endpoint。如今使用 curl 咱们能够获取要查看的堆信息文件:

curl -sK -v http://localhost:8080/debug/pprof/heap > heap.out
复制代码

只有在你的程序以前没有 http listener 时才须要添加上面的http.ListenAndServe()。若是有的话就没有必要再监听了,它会自动处理。还可使用ServeMux.HandleFunc()来设置它,这对于更复杂的 http 程序有意义。

使用 pprof

因此咱们收集了这些数据,如今该干什么呢?如上所述,pprof 有两种主要的内存分析策略。一个是查看当前的内存分配(字节或对象计数),称为inuse。另外一个是查看整个程序运行时的全部分配的字节或对象计数,称为 alloc。这意味着不管它是否被垃圾回收,都会是全部样本的总和。

在这里咱们须要重申一下堆画像文件是内存分配的样例。幕后的pprof使用runtime.MemProfile函数,该函数默认按分配字节每 512KB 收集分配信息。能够修改 MemProfile 以收集全部对象的信息。须要注意的是,这极可能会下降应用程序的运行速度。

这意味着默认状况下,对于在 pprof 监控下抖动的小对象,可能会出现问题。对于大型代码库/长期运行的程序,这不是问题。

一旦收集好画像文件后,就能够将其加载到 pprof 的交互式命令行中了,经过运行:

> go tool pprof heap.out
复制代码

咱们能够观察到显示的信息

Type: inuse_space
Time: Jan 22, 2019 at 1:08pm (IST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
复制代码

这里要注意的事项是Type:inuse_space。这意味着咱们正在查看特定时刻的内存分配数据(当咱们捕获该配置文件时)。type 是sample_index的配置值,可能的值为:

  • inuse_space - 已分配但还没有释放的内存数量
  • inuse_objects - 已分配但还没有释放的对象数量
  • alloc_space - 已分配的内存总量(无论是否已释放)
  • alloc_objects - 已分配的对象总量(无论是否已释放)

如今在交互命令行中输入top,将输出顶级内存消费者

(pprof) top
Showing nodes accounting for 330.04MB, 93.73% of 352.11MB total
Dropped 19 nodes (cum <= 1.76MB)
Showing top 10 nodes out of 56
      flat  flat%   sum%        cum   cum%
  142.02MB 40.33% 40.33%   142.02MB 40.33%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/membuffers/go.(*InternalMessage).lazyCalcOffsets
      28MB  7.95% 48.29%       28MB  7.95%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.TransactionsBlockProofReader (inline)
   26.51MB  7.53% 55.81%    39.01MB 11.08%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.(*ResultsBlockHeaderBuilder).Build
   25.51MB  7.24% 63.06%    32.51MB  9.23%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.(*ResultsBlockProofBuilder).Build
      23MB  6.53% 69.59%       23MB  6.53%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.ResultsBlockHeaderReader (inline)
   20.50MB  5.82% 75.41%    20.50MB  5.82%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.TransactionsBlockMetadataReader (inline)
      20MB  5.68% 81.09%       20MB  5.68%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.TransactionsBlockHeaderReader (inline)
      16MB  4.54% 85.64%       24MB  6.82%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.(*TransactionsBlockHeaderBuilder).Build
   14.50MB  4.12% 89.76%   122.51MB 34.79%  github.com/orbs-network/orbs-network-go/services/gossip/codec.DecodeBlockPairs
      14MB  3.98% 93.73%       14MB  3.98%  github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/orbs-spec/types/go/protocol.ResultsBlockProofReader (inline)
复制代码

咱们能够看到关于Dropped Nodes的一系列数据,这意味着它们被过滤掉了。一个节点或树中的一个“节点”就是一整个对象。丢弃节点有利于咱们更快的找到问题,但有时它可能会隐藏内存问题产生的根本缘由。咱们继续看一个例子。

若是要该画像文件的全部数据,请在运行 pprof 时添加-nodefraction=0选项,或在交互命令行中键入nodefraction=0

在输出列表中,咱们能够看到两个值,flatcum

  • flat 表示堆栈中当前层函数的内存
  • cum 表示堆栈中直到当前层函数所累积的内存

仅仅这个信息有时能够帮助咱们了解是否存在问题。例如,一个函数负责分配了大量内存但没有保留内存的状况。这意味着某些其余对象指向该内存并维护其分配,这说明咱们可能存在系统设计的问题或 bug。

top实际上运行了top10。top 命令支持topN格式,其中N是你想要查看的条目数。在上面的状况,若是键入top70将输出全部节点。

可视化

虽然topN提供了一个文本列表,但 pprof 附带了几个很是有用的可视化选项。能够输入pnggif等等(请参阅go tool pprof -help获取完整信息)。

在咱们的系统上,默认的可视化输出相似于:

这看起来可能有点吓人,但它是程序中内存分配流程(根据堆栈跟踪)的可视化。阅读图表并不像看起来那么复杂。带有数字的白色方块显示已分配的空间(在图形边缘上是它占用内存的数量),每一个更宽的矩形显示调用的函数。

须要注意的是,在上图中,我从执行模式inuse_space中取出了一个 png。不少时候你也应该看看inuse_objects,由于它能够帮助你找到内存分配问题。

深刻挖掘,寻找根本缘由

到目前为止,咱们可以理解应用程序在运行期间内存怎么分配的。这有助于咱们了解咱们程序的行为(或很差的行为)。

在咱们的例子中,咱们能够看到内存由membuffers持有,这是咱们的数据序列化库。这并不意味着咱们在该代码段有内存泄漏,这意味着该函数持有了内存。了解如何阅读图表以及 pprof 输出很是重要。在这个例子中,当咱们序列化数据时,意味着咱们将内存分配给结构和原始对象(int,string),它不会被释放。

跳到结论部分,咱们能够假设序列化路径上的一个节点负责持有内存,例如:

subset of the graph

咱们能够看到日志库中链中的某个地方,控制着>50MB 的已分配内存。这是由咱们的日志记录器调用函数分配的内存。通过思考,这其实是预料之中的。日志记录器会分配内存,是由于它须要序列化数据以将其输出到日志,所以它会形成进程中的内存分配。

咱们还能够看到,在分配路径下,内存仅由序列化持有,而不是任何其余内容。此外,日志记录器保留的内存量约为总量的 30%。综上告诉咱们,最有可能的问题不在于日志记录器。若是它是 100%,或接近它,那么咱们应该一直找下去 - 但事实并不是如此。这可能意味着它记录了一些不该该记录的东西,但不是日志记录器的内存泄漏。

是时候介绍另外一个名为listpprof命令。它接受一个正则表达式,该表达式是内容的过滤器。 “list”其实是与分配相关的带注释的源代码。在咱们能够看到在日志记录器的上下文中将执行list RequestNew,由于咱们但愿看到对日志记录器的调用。这些调用来自刚好以相同前缀开头的两个函数。

(pprof) list RequestNew
Total: 352.11MB
ROUTINE ======================== github.com/orbs-network/orbs-network-go/services/consensuscontext.(*service).RequestNewResultsBlock in /Users/levison/work/go/src/github.com/orbs-network/orbs-network-go/services/consensuscontext/service.go
         0    77.51MB (flat, cum) 22.01% of Total
         .          .     82:}
         .          .     83:
         .          .     84:func (s *service) RequestNewResultsBlock(ctx context.Context, input *services.RequestNewResultsBlockInput) (*services.RequestNewResultsBlockOutput, error) {
         .          .     85:	logger := s.logger.WithTags(trace.LogFieldFrom(ctx))
         .          .     86:
         .    47.01MB     87:	rxBlock, err := s.createResultsBlock(ctx, input)
         .          .     88:	if err != nil {
         .          .     89:		return nil, err
         .          .     90:	}
         .          .     91:
         .    30.51MB     92:	logger.Info("created Results block", log.Stringable("results-block", rxBlock))
         .          .     93:
         .          .     94:	return &services.RequestNewResultsBlockOutput{
         .          .     95:		ResultsBlock: rxBlock,
         .          .     96:	}, nil
         .          .     97:}
ROUTINE ======================== github.com/orbs-network/orbs-network-go/services/consensuscontext.(*service).RequestNewTransactionsBlock in /Users/levison/work/go/src/github.com/orbs-network/orbs-network-go/services/consensuscontext/service.go
         0    64.01MB (flat, cum) 18.18% of Total
         .          .     58:}
         .          .     59:
         .          .     60:func (s *service) RequestNewTransactionsBlock(ctx context.Context, input *services.RequestNewTransactionsBlockInput) (*services.RequestNewTransactionsBlockOutput, error) {
         .          .     61:	logger := s.logger.WithTags(trace.LogFieldFrom(ctx))
         .          .     62:	logger.Info("starting to create transactions block", log.BlockHeight(input.CurrentBlockHeight))
         .    42.50MB     63:	txBlock, err := s.createTransactionsBlock(ctx, input)
         .          .     64:	if err != nil {
         .          .     65:		logger.Info("failed to create transactions block", log.Error(err))
         .          .     66:		return nil, err
         .          .     67:	}
         .          .     68:
         .          .     69:	s.metrics.transactionsRate.Measure(int64(len(txBlock.SignedTransactions)))
         .    21.50MB     70:	logger.Info("created transactions block", log.Int("num-transactions", len(txBlock.SignedTransactions)), log.Stringable("transactions-block", txBlock))
         .          .     71:	s.printTxHash(logger, txBlock)
         .          .     72:	return &services.RequestNewTransactionsBlockOutput{
         .          .     73:		TransactionsBlock: txBlock,
         .          .     74:	}, nil
         .          .     75:}
复制代码

咱们能够看到所作的内存分配位于cum列中,这意味着分配的内存保留在调用栈中。这与图表显示的内容相关。此时很容易看出日志记录器分配内存是由于咱们发送了整个“block”对象形成的。这个对象须要序列化它的某些部分(咱们的对象是 membuffer 对象,它实现了一些String()函数)。它是一个有用的日志,仍是一个好的作法?可能不是,但它不是日志记录器端或调用日志记录器的代码产生了内存泄漏,

listGOPATH路径下搜索能够找到源代码。若是它搜索的根不匹配(取决于你电脑的项目构建),则可使用-trim_path选项。这将有助于修复它并让你看到带注释的源代码。当正在捕获堆配置文件时要将 git 设置为能够正确提交。

内存泄漏缘由

之因此调查是由于怀疑有内存泄漏的问题。咱们发现内存消耗高于系统预期的须要。最重要的是,咱们看到它不断增长,这是“这里有问题”的另外一个强有力的指标。

此时,在 Java 或.Net 的状况下,咱们将打开一些'gc roots'分析或分析器,并获取引用该数据并形成泄漏的实际对象。正如所解释的那样,对于 Go 来讲这是不可能的,由于工具问题也是因为 Go 低等级的内存表示。

没有详细说明,咱们不知道 Go 把哪一个对象存储在哪一个地址(指针除外)。这意味着实际上,了解哪一个内存地址表示对象(结构)的哪一个成员将须要把某种映射输出到堆画像文件。这可能意味着在进行完整的 core dump 以前,还应该采用堆画像文件,以便将地址映射到分配的行和文件,从而映射到内存中表示的对象。

此时,由于咱们熟悉咱们的系统,因此很容易理解这再也不是一个 bug。它(几乎)是设计的。可是让咱们继续探索如何从工具(pprof)中获取信息以找到根本缘由。

设置nodefraction=0时,咱们将看到已分配对象的整个图,包括较小的对象。咱们来看看输出:

memory visualization with nodefraction=0

咱们有两个新的子树。再次提醒,pprof 堆画像文件是内存分配的采样。对于咱们的系统而言 - 咱们不会遗漏任何重要信息。这个较长的绿色新子树的部分是与系统的其他部分彻底断开的测试运行器,在本篇文章中我没有兴趣考虑它。

system was configured to “leak” 😞

较短的蓝色子树,有一条边链接到整个系统是inMemoryBlockPersistance。这个名字也解释了咱们想象的'泄漏'。这是数据后端,它将全部数据存储在内存中而不是持久化到磁盘。值得注意的是,咱们能够看到它持有两个大的对象。为何是两个?由于咱们能够看到对象大小为 1.28MB,函数占用大小为 2.57MB。

这个问题很好理解。咱们可使用 delve(调试器)(译者注:deleve)来查看调试咱们代码中的内存状况。

如何修复

这是一个糟糕的人为错误。虽然这个过程是有教育意义的,咱们能不能作得更好呢?

咱们仍然能“嗅探到”这个堆信息。反序列化的数据占用了太多的内存,为何 142MB 的内存须要大幅减小呢?.. pprof 能够回答这个问题 - 实际上,它确实能够回答这些问题。

要查看函数的带注释的源代码,咱们能够运行list lazy。咱们使用lazy,由于咱们正在寻找的函数名是lazyCalcOffsets(),并且咱们的代码中也没有以 lazy 开头的其余函数。固然输入list lazyCalcOffsets也能够。

(pprof) list lazy
Total: 352.11MB
ROUTINE ======================== github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/membuffers/go.(*InternalMessage).lazyCalcOffsets in /Users/levison/work/go/src/github.com/orbs-network/orbs-network-go/vendor/github.com/orbs-network/membuffers/go/message.go
  142.02MB   142.02MB (flat, cum) 40.33% of Total
         .          .     29:
         .          .     30:func (m *InternalMessage) lazyCalcOffsets() bool {
         .          .     31:	if m.offsets != nil {
         .          .     32:		return true
         .          .     33:	}
      36MB       36MB     34:	res := make(map[int]Offset)
         .          .     35:	var off Offset = 0
         .          .     36:	var unionNum = 0
         .          .     37:	for fieldNum, fieldType := range m.scheme {
         .          .     38:		// write the current offset
         .          .     39:		off = alignOffsetToType(off, fieldType)
         .          .     40:		if off >= m.size {
         .          .     41:			return false
         .          .     42:		}
  106.02MB   106.02MB     43:		res[fieldNum] = off
         .          .     44:
         .          .     45:		// skip over the content to the next field
         .          .     46:		if fieldType == TypeUnion {
         .          .     47:			if off + FieldSizes[TypeUnion] > m.size {
         .          .     48:				return false
复制代码

咱们能够看到两个有趣的信息。一样,请记住 pprof 堆画像文件会对有关分配的信息进行采样。咱们能够看到flatcum数字是相同的。这代表分配的内存也在这些分配点被保留。

接下来,咱们能够看到make()占用了一些内存。这是很正常的,它是指向数据结构的指针。然而,咱们也看到第 43 行的赋值占用了内存,这意味着它分配了内存。

这让咱们学习了映射 map,其中 map 的赋值不是简单的变量赋值。本文详细介绍了 map 的工做原理。简而言之,map 与切片相比,map 开销更大,“成本”更大,元素更多。

接下来应该保持警戒:若是内存消费是一个相关的考虑因素的话,当数据不稀疏或者能够转换为顺序索引时,使用map[int]T也没问题,可是一般应该使用切片实现。然而,当扩容一个大的切片时,切片可能会使操做变慢,在 map 中这种变慢能够忽略不计。优化没有万能的方法。

在上面的代码中,在检查了咱们如何使用该 map 以后,咱们意识到虽然咱们想象它是一个稀疏数组,但它并非那么稀疏。这与上面描述的状况匹配,咱们能立刻想到一个将 map 改成切片的小型重构其实是可行的,而且可能使该代码内存效率更好。因此咱们将其改成:

func (m *InternalMessage) lazyCalcOffsets() bool {
	if m.offsets != nil {
		return true
	}
	res := make([]Offset, len(m.scheme))
	var off Offset = 0
	var unionNum = 0
	for fieldNum, fieldType := range m.scheme {
		// write the current offset
		off = alignOffsetToType(off, fieldType)
		if off >= m.size {
			return false
		}
		res[fieldNum] = off
复制代码

就这么简单,咱们如今使用切片替代了 map。因为咱们接收数据的方式是懒加载进去的,而且咱们随后如何访问这些数据,除了这两行和保存该数据的结构以外,不须要修改其余代码。这些修改对内存消耗有什么影响?

让咱们来看看benchcmp的几回测试

benchmark                       old ns/op     new ns/op     delta
BenchmarkUint32Read-4           2047          1381          -32.54%
BenchmarkUint64Read-4           507           321           -36.69%
BenchmarkSingleUint64Read-4     251           164           -34.66%
BenchmarkStringRead-4           1572          1126          -28.37%

benchmark                       old allocs     new allocs     delta
BenchmarkUint32Read-4           14             7              -50.00%
BenchmarkUint64Read-4           4              2              -50.00%
BenchmarkSingleUint64Read-4     2              1              -50.00%
BenchmarkStringRead-4           12             6              -50.00%

benchmark                       old bytes     new bytes     delta
BenchmarkUint32Read-4           1120          80            -92.86%
BenchmarkUint64Read-4           320           16            -95.00%
BenchmarkSingleUint64Read-4     160           8             -95.00%
BenchmarkStringRead-4           960           32            -96.67%
复制代码

读取测试的初始化建立分配的数据结构。咱们能够看到运行时间提升了约 30%,内存分配降低了 50%,内存消耗提升了> 90%(!)

因为切片(以前是 map)从未添加过不少数据,所以这些数字几乎显示了咱们将在生产中看到的内容。它取决于数据熵,但可能在内存分配和内存消耗还有提高的空间。

从同一测试中获取堆画像文件来看一下pprof,咱们将看到如今内存消耗实际上降低了约 90%。

须要注意的是,对于较小的数据集,在切片知足的状况就不要使用 map,由于 map 的开销很大。

完整的 core dump

如上所述,这就是咱们如今看到工具受限制的地方。当咱们调查这个问题时,咱们相信本身可以找到根对象,但没有取得多大成功。随着时间的推移,Go 会以很快的速度发展,但在彻底转储或内存表示的状况下,这种演变会带来代价。完整的堆转储格式在修改时不向后兼容。这里描述的最新版本和写入完整堆转储,可使用debug.WriteHeapDump()

虽然如今咱们没有“陷入困境”,由于没有很好的解决方案来探索彻底转储(full down)。 目前为止,pprof回答了咱们全部的问题。

请注意,互联网会记住许多再也不相关的信息。若是你打算尝试本身打开一个完整的转储,那么你应该忽略一些事情,从 go1.11 开始:

  • 没有办法在 MacOS 上打开和调试完整的 core dump,只有 Linux 能够。
  • github.com/randall77/h…上的工具适用于 Go1.3,它存在 1.7+的分支,但它也不能正常工做(不完整)。
  • github.com/golang/debu…上查看并不真正编译。它很容易修复(内部的包指向 golang.org 而不是 github.com),可是,在 MacOS 或者 Linux 上可能都不起做用。
  • 此外,github.com/randall77/c…在 MacOS 也会失败

pprof UI

关于 pprof,要注意的一个细节是它的 UI 功能。在开始调查与使用 pprof 画像文件相关的问题时能够节省大量时间。(译者注:须要安装 graphviz)

go tool pprof -http=:8080 heap.out
复制代码

此时它应该打开 Web 浏览器。若是没有,则浏览你设置的端口。它使你可以比命令行更快地更改选项并得到视觉反馈。消费信息的一种很是有用的方法。

UI 确实让我熟悉了火焰图,它能够很是快速地暴露代码的罪魁祸首。

结论

Go 是一种使人兴奋的语言,拥有很是丰富的工具集,你能够用 pprof 作更多的事情。例如,这篇文章没有涉及到的 CPU 分析。

其余一些好的文章:

  • rakyll.org/archive/ - 我相信这是围绕性能监控的主要贡献者之一,她的博客上有不少好帖子
  • github.com/google/gops - 由JBD(运行 rakyll.org)编写,此工具保证是本身的博客文章。
  • medium.com/@cep21/usin… - go tool trace是用来作 CPU 分析的,这是一个关于该分析功能的不错的帖子。
相关文章
相关标签/搜索