淺析Go語言中內(nèi)存泄漏的原因與解決方法
遵循一個(gè)約定:如果goroutine
負(fù)責(zé)創(chuàng)建goroutine
,它也負(fù)責(zé)確保他可以停止 goroutine
channel 泄漏
發(fā)送不接收,一般來說發(fā)送者,正常發(fā)送,接收者正常接收,這樣沒啥問題。但是一旦接收者異常,發(fā)送者會(huì)被阻塞,造成泄漏。
select case 導(dǎo)致協(xié)程泄漏
func leakOfMemory() { errChan := make(chan error) //a. go func() { time.Sleep(2 * time.Second) errChan <- errors.New("chan error") // b. fmt.Println("finish ending ") }() select { case <-time.After(time.Second): fmt.Println("超時(shí)") //c case err := <-errChan: //d. fmt.Println("err:", err) } fmt.Println("leakOfMemory exit") } func TestLeakOfMemory(t *testing.T) { leakOfMemory() time.Sleep(3 * time.Second) fmt.Println("main exit...") fmt.Println("NumGoroutine:", runtime.NumGoroutine()) }
上面的代碼執(zhí)行結(jié)果:
=== RUN TestLeakOfMemory
超時(shí)
leakOfMemory exit
main exit...
NumGoroutine: 3
--- PASS: TestLeakOfMemory (4.00s)
PASS
最開始只有兩個(gè) goruntine ,為啥執(zhí)行后有三個(gè) goruntine ?
由于沒有往 errChan
中發(fā)送消息,所以 d
處 會(huì)一直阻塞,1s 后 ,c
處打印超時(shí)
,程序退出,此時(shí),有個(gè)協(xié)程在 b
處往協(xié)程中塞值,但是此時(shí)外面的 goruntine
已經(jīng)退出了,此時(shí) errChan
沒有接收者,那么就會(huì)在 b
處阻塞,因此協(xié)程一直沒有退出,造成了泄漏,如果有很多類似的代碼,會(huì)造成 OOM
。
for range 導(dǎo)致的協(xié)程泄漏
看如下代碼:
func leakOfMemory_1(nums ...int) { out := make(chan int) // sender go func() { defer close(out) for _, n := range nums { // c. out <- n time.Sleep(time.Second) } }() // receiver go func() { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() for n := range out { //b. if ctx.Err() != nil { //a. fmt.Println("ctx timeout ") return } fmt.Println(n) } }() } func TestLeakOfMemory(t *testing.T) { fmt.Println("NumGoroutine:", runtime.NumGoroutine()) leakOfMemory_1(1, 2, 3, 4, 5, 6, 7) time.Sleep(3 * time.Second) fmt.Println("main exit...") fmt.Println("NumGoroutine:", runtime.NumGoroutine()) }
上述代碼執(zhí)行結(jié)果:
=== RUN TestLeakOfMemory
NumGoroutine: 2
1
2
ctx timeout
main exit...
NumGoroutine: 3
--- PASS: TestLeakOfMemory (3.00s)
PASS
理論上,是不是最開始只有2個(gè)goruntine
,實(shí)際上執(zhí)行完出現(xiàn)了3個(gè)gorountine
, 說明 leakOfMemory_1
里面起碼有一個(gè)協(xié)程沒有退出。 因?yàn)闀r(shí)間到了,在 a
出,程序就準(zhǔn)備退出了,也就是說 b
這個(gè)就退出了,沒有接收者繼續(xù)接受 chan
中的數(shù)據(jù)了,c
處往chan
寫數(shù)據(jù)就阻塞了,因此協(xié)程一直沒有退出,就造成了泄漏。
如何解決上面說的協(xié)程泄漏問題?
可以加個(gè)管道通知來防止內(nèi)存泄漏。
func leakOfMemory_2(done chan struct{}, nums ...int) { out := make(chan int) // sender go func() { defer close(out) for _, n := range nums { select { case out <- n: case <-done: return } time.Sleep(time.Second) } }() // receiver go func() { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() for n := range out { if ctx.Err() != nil { fmt.Println("ctx timeout ") return } fmt.Println(n) } }() } func TestLeakOfMemory(t *testing.T) { fmt.Println("NumGoroutine:", runtime.NumGoroutine()) done := make(chan struct{}) defer close(done) leakOfMemory_2(done, 1, 2, 3, 4, 5, 6, 7) time.Sleep(3 * time.Second) done <- struct{}{} fmt.Println("main exit...") fmt.Println("NumGoroutine:", runtime.NumGoroutine()) }
代碼執(zhí)行結(jié)果:
=== RUN TestLeakOfMemory
NumGoroutine: 2
1
2
ctx timeout
main exit...
NumGoroutine: 2
--- PASS: TestLeakOfMemory (3.00s)
PASS
最開始是 2個(gè) goruntine
程序結(jié)束后還2個(gè) goruntine
,沒有協(xié)程泄漏。
goruntine 中 map 并發(fā)
map
是引用類型,函數(shù)值傳值是調(diào)用,參數(shù)副本依然指向m
,因?yàn)橹祩鬟f的是引用,對(duì)于共享變量,資源并發(fā)讀寫會(huì)產(chǎn)生競(jìng)爭(zhēng),故共享資源遭受到破壞。
func TestConcurrencyMap(t *testing.T) { m := make(map[int]int) go func() { for { m[3] = 3 } }() go func() { for { m[2] = 2 } }() //select {} time.Sleep(10 * time.Second) }
上訴代碼執(zhí)行結(jié)果:
=== RUN TestConcurrencyMap
fatal error: concurrent map writes
goroutine 5 [running]:
runtime.throw({0x1121440?, 0x0?})
/go/go1.18.8/src/runtime/panic.go:992 +0x71 fp=0xc000049f78 sp=0xc000049f48 pc=0x10333b1
...
用火焰圖分析下內(nèi)存泄漏問題
首先,程序代碼運(yùn)行前,需要加這個(gè)代碼:
import ( "context" "errors" "fmt" "log" "net/http" _ "net/http/pprof" "runtime" "testing" "time" ) func TestLeakOfMemory(t *testing.T) { //leakOfMemory() fmt.Println("NumGoroutine:", runtime.NumGoroutine()) for i := 0; i < 1000; i++ { go leakOfMemory_1(1, 2, 3, 4, 5, 6, 7) } //done := make(chan struct{}) //defer close(done) //leakOfMemory_2(done, 1, 2, 3, 4, 5, 6, 7) time.Sleep(3 * time.Second) //done <- struct{}{} fmt.Println("main exit...") fmt.Println("NumGoroutine:", runtime.NumGoroutine()) log.Println(http.ListenAndServe("localhost:6060", nil)) }
上面的執(zhí)行后,登陸網(wǎng)址 http://localhost:6060/debug/pprof/goroutine?debug=1
,可以看到下面的頁面:
但是看不到圖形界面,怎么辦?
需要安裝 graphviz
在控制臺(tái)執(zhí)行如下命令
brew install graphviz # 安裝graphviz,只需要安裝一次就行了 go tool pprof -http=":8081" http://localhost:6060/debug/pprof/goroutine?debug=1
然后可以登陸網(wǎng)頁:http://localhost:8081/ui/
看到下圖:
發(fā)現(xiàn)有一個(gè)程序//GoProject/main/concurrency/channel.leakOfMemory_1.func1
占用 cpu 特別大. 想看下這個(gè)程序是啥?
分析協(xié)程泄漏
使用如下結(jié)果:
go tool pprof http://localhost:6060/debug/pprof/goroutine
火焰圖分析:
Total:總共采樣次數(shù),100次。
Flat:函數(shù)在樣本中處于運(yùn)行狀態(tài)的次數(shù)。簡(jiǎn)單來說就是函數(shù)出現(xiàn)在棧頂?shù)拇螖?shù),而函數(shù)在棧頂則意味著它在使用CPU。
Flat%:Flat / Total。
Sum%:自己以及所有前面的Flat%的累積值。解讀方式:表中第3行Sum% 32.4%,意思是前3個(gè)函數(shù)(運(yùn)行狀態(tài))的計(jì)數(shù)占了總樣本數(shù)的32.4%
Cum:函數(shù)在樣本中出現(xiàn)的次數(shù)。只要這個(gè)函數(shù)出現(xiàn)在棧中那么就算進(jìn)去,這個(gè)和Flat不同(必須是棧頂才能算進(jìn)去)。也可以解讀為這個(gè)函數(shù)的調(diào)用次數(shù)。
Cum%:Cum / Total
進(jìn)入控制臺(tái),輸入 top
Type: goroutine
Time: Feb 5, 2024 at 10:02am (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 1003, 99.90% of 1004 total
Dropped 35 nodes (cum <= 5)
flat flat% sum% cum cum%
1003 99.90% 99.90% 1003 99.90% runtime.gopark
0 0% 99.90% 1000 99.60% //GoProject/main/concurrency/channel.leakOfMemory_1.func1
0 0% 99.90% 1000 99.60% runtime.chansend
0 0% 99.90% 1000 99.60% runtime.chansend1
(pprof)
其中 其中runtime.gopark即可認(rèn)為是掛起的goroutine數(shù)量。發(fā)現(xiàn)有大量協(xié)程被 runtime.gopark
然后輸入 traces runtime.gopark
(pprof) traces runtime.gopark
Type: goroutine
Time: Feb 5, 2024 at 10:02am (CST)
-----------+-------------------------------------------------------
1000 runtime.gopark
runtime.chansend
runtime.chansend1
//GoProject/main/concurrency/channel.leakOfMemory_1.func1
-----------+-------------------------------------------------------
1 runtime.gopark
runtime.chanrecv
runtime.chanrecv1
testing.(*T).Run
testing.runTests.func1
testing.tRunner
testing.runTests
testing.(*M).Run
main.main
runtime.main
-----------+-------------------------------------------------------
1 runtime.gopark
runtime.netpollblock
internal/poll.runtime_pollWait
internal/poll.(*pollDesc).wait
internal/poll.(*pollDesc).waitRead (inline)
internal/poll.(*FD).Read
net.(*netFD).Read
net.(*conn).Read
net/http.(*connReader).backgroundRead
-----------+-------------------------------------------------------
1 runtime.gopark
runtime.netpollblock
internal/poll.runtime_pollWait
internal/poll.(*pollDesc).wait
internal/poll.(*pollDesc).waitRead (inline)
internal/poll.(*FD).Accept
net.(*netFD).accept
net.(*TCPListener).accept
net.(*TCPListener).Accept
net/http.(*Server).Serve
net/http.(*Server).ListenAndServe
net/http.ListenAndServe (inline)
//GoProject/main/concurrency/channel.TestLeakOfMemory
testing.tRunner
-----------+-------------------------------------------------------
(pprof)
可以發(fā)現(xiàn)泄漏了 1000 個(gè) goruntine
。
然后通過調(diào)用棧,可以看到調(diào)用鏈路:
channel.leakOfMemory_1.func1->runtime.chansend1->runtime.chansend->runtime.gopark
runtime.chansend1
是阻塞的調(diào)用,協(xié)程最終被 runtime.gopark
掛起,從而導(dǎo)致泄漏。
然后再輸入 list GoProject/main/concurrency/channel. leakOfMemory_1.func1
可以看到如下
(pprof) list //GoProject/main/concurrency/channel.
leakOfMemory_1.func1
Total: 1004
ROUTINE ======================== //GoProject/main/concurrency/channel.leakOfMemory_1.func1 in /Users/bytedance/go/src///GoProject/main/concurrency/channel/channel_test.go
0 1000 (flat, cum) 99.60% of Total
. . 62: out := make(chan int)
. . 63: // sender
. . 64: go func() {
. . 65: defer close(out)
. . 66: for _, n := range nums {
. 1000 67: out <- n
. . 68: time.Sleep(time.Second)
. . 69: }
. . 70: }()
. . 71:
. . 72: // receiver
可以看到使用了一個(gè)非緩沖的 channel
, 上面已經(jīng)分析了,沒有接收者,發(fā)送者out
在寫入channel
時(shí)阻塞, 協(xié)程無法退出,因此有協(xié)程泄漏。
分析內(nèi)存增長泄漏
go tool pprof http://localhost:6060/debug/pprof/heap
然后輸入 top
(pprof) top
Showing nodes accounting for 6662.08kB, 86.68% of 7686.14kB total
Showing top 10 nodes out of 24
flat flat% sum% cum cum%
5125.63kB 66.69% 66.69% 5125.63kB 66.69% runtime.allocm
1024.41kB 13.33% 80.01% 1024.41kB 13.33% runtime.malg
512.05kB 6.66% 86.68% 512.05kB 6.66% internal/poll.runtime_Semacquire
0 0% 86.68% 512.05kB 6.66% GoProject/main/concurrency/channel.leakOfMemory_1.func2
0 0% 86.68% 512.05kB 6.66% fmt.Fprintln
0 0% 86.68% 512.05kB 6.66% fmt.Println (inline)
0 0% 86.68% 512.05kB 6.66% internal/poll.(*FD).Write
0 0% 86.68% 512.05kB 6.66% internal/poll.(*FD).writeLock (inline)
0 0% 86.68% 512.05kB 6.66% internal/poll.(*fdMutex).rwlock
0 0% 86.68% 512.05kB 6.66% os.(*File).Write
(pprof)
看著不是很大,達(dá)不到內(nèi)存增長泄漏的級(jí)別。
以上就是淺析Go語言中內(nèi)存泄漏的原因與解決方法的詳細(xì)內(nèi)容,更多關(guān)于Go內(nèi)存泄漏的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go使用Redis實(shí)現(xiàn)分布式鎖的常見方法
Redis?提供了一些原語,可以幫助我們實(shí)現(xiàn)高效的分布式鎖,下邊是使用?Redis?實(shí)現(xiàn)分布式鎖的一種常見方法,通過代碼示例給大家介紹的非常詳細(xì),具有一定的參考價(jià)值,需要的朋友可以參考下2024-11-11Go網(wǎng)絡(luò)編程TCP抓包實(shí)操示例探究
作為一名軟件開發(fā)者,網(wǎng)絡(luò)編程是必備知識(shí),本文通過?Go?語言實(shí)現(xiàn)?TCP?套接字編程,并結(jié)合?tcpdump?工具,展示它的三次握手、數(shù)據(jù)傳輸以及四次揮手的過程,幫助讀者更好地理解?TCP?協(xié)議與?Go?網(wǎng)絡(luò)編程2024-01-01GO中使用谷歌GEMINI模型任務(wù)代碼實(shí)例
這篇文章主要為大家介紹了GO中使用谷歌GEMINI模型任務(wù)代碼實(shí)例探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01