溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

golang中怎么防止goroutine泄露

發(fā)布時(shí)間:2021-07-21 11:24:31 來(lái)源:億速云 閱讀:341 作者:Leah 欄目:大數(shù)據(jù)

這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)?lái)有關(guān)golang中怎么防止goroutine泄露,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

NumGoroutine

runtime.NumGoroutine 可以獲取當(dāng)前進(jìn)程中正在運(yùn)行的 goroutine 數(shù)量,觀察這個(gè)數(shù)字可以初步判斷出是否存在 goroutine 泄露異常。

一個(gè)示例,如下:

package main

import (
	"net/http"
	"runtime"
	"strconv"
)

func write(w http.ResponseWriter, data []byte) {
	_, _ = w.Write(data)
}

func count(w http.ResponseWriter, r *http.Request) {
	write([]byte(strconv.Itoa(runtime.NumGoroutine())))
}

func main() {
	http.HandleFunc("/_count", count)
	http.ListenAndServe(":6080", nil)
}

功能很簡(jiǎn)單,設(shè)置 _count 路由請(qǐng)求處理函數(shù) count,它負(fù)責(zé)輸出服務(wù)當(dāng)前 goroutine 數(shù)量。啟動(dòng)服務(wù)后訪問(wèn) localhost:6080/_count 即可。

但只是一個(gè)數(shù)值,我們就能確認(rèn)是否泄露了嗎?

首先,如果這個(gè)數(shù)值很大,是不是就能說(shuō)明出現(xiàn)了泄露。我的答案是否。理由很簡(jiǎn)單,高并發(fā)情況下的 goroutine 數(shù)量肯定很高的,但并非出現(xiàn)了泄露,可能只是當(dāng)前的服務(wù)的承載能力還不夠。我們可以在數(shù)量基礎(chǔ)上引入時(shí)間,即如果 goroutine 隨著時(shí)間增加,數(shù)量在不斷上升,而基本沒(méi)有下降,基本可以確定存在泄露。我們可以定時(shí)采集不同時(shí)刻的數(shù)據(jù)來(lái)分析。

演示案例

為了更好的演示效果,我們?yōu)榉?wù)再增加一個(gè)處理函數(shù) query, 并綁定路由 /query 上。假設(shè)它負(fù)責(zé)從多個(gè)數(shù)據(jù)表中查出數(shù)據(jù)返回給用戶。這個(gè)例子在后面的演示會(huì)一直使用。

代碼如下:

func query(w http.ResponseWriter, r *http.Request) {
	c := make(chan byte)

	go func() {
		c <- 0x31
	}()

	go func() {
		c <- 0x32
	}()

	go func() {
		c <- 0x33
	}()

	rs := make([]byte, 0)
	for i := 0; i < 2; i++ {
		rs = append(rs, <-c)
	}

	write(w, rs)
}

在 query 中,我們啟動(dòng)了 3 個(gè) goroutine 執(zhí)行數(shù)據(jù)庫(kù)查詢,通過(guò) channel 傳遞返回?cái)?shù)據(jù)。這里的問(wèn)題是,query 函數(shù)中只從 channel 中接收兩次數(shù)據(jù)就退出了循環(huán),這會(huì)導(dǎo)致其中一個(gè) goroutine 因缺少接收者而無(wú)法釋放。

我們可以多次請(qǐng)求 localhost:6080/query,然后通過(guò) _count 查看服務(wù)當(dāng)前的 goroutine 數(shù)量。手動(dòng)麻煩,可以用 ab 命令進(jìn)行做個(gè)簡(jiǎn)單壓測(cè)。

$ ab -n 1000 -c 100 localhost:6080/query

命令的意思是,總共訪問(wèn) 1000 次,并發(fā)訪問(wèn) 100 次。

pprof

