前言
嗨,大家好,我是asong,我今天又来了。
背景
我先贴一下会发生内存泄漏的代码段,根据代码可以更好的进行讲解:
看了这段代码,你知道是哪里发生内存泄漏了嘛?我先来告诉大家,这里time.After(time.Millisecond * 5)
会发生内存泄漏,具体原因嘛别着急,我们一步步分析。
验证
我们来写一段代码进行验证,先看代码吧:
这段代码我们该怎么验证呢?看代码估计你们也猜到了,没错就是go tool pprof
,可能有些小伙伴不知道这个工具,那我简单介绍一下基本使用,不做详细介绍,更多功能可自行学习。
再介绍pprof
之前,我们其实还有一种方法,可以测试此段代码是否发生了内存泄漏,就是使用top
命令查看该进程占用cpu
情况,输入top
命令,我们会看到cpu
一直在飙升,这种方法可以确定发生内存泄漏,但是不能确定发生问题的代码在哪部分,所以最好还是使用pprof
工具进行分析,他可以确定具体出现问题的代码。
proof 介绍
定位goroutine泄露会使用到pprof,pprof是Go的性能工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等,当需要性能调优或者定位Bug时候,这些记录的信息是相当重要。使用pprof有多种方式,Go已经现成封装好了1个:net/http/pprof
,使用简单的几行命令,就可以开启pprof,记录运行信息,并且提供了Web服务,能够通过浏览器和命令行2种方式获取运行数据。
基本使用也很简单,看这段代码:
使用还是很简单的吧,这样我们就开启了go tool pprof
。下面我们开始实践来说明pprof
的使用。
验证流程
首先我们先运行我的测试代码,然后打开我们的终端输入如下命令:
这里的作用是使用go tool pprof
命令获取指定的profile文件,采集60s的CPU
使用情况,会将采集的数据下载到本地,之后进入交互模式,可以使用命令行查看运行信息。
进入命令行交互模式后,我们输入top
命令查看内存占用情况。
第一次接触的不知道这些参数的意思,我们先来解释一下各个参数吧,top
会列出5个统计数据:
- flat: 本函数占用的内存量。
- flat%: 本函数内存占使用中内存总量的百分比。
- sum%: 前面每一行flat百分比的和,比如第2行虽然的100% 是 100% + 0%。
- cum: 是累计量,加入main函数调用了函数f,函数f占用的内存量,也会记进来。
- cum%: 是累计量占总量的百分比。
这个我们可以看出time.NewTimer
占用内存很高,这么看也不是很直观,我们可以使用火焰图来查看,打开终端输入如下命令即可:
浏览器会自动弹出,看下图:
我们可以看到time.NewTimer
这个方法导致调用链占了很长时间,占用CPU很长时间,这种方法可以帮我定位到出现问题的代码,还是很方便的。知道了什么问题,接下来我们就来分析一下原因吧。
原因分析
分析具体原因之前,我们先来了解一下go中两个定时器ticker
和timer
,因为不知道这两个的使用,确实不知道具体原因。
ticker和timer
Golang中time包有两个定时器,分别为ticker 和 timer。两者都可以实现定时功能,但各自都有自己的使用场景。
我们来看一下他们的区别:
- ticker定时器表示每隔一段时间就执行一次,一般可执行多次。
- timer定时器表示在一段时间后执行,默认情况下只执行一次,如果想再次执行的话,每次都需要调用 time.Reset()方法,此时效果类似ticker定时器。同时也可以调用stop()方法取消定时器
- timer定时器比ticker定时器多一个Reset()方法,两者都有Stop()方法,表示停止定时器,底层都调用了stopTimer()函数。
原因
上面我们了介绍go的两个定时器,现在我们回到我们的问题,我们的代码使用time.After来做超时控制,time.After
其实内部调用的就是timer
定时器,根据timer
定时器的特点,具体原因就很明显了。
这里我们的定时时间设置的是3分钟, 在for循环每次select的时候,都会实例化一个一个新的定时器。该定时器在3分钟后,才会被激活,但是激活后已经跟select无引用关系,被gc给清理掉。这里最关键的一点是在计时器触发之前,垃圾收集器不会回收 Timer,换句话说,被遗弃的time.After定时任务还是在时间堆里面,定时任务未到期之前,是不会被gc清理的,所以这就是会造成内存泄漏的原因。每次循环实例化的新定时器对象需要3分钟才会可能被GC清理掉,如果我们把上面代码中的3分钟改小点,会有所改善,但是仍存在风险,下面我们就使用正确的方法来修复这个bug。
修复bug
方法一:使用timer
定时器
time.After
虽然调用的是timer
定时器,但是他没有使用time.Reset()
方法再次激活定时器,所以每一次都是新创建的实例,才会造成的内存泄漏,我们添加上time.Reset
每次重新激活定时器,即可完成解决问题。
方法二:ticker定时器
直接使用ticker
定时器就好啦,因为ticker
每隔一段时间就执行一次,一般可执行多次,相当于timer
定时器调用了time.Reset
。
总结
不知道这篇文章你们看懂了吗?没看懂的可以下载测试代码,自己测试一下,更能加深印象的呦~~~
这篇文章主要介绍了排查问题的思路,
go tool pprof
这个工具很重要,遇到性能和内存gc问题,都可以使用golang tool pprof来排查分析问题。不会的小伙伴还是要学起来的呀~~~最后感谢指出问题的那位网友,让我又有所收获,非常感谢,所以说嘛,还是要共同进步的呀,你不会的,并不代表别人不会,虚心使人进步嘛,加油各位小伙伴们~~~