一个易中招的 Go 内存泄露案例

2024-01-09 18:38
文章标签 go 内存 案例 泄露 中招

本文主要是介绍一个易中招的 Go 内存泄露案例,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

大家好,我是煎鱼。

前几天在公众号分享了一篇 Go timer 源码解析的文章《难以驾驭的 Go timer,一文带你参透计时器的奥秘》。

如果大家也有兴趣共同交流,欢迎关注煎鱼的公众号,加我微信后拉你进群。

在评论区有小伙伴提到了经典的 timer.After 泄露问题,希望我能聊聊,这是一个不能不知的一个大 “坑”。

今天煎鱼就带大家来研讨一下这个问题。

timer.After

今天是男主角是Go 标准库 time 所提供的 After 方法。函数签名如下:

func After(d Duration) <-chan Time

该方法可以在一定时间(根据所传入的 Duration)后主动返回 time.Time 类型的 channel 消息。

在常见的场景下,我们会基于此方法做一些计时器相关的功能开发,例子如下:

func main() {ch := make(chan string)go func() {time.Sleep(time.Second * 3)ch <- "脑子进煎鱼了"}()select {case _ = <-ch:case <-time.After(time.Second * 1):fmt.Println("煎鱼出去了,超时了!!!")}
}

在运行 1 秒钟后,输出结果:

煎鱼出去了,超时了!!!

上述程序在在运行 1 秒钟后将触发 time.After 方法的定时消息返回,输出了超时的结果。

坑在哪里

从例子来看似乎非常正常,也没什么 “坑” 的样子。难道是 timer.After 方法的虚晃一枪?

我们再看一个不像是有问题例子,这在 Go 工程中经常能看见,只是大家都没怎么关注。

代码如下:

