乐趣区

关于golang:慎用timeAfter会造成内存泄漏golang

前言

嗨,大家好,我是 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 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 工具进行剖析,他能够确定具体呈现问题的代码。

proof 介绍

定位 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 个统计数据:

  • 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 库进行数据校验,开了~~~
退出移动版