前言

嗨,大家好,我是asong,我明天又来了。昨天发表了一篇文章:手把手教姐姐写音讯队列,其中一段代码被仔细的读者发现了有内存透露的危险,的确是这样,本人没有留神到这方面,谋求完满的我,马上进行了排查并更改了这个bug。当初我就把这个bug分享一下,防止小伙伴们后续踩坑。

测试代码曾经放到了github:https://github.com/asong2020/...

欢送star~~~

背景

我先贴一下会产生内存透露的代码段,依据代码能够更好的进行解说:

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)会产生内存透露,具体起因嘛别着急,咱们一步步剖析。

验证

咱们来写一段代码进行验证,先看代码吧:

package mainimport (    "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工具进行剖析,他能够确定具体呈现问题的代码。

proof 介绍

定位goroutine泄露会应用到pprof,pprof是Go的性能工具,在程序运行过程中,能够记录程序的运行信息,能够是CPU应用状况、内存应用状况、goroutine运行状况等,当须要性能调优或者定位Bug时候,这些记录的信息是相当重要。应用pprof有多种形式,Go曾经现成封装好了1个:net/http/pprof,应用简略的几行命令,就能够开启pprof,记录运行信息,并且提供了Web服务,可能通过浏览器和命令行2种形式获取运行数据。

根本应用也很简略,看这段代码:

package mainimport (    "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个统计数据:

  • flat: 本函数占用的内存量。
  • flat%: 本函数内存占应用中内存总量的百分比。
  • sum%: 后面每一行flat百分比的和,比方第2行尽管的100% 是 100% + 0%。
  • cum: 是累计量,退出main函数调用了函数f,函数f占用的内存量,也会记进来。
  • cum%: 是累计量占总量的百分比。

这个咱们能够看出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中两个定时器tickertimer,因为不晓得这两个的应用,的确不晓得具体起因。

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每次从新激活定时器,即可实现解决问题。

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定时器就好啦,因为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,我拉你入群。欢送各位的关注,咱们下期见~~~

举荐往期文章:

  • 手把手教姐姐写音讯队列
  • 详解Context包,看这一篇就够了!!!
  • go-ElasticSearch入门看这一篇就够了(一)
  • 面试官:go中for-range应用过吗?这几个问题你能解释一下起因吗
  • 学会wire依赖注入、cron定时工作其实就这么简略!
  • 据说你还不会jwt和swagger-饭我都不吃了带着实际我的项目我就来了
  • 把握这些Go语言个性,你的程度将进步N个品位(二)
  • go实现多人聊天室,在这里你想聊什么都能够的啦!!!
  • grpc实际-学会grpc就是这么简略
  • go规范库rpc实际
  • 2020最新Gin框架中文文档 asong又捡起来了英语,用心翻译
  • 基于gin的几种热加载形式
  • boss: 这小子还不会应用validator库进行数据校验,开了~~~