0%

go 内存泄漏,pprof 够用了么

pprof 确实很好用,设计实现都很精巧,半年前写过一篇,go 语言 pprof heap profile 实现机制
用 pprof 来分析内存泄漏,通常情况下,是够用了,不过,有时候也不够用~
为啥呢,因为 pprof 只是记录了内存对象被创建时的调用栈,并没有引用关系。也就是说,没有办法知道,内存对象是因为被谁引用了,导致没有被释放。
对此,同事元总有一个很形象的比喻,pprof 只能看到出生证,却查不了暂住证。

需要引用关系

有些场景下,我们知道了泄漏的内存,是从哪里申请的,但是翻了半天代码,也搞不清楚内存为啥没有释放。
比如,内存对象经过复杂的调用传递,或者复杂的内存池复用机制,又或者传给了某个不熟悉第三方库,在第三方库中有非预期的使用 …
这些情况下,一个很直觉的想法是,想看看这些内存对象的引用关系

内存引用关系火焰图

内存引用关系火焰图,是一种内存对象引用关系的可视化方式,由春哥首创,最早应用于 OpenResty XRay 产品。这个工具确实是内存分析神器,给不少的客户定位过内存问题,感兴趣的可以移步 OpenResty 官方博客
下图是分析一个 MOSN 服务产生的,从下到上表示的是,从 GC root 到 GC object 的引用关系链,宽度表示的是对象大小(也包括其引用的对象的大小之和)
有了这样的可视化结果,我们可以直观的看到内存对象的引用关系。
图中可以看到,大部分的内存是,MOSN 中 cluster_manager 全局变量中引用的 cluster 内存对象:

内存引用关系火焰图

实现原理

在生成火焰图之前,首先我们需要提取两个关键信息:

  1. 每个内存对象之间的引用关系
  2. 每个内存对象的类型

引用关系

获取引用关系比较简单,首先,我们可以在 heap 中找到所有的 GC 对象。然后遍历所有的对象,再结合 bitmap 信息,获取这个对象引用的其他对象。
基本原理跟 GC mark 是类似的,不过实现上很不一样,因为这个是离线工具,可以简单粗暴的实现。

类型推导

Go 语言作为编译型静态语言,是不需要为每个内存对象存储类型信息的(有点例外的是 interface)。如果是动态类型语言,比如 Lua,则会方便很多,每个 GC 对象都存储了对象的类型。
所以,要获取每个对象的类型,还是比较麻烦的,也是投入时间最多的一块~
当然,也是有解决办法,简单来说就是做逆向类型推导,根据已知内存的类型信息,推导被引用的内存对象的类型信息。
这块还是比较复杂的,后面有空可以单独写一篇来分享~

生成过程

有了这两个关键信息之后,生成过程还是比较清晰的:

  1. 获取所有的内存对象,包括类型,大小,以及他们之间的引用关系,形成一个图
  2. 从 root 对象出发,按照层次遍历,形成一棵树(也就是剪枝过程,每个对象只能被引用一次)
  3. 将这棵树的完整引用关系,当做 backtrace dump 下来
    count 是当前节点的总大小(包括所有子节点),也就是火焰图上的宽度
  4. 从 bt 文件生成 svg,这一步是 brendangregg 的 FlameGraph 标准工具链

使用方式

这个工具是基于 go 官方的 debug 改进来的,不过鉴于 go 官方不那么热心维护 viewcore 了,MOSN 社区先 fork 了一份,搞了个 mosn 分支,作为 MOSN 社区维护的主分支。
待 go 官方先接受了我们之前提的 bugfix 之后,我们再去提交这个 feature。
所以,使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 编译 mosn 维护的 viewcore
git clone git@github.com:mosn/debug.git
cd debug/cmd/viewcore
go build .

# 假设已经有了一个 core 文件(CORE-FILE)
# 以及对应的可执行程序文件(BIN-FILE)
viewcore CORE-FILE --exe BIN-FILE objref ref.bt

# 下载 FlameGraph 工具
git clone git@github.com:brendangregg/FlameGraph.git
../FlameGraph/stackcollapse-stap.pl ref.bt | ../FlameGraph/flamegraph.pl > ref.svg

# 浏览器打开 ref.svg 即可看到火焰图

如果使用碰到问题,欢迎联系~
如果成功定位了某个问题,也欢迎反馈给我们,一起开心下的~

广告

如果觉得有意思,欢迎关注我的公众号~

微信公众号