go 使用pprof 排查内存泄露

go 是自带gc的语言,会自动管理内存,不用像C/C++那样,需要程序员手动释放内存,不用手动管理内存,就能少掉很多头发

go的GC会自动管理内存,但是这不代表go程序就不会内存泄露了。 go常见产生内存泄露的原因就是goroutine没有结束,简单说就是goroutine 被阻塞了,这样就会导致goroutine引用的内存不被GC回收,也就导致了内存泄露。
当然产生内存泄露的原因还有别的,只是暂时我还没有遇到。不管什么原因产生的内存泄露,最终都是因为异常的引用,导致该被回收的内存没有被gc 回收掉

起因

说起go内存泄露分析,还要从年前的一次程序压测说起。我用一个测试程序压测我们游戏的一些数据,大约开了3000个tcp连接到游戏。游戏数据没有问题,但是当测试结束后,发现游戏的Gateway内存占用一直没有下降。本能的让我想起了是不是内存泄露了。马上用pprof分析了一下内存,发现果然是内存泄露了。

因为时公司代码,不方便拿出了分析,大体说一下原因吧

Gateway是一个读写分离的tcp服务,也就是每一个连接都要有两个goroutine,一个读,一个写。

但是当tcp连接断开时,因为时序问题,导致goroutine阻塞了,一直没有结束,就是导致了相关联的内存没有释放。

因为时公司代码,不能随便贴出来,这次就自己写个简单的demo模拟一下吧

因为go 自带的pprof 只能展示文字,不太明显,所有先安装一个可视化插件 graphviz 传送门
linux上可以直接通过 apt 或者 yum 安装就行了。
windows上去网站下一个就好了,我下载 .msi 格式的安装后不能用,重新下了一个 压缩包,解压后把 bin 目录配置到环境变量的 path 中就可以使用了

开始

写一个简单的demo 模拟一下内存泄露

package main

import (
    "math/rand"
    "net/http"
    _ "net/http/pprof"
    "sync"
)

func main() {
    go func() {
        http.ListenAndServe("0.0.0.0:8090", nil)
    }()

    c := make(chan struct{})
    var wg sync.WaitGroup
    wg.Add(1)
    for i := 0; i < 10000; i++ {
        go one(c)
    }
    wg.Wait()
}

func one(c chan struct{}) {
    var a []int64
    for i := 0; i < 10000; i++ {
        a = append(a, rand.Int63())
    }
    
    <-c
}

程序很简单,在 for 循环中开启 1000 个协程,每个协程中往切片中append 1000个数据。
用一个channel模拟协程阻塞,这样就会导致goroutine不会结束。

使用go再带的pprof查看

代码中监听了 8090 端口,在浏览器中输入 http://127.0.0.1:8090/debug/pprof/

下面都有解释,就不用详细介绍了,挑一两个说一下

  • allocs : 程序运行期间,所有内存分配的样本
  • heap : 当前活动对象的内存分配采样
  • guroutine : 当前所有协程的堆栈跟踪

可以看到,当前的程序总共有10005个协程,21次堆内存分配,说明我们的协程是被阻塞的。

使用graphviz查看

在命令行中,在Windows中可以使用 powerShell
输入 go tool pprof -http :8081 http://127.0.0.1:8090/debug/pprof/heap

会在浏览器中打开

这就是当前 heap 采样图
可以在 VIEW 中切换不同的显示方式

图中方块越大便是占用的内存越多,方块中连线越粗表示耗时越多

内存泄露误区

我在排查内存泄露时,当我把goroutine阻塞解决后,通过linux 的 top 命令查看Gateway内存占用时,发现内存没有降下来,一时让我陷入困惑,为啥goroutine 都结束了,为啥内存还不释放呢?直到我在网上找到了这篇文章 传送门
这位大神写的golang 相关的文章,是目前我在网上找到的最牛逼的之一,文章不光有深度,而且通俗易懂。

重新验证

修改上面的代码

package main

import (
    "math/rand"
    "net/http"
    _ "net/http/pprof"
    "sync"
    "time"
)

func main() {
    go func() {
        http.ListenAndServe("0.0.0.0:8090", nil)
    }()

    var wg sync.WaitGroup
    wg.Add(1)
    for i := 0; i < 10000; i++ {
        go one()
    }
    wg.Wait()
}

func one() {
    var a []int64
    for i := 0; i < 10000; i++ {
        a = append(a, rand.Int63())
    }
    time.Sleep(time.Second * 1)
}

现在 go 出来的的函数 one 不再一直阻塞,而是只会阻塞5秒

把程序运行起来看一下

等一段时间后,发现goroutine数量已经下来了,说明阻塞的协程都已经结束了,如图


但是通过任务管理器中,看到程序还是占用着大量的内存


任务管理器

这时点击 heap 查看具体的堆内存情况,拉到最低下,看到一堆参数

参数很多,但是重点关注被框出来的那几个就好了,详细的分析在大神的文章中有分析,在go的库文件中也能找到,里面有详细的注释。
路径 src->runtime->mstats.go 文件中有 MemStats 的定义和注释

这些参数的单位都是字节

  • TotalAlloc : 所有对象分配的总和,整个程序运行期间,只增不减
  • HeapAlloc : 分配的堆对象的字节数,包括可访问的对象和未被GC回收的不可访问的对象,这个值会动态变化,分配对象时增加,回收对象后减少
  • HeapIdle : 闲置的span中的字节数,这些span中已经没有对象了(不用了),但是现在还没有还给操作系统,这些span可以重新用来分配heap和stack。HeapIdle 减去 HeapReleased 的值可以当作 "可以返回到操作系统但由运行时保留的内存量",也就是说,go的runtime可以在不像操作系统申请内存的情况下,也可以分配heap空间,这样可以提高程序性能
  • HeapInuse : 正在使用的span的字节大小
  • HeapReleased : 是返还给操作系统的物理内存的字节数

回到我们的测试程序中,当所有的goroutine都结束时,GC会开始回收切片,但是被回收的内存不会直接换给操作系统,而是由go的runtime暂时保管(也就是 HeapIdle 值会变大),接下来如果再次需要分配空间,go的runtime可以不向操作系统申请内存,直接从自己保管的闲置内存中分配,这样可以提高程序性能。至于GO的runtime什么时候把这部分内存还给操作系统,不同的分配策略和不同的系统不太一样,这个有点深,我还没有深入研究这些。不过 传送门 的文中有介绍 MADV_FREE 有兴趣可以自己学习一下

总结

  • go 虽然有GC,但是使用不当也会导致内存泄露
  • 不同的操作系统和不用策略,会导致go程序的内存已经被回收了,但是没有及时的归还给操作系统

由于水平有限,文中如有谬处,还请在评论区不吝赐教

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容