Golang因Channel未關(guān)閉導(dǎo)致內(nèi)存泄漏的解決方案詳解
現(xiàn)象
某一個(gè)周末我們的服務(wù) oom了,一個(gè)比較重要的job 沒有跑完,需要重跑,以為是偶然,重跑成功,因?yàn)槭侵苣]有去定位原因
又一個(gè)工作日,它又oom了,重跑成功,持續(xù)觀察,job 在oom之前竟然占用了30g左右(這里我們的任務(wù)比較大的數(shù)據(jù)量都在內(nèi)存中計(jì)算,所以這里機(jī)器內(nèi)存量大一點(diǎn))
應(yīng)用使用30g內(nèi)存肯定是不正常的,懷疑內(nèi)存泄漏了,怎么定位內(nèi)存泄漏呢?
定位
搜了一下網(wǎng)上經(jīng)常用到的工具是 go 的 pprof 火焰圖,自己在本地跑了一下,因?yàn)閿?shù)據(jù)量比較少,并沒有發(fā)現(xiàn)什么,暫時(shí)放下了。
后續(xù)某個(gè)早上在公司工具里面打開了一下,發(fā)現(xiàn)有火焰圖的工具,打開看了一下一個(gè)函數(shù)占用了 7224.46mb,占用了 7個(gè)g, 而且這個(gè)函數(shù)是已經(jīng)跑完了,這個(gè)時(shí)候定位到那個(gè)函數(shù)了,和旁邊同事說了一下,同事幫忙看了下郵件告警,每個(gè)下午都會(huì)有任務(wù)失敗告警(任務(wù)失敗會(huì)進(jìn)行重試的); 這里懷疑是失敗了, channel 沒有關(guān)閉,導(dǎo)致 消費(fèi)的go routine 沒有回收。
舉個(gè)例子看下代碼:
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
)
func main() {
readGroup, _ := errgroup.WithContext(context.Background())
consumeGroup, _ := errgroup.WithContext(context.Background())
var (
data = make(chan []int, 10)
)
// 3個(gè)生產(chǎn)者往里面進(jìn)行進(jìn)行生產(chǎn)
readGroup.Go(func() error {
for i := 0; i < 3; i++ {
data <- []int{i}
}
return nil
})
readGroup.Go(func() error {
for i := 3; i < 6; i++ {
data <- []int{i}
}
return nil
})
readGroup.Go(func() (err error) {
for i := 6; i < 9; i++ {
// error
if i == 7 {
err = fmt.Errorf("error le")
return
}
data <- []int{i}
}
return nil
})
// 其中一個(gè)生產(chǎn)者遇到error 返回導(dǎo)致 channel 沒有關(guān)閉,消費(fèi)者沒有退出
// 1個(gè)消費(fèi)者進(jìn)行消費(fèi)
consumeGroup.Go(func() error {
for i := range data {
fmt.Println(i)
}
return nil
})
if err := readGroup.Wait(); err != nil {
fmt.Println(err)
return
}
close(data)
if err := consumeGroup.Wait(); err != nil {
fmt.Println(err)
return
}
fmt.Println("end it")
}這個(gè)case里面,readGroup 遇到error 直接退出了,channel并沒有關(guān)閉,如果是常駐進(jìn)程的程序,消費(fèi)的go routine 并沒有回收,就導(dǎo)致了內(nèi)存泄漏
最簡單的關(guān)閉修復(fù)
將 close 放到最上面的 defer close(data)
不過最好的還是生產(chǎn)者進(jìn)行關(guān)閉,我們可以優(yōu)化一下代碼,把生產(chǎn)者的代碼放到一個(gè)函數(shù)中,這樣就可以讓生產(chǎn)者去進(jìn)行關(guān)閉的操作了
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
)
func main() {
var (
data = make(chan []int, 10)
err error
eg, _ = errgroup.WithContext(context.Background())
)
eg.Go(func() (err error) {
defer close(data)
err = readGroup(data)
return
})
eg.Go(func() (err error) {
err = consumeGroup(data)
return
})
err = eg.Wait()
if err != nil {
return
}
fmt.Println("end it")
}
func consumeGroup(data chan []int) (err error) {
consumeGroup, _ := errgroup.WithContext(context.Background())
consumeGroup.Go(func() error {
for i := range data {
fmt.Println(i)
}
return nil
})
if err = consumeGroup.Wait(); err != nil {
fmt.Println(err)
return
}
return
}
func readGroup(data chan []int) (err error) {
readGroup, _ := errgroup.WithContext(context.Background())
// 3個(gè)生產(chǎn)者往里面進(jìn)行進(jìn)行生產(chǎn)
readGroup.Go(func() error {
for i := 0; i < 3; i++ {
data <- []int{i}
}
return nil
})
readGroup.Go(func() error {
for i := 3; i < 6; i++ {
data <- []int{i}
}
return nil
})
readGroup.Go(func() (err error) {
for i := 6; i < 9; i++ {
// error
if i == 7 {
err = fmt.Errorf("error le")
return
}
data <- []int{i}
}
return nil
})
if err = readGroup.Wait(); err != nil {
fmt.Println(err)
return
}
return
}修復(fù)
將生產(chǎn)者放在一個(gè) go routine 里面,最后如果遇到error的話 defer()的時(shí)候會(huì)把channel給關(guān)閉了
The Channel Closing Principle
One general principle of using Go channels is don't close a channel from the receiver side and don't close a channel if the channel has multiple concurrent senders. In other words, we should only close a channel in a sender goroutine if the sender is the only sender of the channel.
簡單點(diǎn):就是在生產(chǎn)者中進(jìn)行channel的關(guān)閉
后續(xù)討論和遇到的新問題
拆分代碼函數(shù)的時(shí)候又遇到新的問題了,有一個(gè)切片數(shù)組我拆分函數(shù)的時(shí)候,我沒有去接受切片函數(shù)的返回值,導(dǎo)致了切片發(fā)生擴(kuò)容返回的是一個(gè)空切片,并沒有修改掉原來的切片。之前以為在golang里面切片是引用類型,會(huì)自動(dòng)改變其中的值最后查了一下,在go 里面都是值傳遞,可以修改其中的值其實(shí)是使用了指針修改了同一塊地址中的值所以值發(fā)生了變化
總結(jié)
使用channel 的時(shí)候在生產(chǎn)者中進(jìn)行關(guān)閉,思考一些遇到error的時(shí)候channel是否可以正常的關(guān)閉
go 中只有值傳遞,引用傳遞是修改了同一個(gè)指向內(nèi)存地址中的值
參考文章
Golang優(yōu)雅關(guān)閉channel的方法示例
到此這篇關(guān)于Golang因Channel未關(guān)閉導(dǎo)致內(nèi)存泄漏的解決方案詳解的文章就介紹到這了,更多相關(guān)Golang Channel內(nèi)存泄漏內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Golang實(shí)現(xiàn)內(nèi)存數(shù)據(jù)庫的示例詳解
這篇文章主要為大家詳細(xì)介紹了如何基于Golang實(shí)現(xiàn)內(nèi)存數(shù)據(jù)庫,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的小伙伴可以參考一下2023-03-03
golang?四則運(yùn)算計(jì)算器yacc歸約手寫實(shí)現(xiàn)
這篇文章主要為大家介紹了golang?四則運(yùn)算?計(jì)算器?yacc?歸約的手寫實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
Go語言實(shí)戰(zhàn)學(xué)習(xí)之流程控制詳解
這篇文章主要為大家詳細(xì)介紹了Go語言中的流程控制,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Go語言有一定的幫助?,需要的朋友可以參考下2022-08-08
Go語言字符串及strings和strconv包使用實(shí)例
字符串是工作中最常用的,值得我們專門的練習(xí)一下,下面這篇文章主要給大家介紹了關(guān)于Go語言字符串及strings和strconv包使用的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-06-06
Golang使用gob實(shí)現(xiàn)結(jié)構(gòu)體的序列化過程詳解
Golang struct類型數(shù)據(jù)序列化用于網(wǎng)絡(luò)傳輸數(shù)據(jù)或在磁盤上寫入數(shù)據(jù)。在分布式系統(tǒng)中,一端生成數(shù)據(jù)、然后序列化、壓縮和發(fā)送;在另一端,接收數(shù)據(jù)、然后解壓縮、反序列化和處理數(shù)據(jù),整個(gè)過程必須快速有效2023-03-03
Go實(shí)現(xiàn)socks5服務(wù)器的方法
SOCKS5 是一個(gè)代理協(xié)議,它在使用TCP/IP協(xié)議通訊的前端機(jī)器和服務(wù)器機(jī)器之間扮演一個(gè)中介角色,使得內(nèi)部網(wǎng)中的前端機(jī)器變得能夠訪問Internet網(wǎng)中的服務(wù)器,或者使通訊更加安全,這篇文章主要介紹了Go實(shí)現(xiàn)socks5服務(wù)器的方法,需要的朋友可以參考下2023-07-07
GoFrame框架gset交差并補(bǔ)集使用實(shí)例
這篇文章主要為大家介紹了GoFrame框架gset交差并補(bǔ)集使用實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06

