当前位置:首页 > 机械智慧 > 正文

Go会出现内存泄露吗?如何避免?学会本文的方法足矣

最近解决了我们项目中的一个内存泄露问题,事实再次证明pprof是一个好工具,但掌握好工具的正确用法,才能发挥好工具的威力,不然就算你手里有屠龙刀,也成不了天下第一,本文就是带你用pprof定位内存泄露问题。关于Go的内存泄露有这么一句话不知道你听过没有:我所解决的问题,也是goroutine泄露导致...

最近解决了我们项目中的一个内存泄露问题,事实再次证明pprof是一个好工具,但掌握好工具的正确用法,才能发挥好工具的威力,不然就算你手里有屠龙刀,也成不了天下第一,本文就是带你用pprof定位内存泄露......


最近解决了我们项目中的一个内存泄露问题,事实再次证明pprof是一个好工具,但掌握好工具的正确用法,才能发挥好工具的威力,不然就算你手里有屠龙刀,也成不了天下第一,本文就是带你用pprof定位内存泄露问题。

关于Go的内存泄露有这么一句话不知道你听过没有:

我所解决的问题,也是goroutine泄露导致的内存泄露,所以这篇文章主要介绍Go程序的goroutine泄露,掌握了如何定位和解决goroutine泄露,就掌握了内存泄露的大部分场景。

gopprof基本知识

定位goroutine泄露会使用到pprof,pprof是Go的性能工具,在开始介绍内存泄露前,先简单介绍下pprof的基本使用,更详细的使用给大家推荐了资料。

什么是pprof

pprof是Go的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等,当需要性能调优或者定位Bug时候,这些记录的信息是相当重要。

基本使用

使用pprof有多种方式,Go已经现成封装好了1个:net/http/pprof,使用简单的几行命令,就可以开启pprof,记录运行信息,并且提供了Web服务,能够通过浏览器和命令行2种方式获取运行数据。

看个最简单的pprof的例子:

文件:golang_step_by_step/pprof/pprof/