func main() {ch := make(chan int, 10)go func() {in := 1for {in++ch <- in}}()for {select {case _ = <-ch:// do something...continuecase <-time.After(3 * time.Minute):fmt.Printf("现在是:%d,我脑子进煎鱼了!", time.Now().Unix())}}
}

在上述代码中,我们构造了一个 for+select+channel 的一个经典的处理模式。

同时在 select+case 中调用了 time.After 方法做超时控制,避免在 channel 等待时阻塞过久,引发其他问题。

看上去都没什么问题,但是细心一看。在运行了一段时间后,粗暴的利用 top 命令一看:

2524996ce60fd3b6d87e6d7c641b2171.png
运行了一会后,10+GB

我的 Go 工程的内存占用竟然已经达到了 10+GB 之高,并且还在持续增长,非常可怕。

在所设置的超时时间到达后,Go 工程的内存占用似乎一时半会也没有要回退下去的样子,这,到底发生了什么事?

为什么

抱着一脸懵逼的煎鱼,我默默的掏出我早已埋好的 PProf,这是 Go 语言中最强的性能分析剖析工具,在我出版的 《Go 语言编程之旅》特意有花大量的篇幅大面积将讲解过。

在 Go 语言中,PProf 是用于可视化和分析性能分析数据的工具,PProf 以 profile.proto 读取分析样本的集合,并生成报告以可视化并帮助分析数据(支持文本和图形报告)。

我们直接用 go tool pprof 分析 Go 工程中函数内存申请情况,如下图:

009707556ce9d947ff1388088da8ad08.png
PProf

从图来分析,可以发现是不断地在调用 time.After,从而导致计时器 time.NerTimer 的不断创建和内存申请。

这就非常奇怪了,因为我们的 Go 工程里只有几行代码与 time 相关联:

func main() {...for {select {...case <-time.After(3 * time.Minute):fmt.Printf("现在是:%d,我脑子进煎鱼了!", time.Now().Unix())}}
}

由于 Demo 足够的小,我们相信这就是问题代码,但原因是什么呢?

原因在于 for+select,再加上 time.After 的组合会导致内存泄露。因为 for在循环时,就会调用都 select 语句,因此在每次进行 select 时,都会重新初始化一个全新的计时器(Timer)。

我们这个计时器,是在 3 分钟后才会被触发去执行某些事,但重点在于计时器激活后,却又发现和 select 之间没有引用关系了,因此很合理的也就被 GC 给清理掉了,因为没有人需要 “我” 了。

1276f20d4f7320e8a0ed1301084b21b6.png

要命的还在后头,被抛弃的 time.After 的定时任务还是在时间堆中等待触发,在定时任务未到期之前,是不会被 GC 清除的。

c07665c36d8ab76e74e65ac42197d94d.png

但很可惜,他 “永远” 不会到期了,也就是为什么我们的 Go 工程内存会不断飙高,其实是 time.After 产生的内存孤儿们导致了泄露。

解决办法

既然我们知道了问题的根因代码是不断的重复创建 time.After,又没法完整的走完释放的闭环,那解决办法也就有了。

改进后的代码如下:

func main() {timer := time.NewTimer(3 * time.Minute)defer timer.Stop()...for {select {...case <-timer.C:fmt.Printf("现在是:%d,我脑子进煎鱼了!", time.Now().Unix())}}
}

经过一段时间的摸鱼后,再使用 PProf 进行采集和查看:

3cd17927df01f0eeb95a15c3b06458b8.png
PProf

Go 进程的各项指标正常,完好的解决了这个内存泄露的问题。

总结

在今天这篇文章中,我们介绍了标准库 time 的基本常规使用,同时针对 Go 小伙伴所提出的 time.After 方法的使用不当,所导致的内存泄露进行了重现和问题解析。

其根因就在于 Go 语言时间堆的处理机制和常规 for+select+time.After 组合的下意识写法所导致的泄露。

不知道你在日常工作中有没有遇到过相似的问题呢,欢迎留言区评论和交流

(大雾)突然想起我有一个朋友在公司里有看到过类似的代码...


关注煎鱼公众号,吸取精华:

👆 点击关注煎鱼,在知识的海洋里遨游

这篇关于一个易中招的 Go 内存泄露案例的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/588059

相关文章

Python中使用正则表达式精准匹配IP地址的案例

《Python中使用正则表达式精准匹配IP地址的案例》Python的正则表达式(re模块)是完成这个任务的利器,但你知道怎么写才能准确匹配各种合法的IP地址吗,今天我们就来详细探讨这个问题,感兴趣的朋... 目录为什么需要IP正则表达式?IP地址的基本结构基础正则表达式写法精确匹配0-255的数字验证IP地

MySQL高级查询之JOIN、子查询、窗口函数实际案例

《MySQL高级查询之JOIN、子查询、窗口函数实际案例》:本文主要介绍MySQL高级查询之JOIN、子查询、窗口函数实际案例的相关资料,JOIN用于多表关联查询,子查询用于数据筛选和过滤,窗口函... 目录前言1. JOIN(连接查询)1.1 内连接(INNER JOIN)1.2 左连接(LEFT JOI

Go 语言中的select语句详解及工作原理

《Go语言中的select语句详解及工作原理》在Go语言中,select语句是用于处理多个通道(channel)操作的一种控制结构,它类似于switch语句,本文给大家介绍Go语言中的select语... 目录Go 语言中的 select 是做什么的基本功能语法工作原理示例示例 1:监听多个通道示例 2:带

Go标准库常见错误分析和解决办法

《Go标准库常见错误分析和解决办法》Go语言的标准库为开发者提供了丰富且高效的工具,涵盖了从网络编程到文件操作等各个方面,然而,标准库虽好,使用不当却可能适得其反,正所谓工欲善其事,必先利其器,本文将... 目录1. 使用了错误的time.Duration2. time.After导致的内存泄漏3. jsO

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

go中空接口的具体使用

《go中空接口的具体使用》空接口是一种特殊的接口类型,它不包含任何方法,本文主要介绍了go中空接口的具体使用,具有一定的参考价值,感兴趣的可以了解一下... 目录接口-空接口1. 什么是空接口?2. 如何使用空接口?第一,第二,第三,3. 空接口几个要注意的坑坑1:坑2:坑3:接口-空接口1. 什么是空接

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

利用Go语言开发文件操作工具轻松处理所有文件

《利用Go语言开发文件操作工具轻松处理所有文件》在后端开发中,文件操作是一个非常常见但又容易出错的场景,本文小编要向大家介绍一个强大的Go语言文件操作工具库,它能帮你轻松处理各种文件操作场景... 目录为什么需要这个工具?核心功能详解1. 文件/目录存javascript在性检查2. 批量创建目录3. 文件

Go语言中最便捷的http请求包resty的使用详解

《Go语言中最便捷的http请求包resty的使用详解》go语言虽然自身就有net/http包,但是说实话用起来没那么好用,resty包是go语言中一个非常受欢迎的http请求处理包,下面我们一起来学... 目录安装一、一个简单的get二、带查询参数三、设置请求头、body四、设置表单数据五、处理响应六、超

MySQL中实现多表查询的操作方法(配sql+实操图+案例巩固 通俗易懂版)

《MySQL中实现多表查询的操作方法(配sql+实操图+案例巩固通俗易懂版)》本文主要讲解了MySQL中的多表查询,包括子查询、笛卡尔积、自连接、多表查询的实现方法以及多列子查询等,通过实际例子和操... 目录复合查询1. 回顾查询基本操作group by 分组having1. 显示部门号为10的部门名,员