嗨,你們好,我是asong,我今天又來了。昨天發表了一篇文章: 手把手教姐姐寫消息隊列,其中一段代碼被細心的讀者發現了有內存泄漏的危險,確實是這樣,本身沒有注意到這方面,追求完美的我,立刻進行了排查並更改了這個bug
。如今我就把這個bug
分享一下,避免小夥伴們後續踩坑。測試代碼已經放到了github:https://github.com/asong2020/...git
歡迎star~~~github
我先貼一下會發生內存泄漏的代碼段,根據代碼能夠更好的進行講解:golang
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)
會發生內存泄漏,具體緣由嘛彆着急,咱們一步步分析。面試
咱們來寫一段代碼進行驗證,先看代碼吧:shell
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
工具進行分析,他能夠肯定具體出現問題的代碼。瀏覽器
定位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個統計數據:
這個咱們能夠看出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
,由於不知道這兩個的使用,確實不知道具體緣由。
Golang中time包有兩個定時器,分別爲ticker 和 timer。二者均可以實現定時功能,但各自都有本身的使用場景。
咱們來看一下他們的區別:
上面咱們了介紹go的兩個定時器,如今咱們回到咱們的問題,咱們的代碼使用time.After來作超時控制,time.After
其實內部調用的就是timer
定時器,根據timer
定時器的特色,具體緣由就很明顯了。
這裏咱們的定時時間設置的是3分鐘, 在for循環每次select的時候,都會實例化一個一個新的定時器。該定時器在3分鐘後,纔會被激活,可是激活後已經跟select無引用關係,被gc給清理掉。這裏最關鍵的一點是在計時器觸發以前,垃圾收集器不會回收 Timer,換句話說,被遺棄的time.After定時任務仍是在時間堆裏面,定時任務未到期以前,是不會被gc清理的,因此這就是會形成內存泄漏的緣由。每次循環實例化的新定時器對象須要3分鐘纔會可能被GC清理掉,若是咱們把上面代碼中的3分鐘改小點,會有所改善,可是仍存在風險,下面咱們就使用正確的方法來修復這個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
每隔一段時間就執行一次,通常可執行屢次,至關於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
,我拉你入羣。歡迎各位的關注,咱們下期見~~~
推薦往期文章: