Go 实战Go内存泄漏,pprof定位内存泄漏问题

  • Post author:
  • Post category:其他




引言:



最近解决了我们项目中的一个



内存泄露



问题,事实再次证明


pprof


是一个好工具,但掌握好工具的正确用法,才能发挥好工具的威力,不然就算你手里有屠龙刀,也成不了天下第一,本文就是带你用


pprof


定位内存泄露问题。



关于


Go


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



10


次内存泄露,有


9


次是


goroutine


泄露。



我所解决的问题,也是


goroutine


泄露导致的内存泄露,所以

这篇文章主要介绍




Go




程序的




goroutine




泄露,掌握了如何定位和解决




goroutine




泄露,就掌握了内存泄露的大部分场景







本文草稿最初数据都是生产坏境数据,为了防止敏感内容泄露,全部替换成了


demo


数据,


demo


的数据比生产环境数据简单多了,更适合入门理解,有助于掌握


pprof







go pprof


基本知识





定位


goroutine


泄露会使用到


pprof





pprof





Go


的性能工具,在开始介绍内存泄露前,先简单介绍下


pprof


的基本使用,更详细的使用给大家推荐了资料。






什么是pprof





pprof





Go


的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是


CPU


使用情况、内存使用情况、


goroutine


运行情况等,当需要性能调优或者定位


Bug


时候,这些记录的信息是相当重要。






基本使用





使用


pprof


有多种方式,


Go


已经现成封装好了


1


个:



net/http/pprof



,使用简单的几行命令,就可以开启


pprof


,记录运行信息,并且提供了


Web


服务,能够通过浏览器和命令行


2


种方式获取运行数据。




什么是内存泄露?




内存泄露指的是程序运行过程中已不再使用的内存,没有被释放掉


,导致这些内存无法被使用,直到程序结束这些内存才被释放的问题。



Go


虽然有


GC


来回收不再使用的堆内存,减轻了开发人员对内存的管理负担,但这并不意味着


Go


程序不再有内存泄露问题。在


Go


程序中,如果没有


Go


语言的编程思维,也不遵守良好的编程实践,就可能埋下隐患,造成内存泄露问题。




怎么发现内存泄露?








Go


中发现内存泄露有


2


种方法,一个是通用的监控工具,另一个是


go pprof







  1. 监控工具



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




  2. go pprof



    :适合没有监控工具的情况,使用


    Go


    提供的


    pprof


    工具判断是否发生内存泄露。






2


种方式分别介绍一下




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






如果使用云平台部署




Go




程序



,云平台都提供了内存查看的工具,可以查看


OS


的内存占用情况和某个进程的内存占用情况,比如阿里云,我们在


1


个云主机上只部署了


1





Go


服务,所以


OS


的内存占用情况,基本是也反映了进程内存占用情况,


OS


内存占用情况如下,可以看到

随着时间的推进,内存的占用率在不断的提高,这是内存泄露的最明显现象






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




1


、建立一个脚本



prog_mem.sh



,获取进程占用的物理内存情况,脚本内容如下:



复制

1
2
3
4
5
#!/bin/bash
prog_name="your_programe_name"
prog_mem=$(pidstat  -r -u -h -C $prog_name |awk 'NR==4{print $12}')
time=$(date "+%Y-%m-%d %H:%M:%S")
echo $time"\tmemory(Byte)\t"$prog_mem >>~/record/prog_mem.log



2


、然后使用



crontab



建立定时任务,每分钟记录


1


次。使用



crontab -e



编辑


crontab


配置,在最后增加


1


行:



复制

1
*/1 * * * * ~/record/prog_mem.sh



脚本输出的内容保存在



prog_mem.log



,只要大体浏览一下就可以发现内存的增长情况,判断是否存在内存泄露。如果需要可视化,可以直接黏贴



prog_mem.log



内容到


Excel


等表格工具,绘制内存占用图。







go pprof


发现存在内存问题





有情提醒:如果对


pprof


不了解,可以先看


go pprof基本知识


,这是下一节,看完再倒回来看。



如果你


Google


或者百度,


Go


程序内存泄露的文章,它总会告诉你使用



pprof heap



,能够生成漂亮的调用路径图,火焰图等等,然后你根据调用路径就能定位内存泄露问题,我最初也是对此深信不疑,尝试了若干天后,只是发现内存泄露跟某种场景有关,根本找不到内存泄露的根源,

如果哪位朋友用




heap




就能定位内存泄露的线上问题,麻烦介绍下







后来读了


Dave






《High Performance Go Workshop》



,刷新了对


heap


的认识,内存


pprof


的简要内容如下:



Dave


讲了以下几点:




  1. 内存




    profiling




    记录的是堆内存分配的情况,以及调用栈信息



    ,并不是进程完整的内存情况,猜测这也是在


    go pprof


    中称为


    heap


    而不是


    memory


    的原因。




  2. 栈内存的分配是在调用栈结束后会被释放的内存,所以并不在内存




    profile












  3. 内存


    profiling


    是基于抽样的,默认是每


    1000


    次堆内存分配,执行


    1





    profile


    记录。



  4. 因为内存


    profiling


    是基于抽样和它跟踪的是已分配的内存,而不是使用中的内存,(比如有些内存已经分配,看似使用,但实际以及不使用的内存,比如内存泄露的那部分),所以

    不能使用内存




    profiling




    衡量程序总体的内存使用情况








  5. Dave




    个人观点:使用内存




    profiling




    不能够发现内存泄露







基于目前对


heap


的认知,我有


2


个观点:




  1. heap




    能帮助我们发现内存问题,但不一定能发现内存泄露问题



    ,这个看法与


    Dave


    是类似的。


    heap


    记录了内存分配的情况,我们能通过


    heap


    观察内存的变化,增长与减少,内存主要被哪些代码占用了,程序存在内存问题,这只能说明内存有使用不合理的地方,但并不能说明这是内存泄露。




  2. heap




    在帮助定位内存泄露原因上贡献的力量微乎其微



    。如第一条所言,能通过


    heap


    找到占用内存多的位置,但这个位置通常不一定是内存泄露,就算是内存泄露,也只是内存泄露的结果,并不是真正导致内存泄露的根源。



接下来,我介绍怎么用


heap


发现问题,然后再解释为什么


heap


几乎不能定位内存泄露的根因。




heap


“不能”定位内存泄露





heap


能显示内存的分配情况,以及哪行代码占用了多少内存,我们能轻易的找到占用内存最多的地方,如果这个地方的数值还在不断怎大,基本可以认定这里就是内存泄露的位置。



曾想按图索骥,从内存泄露的位置,根据调用栈向上查找,总能找到内存泄露的原因,这种方案看起来是不错的,但实施起来却找不到内存泄露的原因,结果是事半功倍。



原因在于一个


Go


程序,其中有大量的


goroutine


,这其中的调用关系也许有点复杂,也许内存泄露是在某个三方包里。举个栗子,比如下面这幅图,每个椭圆代表


1





goroutine


,其中的数字为编号,箭头代表调用关系。


heap profile


显示


g111


(最下方标红节点)这个协程的代码出现了泄露,任何一个从


g101





g111


的调用路径都可能造成了


g111


的内存泄露,有


2


类可能:






  1. goroutine


    只调用了少数几次,但消耗了大量的内存,说明每个


    goroutine


    调用都消耗了不少内存,

    内存泄露的原因基本就在该协程内部








  2. goroutine


    的调用次数非常多,虽然每个协程调用过程中消耗的内存不多,但该调用路径上,协程数量巨大,造成消耗大量的内存,并且这些


    goroutine


    由于某种原因无法退出,占用的内存不会释放,

    内存泄露的原因在到




    g111




    调用路径上某段代码实现有问题,造成创建了大量的




    g111













2




种情况,就是




goroutine




泄露,这是通过




heap




无法发现的,所以




heap




在定位内存泄露这件事上,发挥的作用不大








goroutine


泄露怎么导致内存泄露?








什么是goroutine泄露?





如果你启动了


1





goroutine


,但并没有符合预期的退出,直到程序结束,此


goroutine


才退出,这种情况就是


goroutine


泄露。



提前思考:什么会导致


goroutine


无法退出


/


阻塞?






goroutine


泄露怎么导致内存泄露?





每个


goroutine


占用


2KB


内存,泄露


1


百万


goroutine


至少泄露



2KB * 1000000 = 2GB



内存,为什么说至少呢?



goroutine


执行过程中还存在一些变量,如果这些变量指向堆内存中的内存,


GC


会认为这些内存仍在使用,不会对其进行回收,这些内存谁都无法使用,造成了内存泄露。



所以


goroutine


泄露有


2


种方式造成内存泄露:



  1. goroutine


    本身的栈所占用的空间造成内存泄露。



  2. goroutine


    中的变量所占用的堆内存导致堆内存泄露,这一部分是能通过


    heap profile


    体现出来的。



Dave


在文章中也提到了,如果不知道何时停止一个


goroutine


,这个


goroutine


就是潜在的内存泄露:




怎么确定是goroutine泄露引发的内存泄露





掌握了前面的


pprof


命令行的基本用法,很快就可以确认是否是


goroutine


泄露导致内存泄露,如果你不记得了,马上回去看一下



go pprof基本知识








判断依据:在节点正常运行的情况下,隔一段时间获取




goroutine




的数量,如果后面获取的那次,某些




goroutine




比前一次多,如果多获取几次,是持续增长的,就极有可能是




goroutine




泄露








定位goroutine泄露的2种方法




使用


pprof





2


种方式,一种是


web


网页,一种是


go tool pprof


命令行交互,这两种方法查看


goroutine


都支持,但有轻微不同,也有各自的优缺点。




总结





文章略长,但全是干货,感谢阅读到这。然读到着了,跟定很想掌握


pprof


,建议实践一把,现在和大家温习一把本文的主要内容。






goroutine


泄露的本质





goroutine


泄露的本质是


channel


阻塞,无法继续向下执行,导致此


goroutine


关联的内存都无法释放,进一步造成内存泄露。




除了




channel




阻塞,也有可能是死循环导致




goroutine




无法退出







goroutine


泄露的发现和定位





利用好


go pprof


获取


goroutine profile


文件,然后利用


3


个命令


top





traces





list


定位内存泄露的原因。






goroutine


泄露的场景





泄露的场景不仅限于以下两类,但因


channel


相关的泄露是最多的。



  1. channel


    的读或者写:



    1. 无缓冲


      channel


      的阻塞通常是写操作因为没有读而阻塞



    2. 有缓冲的


      channel


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



    3. 期待从


      channel


      读数据,结果没有


      goroutine






  2. select


    操作,


    select


    里也是


    channel


    操作,如果所有


    case


    上的操作阻塞,


    goroutine


    也无法继续执行。






编码goroutine泄露的建议





为避免


goroutine


泄露造成内存泄露,启动


goroutine


前要思考清楚:



  1. goroutine


    如何退出?



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


    goroutine







原文链接:



实战Go内存泄露 | Go语言充电站