Golang因Channel未關閉導致內存泄漏的解決方案詳解
現(xiàn)象
某一個周末我們的服務 oom了,一個比較重要的job 沒有跑完,需要重跑,以為是偶然,重跑成功,因為是周末沒有去定位原因
又一個工作日,它又oom了,重跑成功,持續(xù)觀察,job 在oom之前竟然占用了30g左右(這里我們的任務比較大的數據量都在內存中計算,所以這里機器內存量大一點)
應用使用30g內存肯定是不正常的,懷疑內存泄漏了,怎么定位內存泄漏呢?
定位
搜了一下網上經常用到的工具是 go 的 pprof 火焰圖,自己在本地跑了一下,因為數據量比較少,并沒有發(fā)現(xiàn)什么,暫時放下了。
后續(xù)某個早上在公司工具里面打開了一下,發(fā)現(xiàn)有火焰圖的工具,打開看了一下一個函數占用了 7224.46mb,占用了 7個g, 而且這個函數是已經跑完了,這個時候定位到那個函數了,和旁邊同事說了一下,同事幫忙看了下郵件告警,每個下午都會有任務失敗告警(任務失敗會進行重試的); 這里懷疑是失敗了, channel 沒有關閉,導致 消費的go routine 沒有回收。
舉個例子看下代碼:
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個生產者往里面進行進行生產
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
})
// 其中一個生產者遇到error 返回導致 channel 沒有關閉,消費者沒有退出
// 1個消費者進行消費
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")
}這個case里面,readGroup 遇到error 直接退出了,channel并沒有關閉,如果是常駐進程的程序,消費的go routine 并沒有回收,就導致了內存泄漏
最簡單的關閉修復
將 close 放到最上面的 defer close(data)
不過最好的還是生產者進行關閉,我們可以優(yōu)化一下代碼,把生產者的代碼放到一個函數中,這樣就可以讓生產者去進行關閉的操作了
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個生產者往里面進行進行生產
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
}修復
將生產者放在一個 go routine 里面,最后如果遇到error的話 defer()的時候會把channel給關閉了
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.
簡單點:就是在生產者中進行channel的關閉
后續(xù)討論和遇到的新問題
拆分代碼函數的時候又遇到新的問題了,有一個切片數組我拆分函數的時候,我沒有去接受切片函數的返回值,導致了切片發(fā)生擴容返回的是一個空切片,并沒有修改掉原來的切片。之前以為在golang里面切片是引用類型,會自動改變其中的值最后查了一下,在go 里面都是值傳遞,可以修改其中的值其實是使用了指針修改了同一塊地址中的值所以值發(fā)生了變化
總結
使用channel 的時候在生產者中進行關閉,思考一些遇到error的時候channel是否可以正常的關閉
go 中只有值傳遞,引用傳遞是修改了同一個指向內存地址中的值
參考文章
到此這篇關于Golang因Channel未關閉導致內存泄漏的解決方案詳解的文章就介紹到這了,更多相關Golang Channel內存泄漏內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
golang使用grpc+go-kit模擬oauth認證的操作
這篇文章主要介紹了golang使用grpc+go-kit模擬oauth認證的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04
Go語言使用protojson庫實現(xiàn)Protocol Buffers與JSON轉換
本文主要介紹Google開源的工具庫Protojson庫如何Protocol Buffers與JSON進行轉換,以及和標準庫encoding/json的性能對比,需要的朋友可以參考下2023-09-09

