前言
嗨,大家好,我是 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 中两个定时器 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
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 库进行数据校验,开了~~~