packagemainimport("fmt""net/http"_"net/http/pprof")funcmain(){//开启pprof,监听请求ip:="0.0.0.0:6060"iferr:=(ip,nil);err!=nil{("startpproffailedon%s\n",ip)}}

提醒:本文所有代码部分可左右滑动

浏览器方式


输入网址ip:port/debug/pprof/打开pprof主页,从上到下依次是5类profile信息:

block:goroutine的阻塞信息,本例就截取自一个goroutine阻塞的demo,但block为0,没掌握block的用法

goroutine:所有goroutine的信息,下面的fullgoroutinestackdump是输出所有goroutine的调用栈,是goroutine的debug=2,后面会详细介绍。

heap:堆内存的信息

mutex:锁的信息

threadcreate:线程信息

命令行方式

当连接在服务器终端上的时候,是没有浏览器可以使用的,Go提供了命令行的方式,能够获取以上5类信息,这种方式用起来更方便。

使用命令gotoolpprofurl可以获取指定的profile文件,此命令会发起http请求,然后下载数据到本地,之后进入交互式模式,就像gdb一样,可以使用命令查看运行信息,以下是5类请求的方式:

30-secondCPUprofilegotoolpprofhttp://localhost:6060/debug/pprof/profile?seconds=120下载heapprofilegotoolpprofhttp://localhost:6060/debug/pprof/heap下载goroutineprofilegotoolpprofhttp://localhost:6060/debug/pprof/goroutine下载blockprofilegotoolpprofhttp://localhost:6060/debug/pprof/block下载mutexprofilegotoolpprofhttp://localhost:6060/debug/pprof/mutex

上面的pprof/太简单了,如果去获取内存profile,几乎获取不到什么,换一个Demo进行内存profile的展示:

文件:golang_step_by_step/pprof/heap/

//展示内存增长和pprof,并不是泄露packagemainimport("fmt""net/http"_"net/http/pprof""os""time")//运行一段时间:fatalerror:runtime:outofmemoryfuncmain(){//开启pprofgofunc(){ip:="0.0.0.0:6060"iferr:=(ip,nil);err!=nil{("startpproffailedon%s\n",ip)(1)}}()tick:=(/100)varbuf[]byteforrangetick{buf=app(buf,make([]byte,1024*1024))}}

上面这个demo会不断的申请内存,把它编译运行起来,然后执行:

$gotoolpprofhttp://localhost:6060/debug/pprof/heapFetchingprofileoverHTTPfromhttp://localhost:6060/debug/pprof/heapSavedprofilein/home/ubuntu/pprof/____//---下载到的内存profile文件File:demo//程序名称BuildID:a9069a125ee9c0df3713b2149ca859e8d4d11d5aType:inuse_spaceTime:May16,2019at8:55pm(CST)Enteringinteractivemode(type"help"forcommands,"o"foroptions)(pprof)(pprof)(pprof)help//使用help打印所有可用命令Commands:callgrindOutputsagraphincallgrindformatcommentsOutputallprofilecommentsdisasmOutputassemblylistingsannotatedwithsamplesdotOutputsagraphinDOTformateogVisualizegraphthrougheogevinceVisualizegraphthroughevincegifOutputsagraphimageinGIFformatgvVisualizegraphthroughgvkcachegrindVisualizereportinKCachegrindlistOutputannotatedsourceforfunctionsmatchingregexppdfOutputsagraphinPDFformatpeekOutputcallers/calleesoffunctionsmatchingregexppngOutputsagraphimageinPNGformatprotoOutputstheprofileincompressedprotobufformatpsOutputsagraphinPSformatrawOutputsatextrepresentationoftherawprofilesvgOutputsagraphinSVGformattagsOutputsalltagsintheprofiletextOutputstopentriesintextformtopOutputstopentriesintextformtopprotoOutputstopentriesincompressedprotobufformattracesOutputsallprofilesamplesintextformtreeOutputsatextreringofcallgraphwebVisualizegraphthroughwebbrowserweblistDisplayannotatedsourceinawebbrowsero/optionsListoptionsandtheircurrentvaluesquit/exit/^DExitpprof.

下载得到的文件:/home/ubuntu/pprof/____,这其中包含了程序名demo,profile类型alloc已分配的内存,inuse代表使用中的内存。

help可以获取帮助,最先会列出支持的命令,想掌握pprof,要多看看,多尝试。

关于命令,本文只会用到3个,我认为也是最常用的:top、list、traces,分别介绍一下。

top

按指标大小列出前10个函数,比如内存是按内存占用多少,CPU是按执行时间多少。

(pprof),100%%sum%cumcum%814.62MB100%100%814.62MB100%%100%814.62MB100%

top会列出5个统计数据:

flat:本函数占用的内存量。

flat%:本函数内存占使用中内存总量的百分比。

sum%:前面每一行flat百分比的和,比如第2行虽然的100%是100%+0%。

cum:是累计量,加入main函数调用了函数f,函数f占用的内存量,也会记进来。

cum%:是累计量占总量的百分比。

list

查看某个函数的代码,以及该函数每行代码的指标信息,如果函数名不明确,会进行模糊匹配,比如listmain会列出和。

(pprof)//精确列出函数Total:814.62MBROUTINE========================/home/ubuntu/heap/(flat,cum)100%ofTotal..20:}()..21:..22:tick:=(/100)..23:varbuf[]byte..24:forrangetick{814.62:buf=app(buf,make([]byte,1024*1024))..26:}..27:}..28:(pprof)listmain//匹配所有函数名带main的函数Total:814.62MBROUTINE========================/home/ubuntu/heap/(flat,cum)100%ofTotal..20:}()..21:..//省略几行..28:ROUTINE========================/usr/lib//src/runtime/(flat,cum)100%ofTotal..193://Aprogramcompiledwith-buildmode=c-archiveorc-shared..//省略几行

可以看到在中的第25行占用了814.62MB内存,左右2个数据分别是flat和cum,含义和top中解释的一样。

traces

打印所有调用栈,以及调用栈的指标信息。

(pprof)tracesFile:demo2Type:inuse_spaceTime:May16,2019at7:08pm(CST)-----------+-------------------------------------------------------bytes:813.46+-------------------------------------------------------bytes:650.77//省略几十行

每个-----隔开的是一个调用栈,能看到调用了,并且中占用了813.46MB内存。

其他的profile操作和内存是类似的,这里就不展示了。

这里只是简单介绍本文用到的pprof的功能,pprof功能很强大,也经常和benchmark结合起来,但这不是本文的重点,所以就不多介绍了,为大家推荐几篇文章,一定要好好研读、实践:

Go官方博客关于pprof的介绍,很详细,也包含样例,可以实操:ProfilingGoPrograms。

跟煎鱼也讨论过pprof,煎鱼的这篇文章也很适合入门:Golang大杀器之性能剖析PProf。

什么是内存泄露

内存泄露指的是程序运行过程中已不再使用的内存,没有被释放掉,导致这些内存无法被使用,直到程序结束这些内存才被释放的问题。

Go虽然有GC来回收不再使用的堆内存,减轻了开发人员对内存的管理负担,但这并不意味着Go程序不再有内存泄露问题。在Go程序中,如果没有Go语言的编程思维,也不遵守良好的编程实践,就可能埋下隐患,造成内存泄露问题。

怎么发现内存泄露

在Go中发现内存泄露有2种方法,一个是通用的监控工具,另一个是gopprof:

监控工具:固定周期对进程的内存占用情况进行采样,数据可视化后,根据内存占用走势(持续上升),很容易发现是否发生内存泄露。

gopprof:适合没有监控工具的情况,使用Go提供的pprof工具判断是否发生内存泄露。

这2种方式分别介绍一下。

监控工具查看进程内在占用情况

如果使用云平台部署Go程序,云平台都提供了内存查看的工具,可以查看OS的内存占用情况和某个进程的内存占用情况,比如阿里云,我们在1个云主机上只部署了1个Go服务,所以OS的内存占用情况,基本是也反映了进程内存占用情况,OS内存占用情况如下,可以看到随着时间的推进,内存的占用率在不断的提高,这是内存泄露的最明显现象:


如果没有云平台这种内存监控工具,可以制作一个简单的内存记录工具。

1、建立一个脚本prog_,获取进程占用的物理内存情况,脚本内容如下:

复制

0+0xf8/home/ubuntu/heap/leak_:53#0+0x2a/home/ubuntu/heap/leak_:54

根据上面的提示,就能判断32015个goroutine运行到leak_的53行:

funcalloc2(outChchan-int){func(){("alloc-fmexit")//分配内存,假用一下buf:=make([]byte,1024*1024*10)_=len(buf)("allocdone")outCh-0//53行}()}

阻塞的原因是outCh这个写操作无法完成,outCh是无缓冲的通道,并且由于以下代码是死代码,所以goroutine始终没有从outCh读数据,造成outCh阻塞,进而造成无数个alloc2的goroutine阻塞,形成内存泄露:

iffalse{-outCh}

方式二

url请求中设置debug=2:

http://ip:port/debug/pprof/goroutine?debug=2


第2种方式和第1种方式是互补的,它可以看到每个goroutine的信息:

goroutine20[chans,2minutes]:20是goroutineid,[]中是当前goroutine的状态,阻塞在写channel,并且阻塞了2分钟,长时间运行的系统,你能看到阻塞时间更长的情况。

同时,也可以看到调用栈,看当前执行停到哪了:leak_的53行,

goroutine20[chans,2minutes]:(0xc42015e060)/home/ubuntu/heap/leak_:53+0xf9//这(0xc42015e060)/home/ubuntu/heap/leak_:54+0/home/ubuntu/heap/leak_:42+0x3f

命令行交互式方法

Web的方法是简单粗暴,无需登录服务器,浏览器打开看看就行了。但就像前面提的,没有浏览器可访问时,命令行交互式才是最佳的方式,并且也是手到擒来,感觉比Web一样方便。

命令行交互式只有1种获取goroutineprofile的方法,不像Web网页分debug=1和debug=22中方式,并将profile文件保存到本地:

//注意命令没有`debug=1`,debug=1,加debug有些版本的go不支持$gotoolpprof(CST)Enteringinteractivemode(type"help"forcommands,"o"foroptions)(pprof)

命令行只需要掌握3个命令就好了,上面介绍过了,详细的倒回去看top,list,traces:

top:显示正运行到某个函数goroutine的数量

traces:显示所有goroutine的调用栈

list:列出代码详细的信息。

我们依然使用leak_这个demo,

$__:leak_demoType:goroutineTime:May16,2019at2:44pm(CST)Enteringinteractivemode(type"help"forcommands,"o"foroptions)(pprof)(pprof)(pprof)topShowingnodesaccountingfor20312,100%of20312totalflatflat%sum%cumcum%20312100%100%20312100%%100%20312100%%100%20312100%%100%20312100%%100%20312100%%100%20312100%(pprof)(pprof)tracesFile:leak_demoType:goroutineTime:May16,2019at2:44pm(CST)-----------+-------------------------------------------------------20312//channel发送//alloc2中的匿名函数+-------------------------------------------------------

top命令在怎么确定是goroutine泄露引发的内存泄露介绍过了,直接看traces命令,traces能列出002中比001中多的那些goroutine的调用栈,这里只有1个调用栈,有20312个goroutine都执行这个调用路径,可以看到alloc2中的匿名函数调用了写channel的操作,然后阻塞挂起了goroutine,使用list列出的代码,显示有20312个goroutine阻塞在53行:

(pprof):20312ROUTINE========================/home/ubuntu/heap/leak_(flat,cum)100%ofTotal..48://分配内存,假用一下..49:buf:=make([]byte,1024*1024*10)..50:_=len(buf)..51:("allocdone")..52:.2031253:outCh-0//看这..54:}()..55:}..56:

友情提醒:使用list命令的前提是程序的源码在当前机器,不然可没法列出源码。服务器上,通常没有源码,那我们咋办呢?刚才介绍了Web查看的方式,那里会列出代码行数,我们可以使用wget下载网页:

$wgethttp://localhost:6060/debug/pprof/goroutine?debug=1

下载网页后,使用编辑器打开文件,使用关键字进行搜索,找到与当前相同的调用栈,就可以看到该goroutine阻塞在哪一行了,不要忘记使用debug=2还可以看到阻塞了多久和原因,Web方式中已经介绍了,此处省略代码几十行。

总结

文章略长,但全是干货,感谢阅读到这。然读到着了,跟定很想掌握pprof,建议实践一把,现在和大家温习一把本文的主要内容。

goroutine泄露的本质

goroutine泄露的本质是channel阻塞,无法继续向下执行,导致此goroutine关联的内存都无法释放,进一步造成内存泄露。

goroutine泄露的发现和定位

利用好gopprof获取goroutineprofile文件,然后利用3个命令top、traces、list定位内存泄露的原因。

goroutine泄露的场景

泄露的场景不仅限于以下两类,但因channel相关的泄露是最多的。

channel的读或者写:

无缓冲channel的阻塞通常是写操作因为没有读而阻塞

有缓冲的channel因为缓冲区满了,写操作阻塞

期待从channel读数据,结果没有goroutine写

select操作,select里也是channel操作,如果所有case上的操作阻塞,goroutine也无法继续执行。

编码goroutine泄露的建议

为避免goroutine泄露造成内存泄露,启动goroutine前要思考清楚:

goroutine如何退出?

是否会有阻塞造成无法退出?如果有,那么这个路径是否会创建大量的goroutine?

示例源码

本文所有示例源码,及历史文章、代码都存储在Github,阅读原文可直接跳转,Github:。

推荐阅读

这些既是参考资料也是推荐阅读的文章,不容错过。

【GoBlog关于pprof详细介绍和Demo】

【Dave关于高性能Go程序的workshop】

【Golang大杀器之性能剖析PProf】

【SO上goroutine调用栈各字段的介绍】

【我的老文,有的介绍,想学习调度器,可以看下系列文章Go调度器系列(2)宏观看调度器】(2)宏观看调度器

最新文章