首页 > 系统相关 >慎用time.After会造成内存泄漏(go)

慎用time.After会造成内存泄漏(go)

时间:2023-02-07 17:58:10浏览次数:34  
标签:case 定时器 pprof After 内存 time go

2020-09-24更新
修复文章的问题:

去除使用time.Ticker方法修复bug,不符合select超时逻辑
以前使用go tool pprof分析内存占用方法是错误的,现在已经更改过来了。
前言
嗨,大家好,我是asong,我今天又来了。昨天发表了一篇文章:手把手教姐姐写消息队列,其中一段代码被细心的读者发现了有内存泄漏的危险,确实是这样,自己没有注意到这方面,追求完美的我,马上进行了排查并更改了这个bug。现在我就把这个bug分享一下,避免小伙伴们后续踩坑。

测试代码已经放到了github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/time_oom_validate

欢迎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=:8081 http://localhost:6060/debug/pprof/heap
1
浏览器会自动弹出,看下图:

 

看这个图,都爆红了,time.Timer导致占用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每次重新激活定时器,即可完成解决问题。

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
pub := func(start int) {
idleDuration := 5 * time.Millisecond
idleTimeout := time.NewTimer(idleDuration)
defer idleTimeout.Stop()
for j := start; j < count; j += concurrency {
if !idleTimeout.Stop(){
select {
case <- idleTimeout.C:
default:
}
}
idleTimeout.Reset(idleDuration)
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来排查分析问题。不会的小伙伴还是要学起来的呀~~~

最后感谢指出问题的那位网友,让我又有所收获,非常感谢,所以说嘛,还是要共同进步的呀,你不会的,并不代表别人不会,虚心使人进步嘛,加油各位小伙伴们~~~

标签:case,定时器,pprof,After,内存,time,go
From: https://www.cnblogs.com/cheyunhua/p/17099311.html

相关文章

  • How to set the sprint goal before each quarter and pull the tickets into the cur
    ①IfoundtheepicIwanttodoandcreatedtheticketstodo  ②ThenIfoundmyticketinthebacklog  ③Rightclickandselec......
  • Go 语言中结构体的 Tag 用法
    转自:https://zhuanlan.zhihu.com/p/258978922抛砖引玉:什么是Tag?正常情况下,你定义的结构体是这样子的,每个字段都由名字和字段类型组成typePersonstruct{Namest......
  • java.lang.NoSuchMethodError: com.google.gson.GsonBuilder.setLenient()Lcom/google
    引入第三方的工具类,重新启动项目就会奇奇怪怪的报错。加载不到gson类。一开始以为是没有加载到,各种引入,清理问题依然存在。直到仔细看报错信息,里面是spring的类报错,才意识......
  • go语言context教程
    预览目录Context原理遵循规则Context包WithCancel例子WithDeadline例子WithTimeout例子WithValue例子参考连接对于golang开发者来说context(上下文)包一......
  • ONLYOFFICE文档​ TEXTBEFORE, TEXTAFTER, TEXTSPLIT 公式
    ​公式编辑器与常见的文字处理软件和演示程序配合使用,特别是数学编辑器已经成为当下互联网最常用的软件之一,在各种文档中加入复杂的数学公式和符号,在编辑试卷、书籍等途径上......
  • 开心档-软件开发入门之MongoDB 覆盖索引查询
     作者简介:每天分享​​MongoDB教程​的学习经验、和学习笔记。  座右铭:有自制力,做事有始有终;学习能力强,愿意不断地接触学习新知识。个人主页:​​雪奈椰子的主页​​ 前......
  • 开心档-软件开发入门之MongoDB 创建集合
    作者简介:每天分享​​MongoDB教程​​的学习经验、和学习笔记。  座右铭:有自制力,做事有始有终;学习能力强,愿意不断地接触学习新知识。​个人主页:​​雪奈椰子的主页​​​......
  • MongoDB--字符串长度计算
    MongoDB--字符串长度计算db.event_coll.aggregate([{$project:{"road":1,"length_Bytes":{$strLenBytes:"$road"},"length_CP":{$strLen......
  • Django+Pytest+Jenkins搭建持续集成自动化测试
    系统环境采用的是Mac系统环境下搭建自动化框架。设计思路持续集成测试:定时某时间自动执行测试脚本,接口测试可用Jmeter+ant+allure+Jenkins进行搭建接口持续集成测试,这个针对......
  • django后台定制化展示参数
    list_display 可显示的数据库字段 list_display=['id','name']list_filter 右边栏过滤器 list_filter=['name']search_fields 可根据什么搜索 search_fields=['name......