前面的例子比較簡(jiǎn)單,發(fā)現(xiàn)泄露后,我們可以立刻確定存在的問(wèn)題。但如果比較復(fù)雜的項(xiàng)目,我們就很難發(fā)現(xiàn)問(wèn)題代碼的出現(xiàn)位置了。

如何解決呢?

我們可以引入一個(gè)輔助工具,pprof。它是由 Go 官方提供的可用于收集程序運(yùn)行時(shí)報(bào)告的工具,其中包含 CPU、內(nèi)存等信息。當(dāng)然,也可以獲取運(yùn)行時(shí) goroutine 堆棧信息,如此一來(lái),我們就可以很容易看出哪里導(dǎo)致了 goroutine 泄露。

runtime/pprof

我們可以再加入一個(gè)名為 goroutineStack 的 handler,用于查看程序中 goroutine 的堆棧信息,,地址為 _goroutine

實(shí)現(xiàn)代碼如下:

import "runtime/pprof"

func goroutineStack(w http.ResponseWriter, r *http.Request) {
	_ = pprof.Lookup("goroutine").WriteTo(w, 1)
}

訪問(wèn) _goroutine,將會(huì)得到類似如下的信息:

goroutine profile: total 1004
948 @ 0x102e70b 0x102e7b3 0x10068ed 0x10066c5 0x1233b37 0x10595d1
#	0x1233b36	main.query.func2+0x36	/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:20

45 @ 0x102e70b 0x102e7b3 0x10068ed 0x10066c5 0x1233ae7 0x10595d1
#	0x1233ae6	main.query.func1+0x36	/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:16

7 @ 0x102e70b 0x102e7b3 0x10068ed 0x10066c5 0x1233b87 0x10595d1
#	0x1233b86	main.query.func3+0x36	/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:24

1 @ 0x102e70b 0x1029ba9 0x1029256 0x108b7da 0x108b8ed 0x108c216 0x112f80f 0x113b348 0x11f5f6a 0x10595d1
#	0x1029255	internal/poll.runtime_pollWait+0x65		/usr/local/go/src/runtime/netpoll.go:173
#	0x108b7d9	internal/poll.(*pollDesc).wait+0x99		/usr/local/go/src/internal/poll/fd_poll_runtime.go:85
#	0x108b8ec	internal/poll.(*pollDesc).waitRead+0x3c		/usr/local/go/src/internal/poll/fd_poll_runtime.go:90
#	0x108c215	internal/poll.(*FD).Read+0x1d5			/usr/local/go/src/internal/poll/fd_unix.go:169
#	0x112f80e	net.(*netFD).Read+0x4e				/usr/local/go/src/net/fd_unix.go:202
#	0x113b347	net.(*conn).Read+0x67				/usr/local/go/src/net/net.go:177
#	0x11f5f69	net/http.(*connReader).backgroundRead+0x59	/usr/local/go/src/net/http/server.go:676

1 @ 0x102e70b 0x1029ba9 0x1029256 0x108b7da 0x108b8ed 0x108c216 0x112f80f 0x113b348 0x11f63ec 0x10fb596 0x10fbf76 0x10fc174 0x119ebbf 0x119eaeb 0x11f315c 0x11f7672 0x11fb23e 0x10595d1
#	0x1029255	internal/poll.runtime_pollWait+0x65		/usr/local/go/src/runtime/netpoll.go:173
#	0x108b7d9	internal/poll.(*pollDesc).wait+0x99		/usr/local/go/src/internal/poll/fd_poll_runtime.go:85
#	0x108b8ec	internal/poll.(*pollDesc).waitRead+0x3c		/usr/local/go/src/internal/poll/fd_poll_runtime.go:90
#	0x108c215	internal/poll.(*FD).Read+0x1d5			/usr/local/go/src/internal/poll/fd_unix.go:169
#	0x112f80e	net.(*netFD).Read+0x4e				/usr/local/go/src/net/fd_unix.go:202
#	0x113b347	net.(*conn).Read+0x67				/usr/local/go/src/net/net.go:177
#	0x11f63eb	net/http.(*connReader).Read+0xfb		/usr/local/go/src/net/http/server.go:786
#	0x10fb595	bufio.(*Reader).fill+0x105			/usr/local/go/src/bufio/bufio.go:100
#	0x10fbf75	bufio.(*Reader).ReadSlice+0x35			/usr/local/go/src/bufio/bufio.go:341
#	0x10fc173	bufio.(*Reader).ReadLine+0x33			/usr/local/go/src/bufio/bufio.go:370
#	0x119ebbe	net/textproto.(*Reader).readLineSlice+0x6e	/usr/local/go/src/net/textproto/reader.go:55
#	0x119eaea	net/textproto.(*Reader).ReadLine+0x2a		/usr/local/go/src/net/textproto/reader.go:36
#	0x11f315b	net/http.readRequest+0x8b			/usr/local/go/src/net/http/request.go:958
#	0x11f7671	net/http.(*conn).readRequest+0x161		/usr/local/go/src/net/http/server.go:966
#	0x11fb23d	net/http.(*conn).serve+0x49d			/usr/local/go/src/net/http/server.go:1788

1 @ 0x102e70b 0x1029ba9 0x1029256 0x108b7da 0x108b8ed 0x108ce80 0x112fd92 0x1142c5e 0x1141967 0x11ff7df 0x121da4c 0x11fed5f 0x11fea16 0x11ff534 0x1233a91 0x102e317 0x10595d1
#	0x1029255	internal/poll.runtime_pollWait+0x65		/usr/local/go/src/runtime/netpoll.go:173
#	0x108b7d9	internal/poll.(*pollDesc).wait+0x99		/usr/local/go/src/internal/poll/fd_poll_runtime.go:85
#	0x108b8ec	internal/poll.(*pollDesc).waitRead+0x3c		/usr/local/go/src/internal/poll/fd_poll_runtime.go:90
#	0x108ce7f	internal/poll.(*FD).Accept+0x19f		/usr/local/go/src/internal/poll/fd_unix.go:384
#	0x112fd91	net.(*netFD).accept+0x41			/usr/local/go/src/net/fd_unix.go:238
#	0x1142c5d	net.(*TCPListener).accept+0x2d			/usr/local/go/src/net/tcpsock_posix.go:139
#	0x1141966	net.(*TCPListener).AcceptTCP+0x46		/usr/local/go/src/net/tcpsock.go:247
#	0x11ff7de	net/http.tcpKeepAliveListener.Accept+0x2e	/usr/local/go/src/net/http/server.go:3232
#	0x11fed5e	net/http.(*Server).Serve+0x22e			/usr/local/go/src/net/http/server.go:2826
#	0x11fea15	net/http.(*Server).ListenAndServe+0xb5		/usr/local/go/src/net/http/server.go:2764
#	0x11ff533	net/http.ListenAndServe+0x73			/usr/local/go/src/net/http/server.go:3004
#	0x1233a90	main.main+0xb0					/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:40
#	0x102e316	runtime.main+0x206				/usr/local/go/src/runtime/proc.go:201

1 @ 0x122ce28 0x122cc30 0x1229694 0x1233723 0x11fc194 0x11fde37 0x11fe8eb 0x11fb3e6 0x10595d1
#	0x122ce27	runtime/pprof.writeRuntimeProfile+0x97			/usr/local/go/src/runtime/pprof/pprof.go:707
#	0x122cc2f	runtime/pprof.writeGoroutine+0x9f			/usr/local/go/src/runtime/pprof/pprof.go:669
#	0x1229693	runtime/pprof.(*Profile).WriteTo+0x3e3			/usr/local/go/src/runtime/pprof/pprof.go:328
#	0x1233722	study/goroutine/leak/06/leak.GoroutineStack+0x92	/Users/polo/Public/Work/go/src/study/goroutine/leak/06/leak/handlers.go:19
#	0x11fc193	net/http.HandlerFunc.ServeHTTP+0x43			/usr/local/go/src/net/http/server.go:1964
#	0x11fde36	net/http.(*ServeMux).ServeHTTP+0x126			/usr/local/go/src/net/http/server.go:2361
#	0x11fe8ea	net/http.serverHandler.ServeHTTP+0xaa			/usr/local/go/src/net/http/server.go:2741
#	0x11fb3e5	net/http.(*conn).serve+0x645				/usr/local/go/src/net/http/server.go:1847

首先是第一行,如下:

goroutine profile: total 1004

統(tǒng)計(jì)信息,和 NumGoroutine 的返回結(jié)果相同。當(dāng)前共有 1004 個(gè) goroutine 在運(yùn)行。

接下來(lái)的部分,主要是具體介紹每個(gè) goroutine 的情況,相同函數(shù)的 goroutine 會(huì)被合并統(tǒng)計(jì),并按數(shù)量從大到小排序。輸出前三段就是我們?cè)?query 函數(shù)中開(kāi)啟的三個(gè) goroutine。

948 @ 0x102e70b 0x102e7b3 0x10068ed 0x10066c5 0x1233b37 0x10595d1
#	0x1233b36	main.query.func2+0x36	/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:20

45 @ 0x102e70b 0x102e7b3 0x10068ed 0x10066c5 0x1233ae7 0x10595d1
#	0x1233ae6	main.query.func1+0x36	/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:16

7 @ 0x102e70b 0x102e7b3 0x10068ed 0x10066c5 0x1233b87 0x10595d1
#	0x1233b86	main.query.func3+0x36	/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:24

分別是 main.query.func1、main.query.func2 以及 main.query.func3,對(duì)應(yīng)于它們,當(dāng)前仍在運(yùn)行中的 goroutine 數(shù)量分別是 45、948、7??礃幼有孤兜?goroutine 函數(shù)分布并非均勻。

幾個(gè)函數(shù)都是匿名的,如果我們需要確定具體位置,可以通過(guò)堆棧實(shí)現(xiàn)。比如 func1,明確指出了位于的所在文件和代碼行數(shù)。

http/net/pprof

前面部分是通過(guò)自己編寫(xiě)代碼把 goroutine 的分析統(tǒng)計(jì)指標(biāo)加入到了 HTTP 服務(wù)中。其實(shí),官方已經(jīng)實(shí)現(xiàn)了這個(gè)功能,并且涉及的不僅僅是 goroutine,還有 CPU、內(nèi)存等。

它的操作很簡(jiǎn)單,我們只需要在服務(wù)啟動(dòng)時(shí)導(dǎo)入 net/http/pprof 即可。接著訪問(wèn)地址 /debug/pprof/goroutine?debug=1,將會(huì)可以看到與上一節(jié)輸出的相同內(nèi)容。

gops

熟悉 Java 的朋友都知道 jps 這個(gè)命令。通過(guò)它,我們可以查看當(dāng)前機(jī)器上有哪些 Java 程序在運(yùn)行。Go 也有類似的命令,gops,它支持列出當(dāng)前環(huán)境下的 Go 進(jìn)程,并支持對(duì) Go 程序的診斷。默認(rèn)情況下,gops 可列出并不支持對(duì)進(jìn)程進(jìn)行成診斷。

今天,我們將只看它和 goroutine 相關(guān)的部分。

一個(gè)示例,如下:

$ gops
97778 96800 gops    go1.11.1 /usr/local/go/bin/gops
97605 73594 leaker* go1.11.1 /Users/polo/Public/Work/go/src/study/goroutine/leak/06/leaker

我的環(huán)境下當(dāng)前只有兩個(gè) go 進(jìn)程在運(yùn)行。

仔細(xì)觀察后,我們會(huì)發(fā)現(xiàn) leaker 進(jìn)程相比 gops 后面多個(gè) * 的標(biāo)號(hào),而 * 表示這個(gè)程序支持通過(guò) gops 診斷。這是因?yàn)槲覀冊(cè)?leaker 加入了診斷支持的代碼,如下:

func main() {
	if err := agent.Listen(agent.Options{ShutdownCleanup: true}); err != nil {
		log.Fatalln(err)
	}

	...
}

執(zhí)行如下命令,查看當(dāng)前的 goroutine 數(shù)量。

$ gops stats 97605
goroutines: 1004
OS threads: 14
GOMAXPROCS: 8
num CPU: 8

其中,97605 是進(jìn)程 PID。

結(jié)果顯示,當(dāng)前在運(yùn)行的 goroutine 有 1004 個(gè)。而且,我們還注意到 OS 級(jí)別的線程才 14 個(gè),可見(jiàn) goroutine 的輕量。

gops 也可以查看堆棧,我們只需執(zhí)行 gops stack PID 即可,這個(gè)就不具體演示了。要說(shuō)明的是,這種方式并不會(huì)對(duì)運(yùn)行相同函數(shù)的 goroutine 做聚合統(tǒng)計(jì),不知道是我沒(méi)找到還是本身不支持。如果的確不支持,也可以自己聚合,但畢竟沒(méi)那么方便。

Leak Test

除了出現(xiàn)問(wèn)題后的檢測(cè)調(diào)試,但如果我們能把泄露檢測(cè)過(guò)程加入到自動(dòng)化測(cè)試中,在正式上線前就避免,豈不是更完美。我們可以通過(guò)一個(gè)開(kāi)源包實(shí)現(xiàn),包的名稱是 leaktest,即泄露測(cè)試的意思。

利用 leaktest,我們測(cè)試下前面寫(xiě)的 http 處理函數(shù) query。因?yàn)橐獧z測(cè) handler 是否泄露,如果經(jīng)過(guò)網(wǎng)絡(luò)就會(huì)丟失服務(wù)端的相關(guān)信息,這時(shí),我們可以借助 Go 中的 net/http/test 包完成測(cè)試。

代碼如下:

func Test_Query(t *testing.T) {
	defer leaktest.Check(t)()

	//創(chuàng)建一個(gè)請(qǐng)求
	req, err := http.NewRequest("GET", "/query", nil)
	if err != nil {
		t.Fatal(err)
	}

	rr := httptest.NewRecorder()

	//直接使用 query(rr,req)
	query(rr, req)

	// 其他測(cè)試
	// ...
}

測(cè)試執(zhí)行輸出如下:

=== RUN   Test_Query
--- FAIL: Test_Query (5.01s)
    leaktest.go:162: leaktest: context canceled
    leaktest.go:168: leaktest: leaked goroutine: goroutine 20 [chan send]:
        study/goroutine/leak/06.query.func2(0xc0001481e0)
        	/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:24 +0x37
        created by study/goroutine/leak/06.query
        	/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:23 +0x7e
FAIL

從輸出信息中,我們可以明確地知道出現(xiàn)了泄露,并且通過(guò)輸出堆棧很快就能定位出現(xiàn)問(wèn)題的代碼。測(cè)試代碼非常簡(jiǎn)單,在測(cè)試函數(shù)開(kāi)始通過(guò) defer 執(zhí)行 leaktest 的 Check。

它提供的三個(gè)檢測(cè)函數(shù),分別是 Check、CheckTimeout 和 CheckContext,從前到后的實(shí)現(xiàn)一個(gè)比一個(gè)底層。Check 默認(rèn)會(huì)等待五秒再執(zhí)行檢測(cè),如果需要改變這個(gè)時(shí)間,可以使用 CheckTimeout 函數(shù)。

上述就是小編為大家分享的golang中怎么防止goroutine泄露了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI