微信搜寻【 脑子进煎鱼了 】关注这一只爆肝煎鱼。本文 GitHub github.com/eddycjy/blog 已收录,有我的系列文章、材料和开源 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 := 1
for {
in++
ch <- in
}
}()
for {
select {
case _ = <-ch:
// do something...
continue
case <-time.After(3 * time.Minute):
fmt.Printf("当初是:%d,我脑子进煎鱼了!", time.Now().Unix())
}
}
}
在上述代码中,咱们结构了一个 for+select+channel
的一个经典的解决模式。
同时在 select+case
中调用了 time.After
办法做超时管制,防止在 channel
期待时阻塞过久,引发其余问题。
看上去都没什么问题,然而仔细一看。在运行了一段时间后,粗犷的利用 top
命令一看:
我的 Go 工程的内存占用居然曾经达到了 10+GB 之高,并且还在持续增长,十分可怕。
在所设置的超时工夫达到后,Go 工程的内存占用仿佛一时半会也没有要回退下去的样子,这,到底产生了什么事?
为什么
抱着一脸懵逼的煎鱼,我默默的掏出我早已埋好的 PProf,这是 Go 语言中最强的性能剖析分析工具,在我出版的《Go 语言编程之旅》特意有花量章节的篇幅大面积将解说过。
在 Go 语言中,PProf 是用于可视化和剖析性能剖析数据的工具,PProf 以 profile.proto 读取剖析样本的汇合,并生成报告以可视化并帮忙剖析数据(反对文本和图形报告)。
咱们间接用 go tool pprof
剖析 Go 工程中函数内存申请状况,如下图:
从图来剖析,能够发现是一直地在调用 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 给清理掉了,因为没有人须要“我”了。
要命的还在后头,被摈弃的 time.After
的定时工作还是在工夫堆中期待触发,在定时工作未到期之前,是不会被 GC 革除的。
但很惋惜,他“永远”不会到期了,也就是为什么咱们的 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 进行采集和查看:
Go 过程的各项指标失常,完整的解决了这个内存泄露的问题。
总结
在明天这篇文章中,咱们介绍了规范库 time
的根本惯例应用,同时针对 Go 小伙伴所提出的 time.After
办法的使用不当,所导致的内存泄露进行了重现和问题解析。
其根因就在于 Go 语言工夫堆的解决机制和惯例 for
+select
+time.After
组合的下意识写法所导致的泄露。
忽然想起我有一个敌人在公司里有看到过相似的代码,在生产踩过这个坑,中午被告警抓起来 …
不晓得你在日常工作中有没有遇到过类似的问题呢,欢送留言区评论和交换。
文章继续更新,能够微信搜【脑子进煎鱼了】浏览,回复【000】有我筹备的一线大厂面试算法题解和材料;本文 GitHub github.com/eddycjy/blog 已收录,欢送 Star 催更。