嗨,你们好,我是asong,我今天又来了。昨天发表了一篇文章: 手把手教姐姐写消息队列,其中一段代码被细心的读者发现了有内存泄漏的危险,确实是这样,本身没有注意到这方面,追求完美的我,立刻进行了排查并更改了这个bug
。如今我就把这个bug
分享一下,避免小伙伴们后续踩坑。测试代码已经放到了github:https://github.com/asong2020/...git
欢迎star~~~github
我先贴一下会发生内存泄漏的代码段,根据代码能够更好的进行讲解:golang
func (b *BrokerImpl) broadcast(msg interface{}, subscribers []chan interface{}) { count := len(subscribers) concurrency := 1 switch { case count > 1000: concurrency = 3 case count > 100: concurrency = 2 default: concurrency = 1 } pub := func(start int) { for j := start; j < count; j += concurrency { select { case subscribers[j] <- msg: case <-time.After(time.Millisecond * 5): case <-b.exit: return } } } for i := 0; i < concurrency; i++ { go pub(i) } }
看了这段代码,你知道是哪里发生内存泄漏了嘛?我先来告诉你们,这里time.After(time.Millisecond * 5)
会发生内存泄漏,具体缘由嘛别着急,咱们一步步分析。面试
咱们来写一段代码进行验证,先看代码吧:shell
package main import ( "fmt" "net/http" _ "net/http/pprof" "time" ) /** time.After oom 验证demo */ func main() { ch := make(chan string,100) go func() { for { ch <- "asong" } }() go func() { // 开启pprof,监听请求 ip := "127.0.0.1:6060" if err := http.ListenAndServe(ip, nil); err != nil { fmt.Printf("start pprof failed on %s\n", ip) } }() for { select { case <-ch: case <- time.After(time.Minute * 3): } } }
这段代码咱们该怎么验证呢?看代码估计大家也猜到了,没错就是go tool pprof
,可能有些小伙伴不知道这个工具,那我简单介绍一下基本使用,不作详细介绍,更多功能可自行学习。设计模式
再介绍pprof
以前,咱们其实还有一种方法,能够测试此段代码是否发生了内存泄漏,就是使用top
命令查看该进程占用cpu
状况,输入top
命令,咱们会看到cpu
一直在飙升,这种方法能够肯定发生内存泄漏,可是不能肯定发生问题的代码在哪部分,因此最好仍是使用pprof
工具进行分析,他能够肯定具体出现问题的代码。浏览器
定位goroutine泄露会使用到pprof,pprof是Go的性能工具,在程序运行过程当中,能够记录程序的运行信息,能够是CPU使用状况、内存使用状况、goroutine运行状况等,当须要性能调优或者定位Bug时候,这些记录的信息是至关重要。使用pprof有多种方式,Go已经现成封装好了1个:net/http/pprof
,使用简单的几行命令,就能够开启pprof,记录运行信息,而且提供了Web服务,可以经过浏览器和命令行2种方式获取运行数据。架构
基本使用也很简单,看这段代码:框架
package main import ( "fmt" "net/http" _ "net/http/pprof" ) func main() { // 开启pprof,监听请求 ip := "127.0.0.1:6060" if err := http.ListenAndServe(ip, nil); err != nil { fmt.Printf("start pprof failed on %s\n", ip) } }
使用仍是很简单的吧,这样咱们就开启了go tool pprof
。下面咱们开始实践来讲明pprof
的使用。函数
首先咱们先运行个人测试代码,而后打开咱们的终端输入以下命令:
$ go tool pprof http://127.0.0.1:6060/debug/pprof/profile -seconds 60
这里的做用是使用go tool pprof
命令获取指定的profile文件,采集60s的CPU
使用状况,会将采集的数据下载到本地,以后进入交互模式,可使用命令行查看运行信息。
进入命令行交互模式后,咱们输入top
命令查看内存占用状况。
<img src="./images/top.png" style="zoom:50%;" />
第一次接触的不知道这些参数的意思,咱们先来解释一下各个参数吧,top
会列出5个统计数据:
这个咱们能够看出time.NewTimer
占用内存很高,这么看也不是很直观,咱们可使用火焰图来查看,打开终端输入以下命令便可:
# pprof.samples.cpu.001.pb.gz 这个要看大家输入上面命令生成的文件名 $ go tool pprof -http=:8081 ~/pprof/pprof.samples.cpu.001.pb.gz
浏览器会自动弹出,看下图:
<img src="./images/pprof.png" style="zoom:50%;" />
咱们能够看到time.NewTimer
这个方法致使调用链占了很长时间,占用CPU很长时间,这种方法能够帮我定位到出现问题的代码,仍是很方便的。知道了什么问题,接下来咱们就来分析一下缘由吧。
分析具体缘由以前,咱们先来了解一下go中两个定时器ticker
和timer
,由于不知道这两个的使用,确实不知道具体缘由。
Golang中time包有两个定时器,分别为ticker 和 timer。二者均可以实现定时功能,但各自都有本身的使用场景。
咱们来看一下他们的区别:
上面咱们了介绍go的两个定时器,如今咱们回到咱们的问题,咱们的代码使用time.After来作超时控制,time.After
其实内部调用的就是timer
定时器,根据timer
定时器的特色,具体缘由就很明显了。
这里咱们的定时时间设置的是3分钟, 在for循环每次select的时候,都会实例化一个一个新的定时器。该定时器在3分钟后,才会被激活,可是激活后已经跟select无引用关系,被gc给清理掉。这里最关键的一点是在计时器触发以前,垃圾收集器不会回收 Timer,换句话说,被遗弃的time.After定时任务仍是在时间堆里面,定时任务未到期以前,是不会被gc清理的,因此这就是会形成内存泄漏的缘由。每次循环实例化的新定时器对象须要3分钟才会可能被GC清理掉,若是咱们把上面代码中的3分钟改小点,会有所改善,可是仍存在风险,下面咱们就使用正确的方法来修复这个bug。
timer
定时器time.After
虽然调用的是timer
定时器,可是他没有使用time.Reset()
方法再次激活定时器,因此每一次都是新建立的实例,才会形成的内存泄漏,咱们添加上time.Reset
每次从新激活定时器,便可完成解决问题。
func (b *BrokerImpl) broadcast(msg interface{}, subscribers []chan interface{}) { count := len(subscribers) concurrency := 1 switch { case count > 1000: concurrency = 3 case count > 100: concurrency = 2 default: concurrency = 1 } //采用Timer 而不是使用time.After 缘由:time.After会产生内存泄漏 在计时器触发以前,垃圾回收器不会回收Timer idleDuration := 5 * time.Millisecond idleTimeout := time.NewTimer(idleDuration) defer idleTimeout.Stop() pub := func(start int) { for j := start; j < count; j += concurrency { idleTimeout.Reset(idleDuration) select { case subscribers[j] <- msg: case <-idleTimeout.C: case <-b.exit: return } } } for i := 0; i < concurrency; i++ { go pub(i) } }
直接使用ticker
定时器就好啦,由于ticker
每隔一段时间就执行一次,通常可执行屡次,至关于timer
定时器调用了time.Reset
。
func (b *BrokerImpl) broadcast(msg interface{}, subscribers []chan interface{}) { count := len(subscribers) concurrency := 1 switch { case count > 1000: concurrency = 3 case count > 100: concurrency = 2 default: concurrency = 1 } //采用Timer 而不是使用time.After 缘由:time.After会产生内存泄漏 在计时器触发以前,垃圾回收器不会回收Timer idleTimeout := time.time.NewTicker(5 * time.Millisecond) defer idleTimeout.Stop() pub := func(start int) { for j := start; j < count; j += concurrency { select { case subscribers[j] <- msg: case <-idleTimeout.C: case <-b.exit: return } } } for i := 0; i < concurrency; i++ { go pub(i) } }
不知道这篇文章大家看懂了吗?没看懂的能够下载测试代码,本身测试一下,更能加深印象的呦~~~这篇文章主要介绍了排查问题的思路,
go tool pprof
这个工具很重要,遇到性能和内存gc问题,均可以使用golang tool pprof来排查分析问题。不会的小伙伴仍是要学起来的呀~~~最后感谢指出问题的那位网友,让我又有所收获,很是感谢,因此说嘛,仍是要共同进步的呀,你不会的,并不表明别人不会,虚心令人进步嘛,加油各位小伙伴们~~~
结尾给你们发一个小福利吧,最近我在看[微服务架构设计模式]这一本书,讲的很好,本身也收集了一本PDF,有须要的小伙能够到自行下载。获取方式:关注公众号:[Golang梦工厂],后台回复:[微服务],便可获取。
我翻译了一份GIN中文文档,会按期进行维护,有须要的小伙伴后台回复[gin]便可下载。
我是asong,一名普普统统的程序猿,让我一块儿慢慢变强吧。我本身建了一个golang
交流群,有须要的小伙伴加我vx
,我拉你入群。欢迎各位的关注,咱们下期见~~~
推荐往期文章: