您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關golang中怎么防止goroutine泄露,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
runtime.NumGoroutine 可以獲取當前進程中正在運行的 goroutine 數量,觀察這個數字可以初步判斷出是否存在 goroutine 泄露異常。
一個示例,如下:
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) }
功能很簡單,設置 _count 路由請求處理函數 count,它負責輸出服務當前 goroutine 數量。啟動服務后訪問 localhost:6080/_count 即可。
但只是一個數值,我們就能確認是否泄露了嗎?
首先,如果這個數值很大,是不是就能說明出現了泄露。我的答案是否。理由很簡單,高并發情況下的 goroutine 數量肯定很高的,但并非出現了泄露,可能只是當前的服務的承載能力還不夠。我們可以在數量基礎上引入時間,即如果 goroutine 隨著時間增加,數量在不斷上升,而基本沒有下降,基本可以確定存在泄露。我們可以定時采集不同時刻的數據來分析。
為了更好的演示效果,我們為服務再增加一個處理函數 query, 并綁定路由 /query 上。假設它負責從多個數據表中查出數據返回給用戶。這個例子在后面的演示會一直使用。
代碼如下:
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 中,我們啟動了 3 個 goroutine 執行數據庫查詢,通過 channel 傳遞返回數據。這里的問題是,query 函數中只從 channel 中接收兩次數據就退出了循環,這會導致其中一個 goroutine 因缺少接收者而無法釋放。
我們可以多次請求 localhost:6080/query
,然后通過 _count 查看服務當前的 goroutine 數量。手動麻煩,可以用 ab 命令進行做個簡單壓測。
$ ab -n 1000 -c 100 localhost:6080/query
命令的意思是,總共訪問 1000 次,并發訪問 100 次。
前面的例子比較簡單,發現泄露后,我們可以立刻確定存在的問題。但如果比較復雜的項目,我們就很難發現問題代碼的出現位置了。
如何解決呢?
我們可以引入一個輔助工具,pprof。它是由 Go 官方提供的可用于收集程序運行時報告的工具,其中包含 CPU、內存等信息。當然,也可以獲取運行時 goroutine 堆棧信息,如此一來,我們就可以很容易看出哪里導致了 goroutine 泄露。
我們可以再加入一個名為 goroutineStack 的 handler,用于查看程序中 goroutine 的堆棧信息,,地址為 _goroutine
。
實現代碼如下:
import "runtime/pprof" func goroutineStack(w http.ResponseWriter, r *http.Request) { _ = pprof.Lookup("goroutine").WriteTo(w, 1) }
訪問 _goroutine
,將會得到類似如下的信息:
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
統計信息,和 NumGoroutine 的返回結果相同。當前共有 1004 個 goroutine 在運行。
接下來的部分,主要是具體介紹每個 goroutine 的情況,相同函數的 goroutine 會被合并統計,并按數量從大到小排序。輸出前三段就是我們在 query 函數中開啟的三個 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,對應于它們,當前仍在運行中的 goroutine 數量分別是 45、948、7。看樣子泄露的 goroutine 函數分布并非均勻。
幾個函數都是匿名的,如果我們需要確定具體位置,可以通過堆棧實現。比如 func1,明確指出了位于的所在文件和代碼行數。
前面部分是通過自己編寫代碼把 goroutine 的分析統計指標加入到了 HTTP 服務中。其實,官方已經實現了這個功能,并且涉及的不僅僅是 goroutine,還有 CPU、內存等。
它的操作很簡單,我們只需要在服務啟動時導入 net/http/pprof 即可。接著訪問地址 /debug/pprof/goroutine?debug=1,將會可以看到與上一節輸出的相同內容。
熟悉 Java 的朋友都知道 jps 這個命令。通過它,我們可以查看當前機器上有哪些 Java 程序在運行。Go 也有類似的命令,gops,它支持列出當前環境下的 Go 進程,并支持對 Go 程序的診斷。默認情況下,gops 可列出并不支持對進程進行成診斷。
今天,我們將只看它和 goroutine 相關的部分。
一個示例,如下:
$ 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
我的環境下當前只有兩個 go 進程在運行。
仔細觀察后,我們會發現 leaker 進程相比 gops 后面多個 * 的標號,而 * 表示這個程序支持通過 gops 診斷。這是因為我們在 leaker 加入了診斷支持的代碼,如下:
func main() { if err := agent.Listen(agent.Options{ShutdownCleanup: true}); err != nil { log.Fatalln(err) } ... }
執行如下命令,查看當前的 goroutine 數量。
$ gops stats 97605 goroutines: 1004 OS threads: 14 GOMAXPROCS: 8 num CPU: 8
其中,97605 是進程 PID。
結果顯示,當前在運行的 goroutine 有 1004 個。而且,我們還注意到 OS 級別的線程才 14 個,可見 goroutine 的輕量。
gops 也可以查看堆棧,我們只需執行 gops stack PID 即可,這個就不具體演示了。要說明的是,這種方式并不會對運行相同函數的 goroutine 做聚合統計,不知道是我沒找到還是本身不支持。如果的確不支持,也可以自己聚合,但畢竟沒那么方便。
除了出現問題后的檢測調試,但如果我們能把泄露檢測過程加入到自動化測試中,在正式上線前就避免,豈不是更完美。我們可以通過一個開源包實現,包的名稱是 leaktest,即泄露測試的意思。
利用 leaktest,我們測試下前面寫的 http 處理函數 query。因為要檢測 handler 是否泄露,如果經過網絡就會丟失服務端的相關信息,這時,我們可以借助 Go 中的 net/http/test 包完成測試。
代碼如下:
func Test_Query(t *testing.T) { defer leaktest.Check(t)() //創建一個請求 req, err := http.NewRequest("GET", "/query", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() //直接使用 query(rr,req) query(rr, req) // 其他測試 // ... }
測試執行輸出如下:
=== 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
從輸出信息中,我們可以明確地知道出現了泄露,并且通過輸出堆棧很快就能定位出現問題的代碼。測試代碼非常簡單,在測試函數開始通過 defer 執行 leaktest 的 Check。
它提供的三個檢測函數,分別是 Check、CheckTimeout 和 CheckContext,從前到后的實現一個比一個底層。Check 默認會等待五秒再執行檢測,如果需要改變這個時間,可以使用 CheckTimeout 函數。
上述就是小編為大家分享的golang中怎么防止goroutine泄露了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。