08.22 「GCTT 出品」Goroutine 洩露

「GCTT 出品」Goroutine 洩露

Goroutine 洩露

Go 中的併發性是以 goroutine(獨立活動)和 channel(用於通信)的形式實現的。處理 goroutine 時,程序員需要小心翼翼地避免洩露。如果最終永遠堵塞在 I/O 上(例如 channel 通信),或者陷入死循環,那麼 goroutine 會發生洩露。即使是阻塞的 goroutine,也會消耗資源,因此,程序可能會使用比實際需要更多的內存,或者最終耗盡內存,從而導致崩潰。讓我們來看看幾個可能會發生洩露的例子。然後,我們將重點關注如何檢測程序是否受到這種問題的影響。

發送到一個沒有接收者的 channel

假設出於冗餘的目的,程序發送請求到許多後端。使用首先收到的響應,丟棄後面的響應。下面的代碼將會通過等待隨機數毫秒,來模擬向下遊服務器發送請求:

package main

import (

"fmt"

"math/rand"

"runtime"

"time"

)

func query() int {

n := rand.Intn(100)

time.Sleep(time.Duration(n) * time.Millisecond)

return n

}

func queryAll() int {

ch := make(chan int)

go func() { ch

go func() { ch

go func() { ch

return

}

func main() {

for i := 0; i < 4; i++ {

queryAll()

fmt.Printf("#goroutines: %d", runtime.NumGoroutine())

}

}

輸出:

#goroutines: 3

#goroutines: 5

#goroutines: 7

#goroutines: 9

每次調用 queryAll 後,goroutine 的數目會發生增長。問題在於,在接收到第一個響應後,“較慢的” goroutine 將會發送到另一端沒有接收者的 channel 中。

可能的解決方法是,如果提前知道後端服務器的數量,那麼使用緩存 channel。否則,只要至少有一個 goroutine 仍在工作,我們就可以使用另一個 goroutine 來接收來自這個 channel 的數據。其他的解決方案可能是使用 context(example),利用 某些機制來取消其他請求。

從沒有發送者的 channel 中接收數據

這種場景類似於發送到一個沒有接收者的 channel。洩露 goroutine 這篇文章中包含了一個示例。

nil channel

寫入到 nil channel 會永遠阻塞:

package main

func main() {

var ch chan struct{}

ch

}

所以它導致死鎖:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send (nil chan)]:

main.main()

...

當從 nil channel 讀取數據時,同樣的事情發生了:

var ch chan struct{}

當傳遞尚未初始化的 channel 時,也可能會發生:

package main

import (

"fmt"

"runtime"

"time"

)

func main() {

var ch chan int

if false {

ch = make(chan int, 1)

ch

}

go func(ch chan int) {

}(ch)

c := time.Tick(1 * time.Second)

for range c {

fmt.Printf("#goroutines: %d", runtime.NumGoroutine())

}

}

在這個例子中,有一個顯而易見的罪魁禍首 —— if false {,但是在更大的程序中,更容易忘記這件事,然後使用 channel 的零值(nil)。

死循環

goroutine 洩露不僅僅是因為 channel 的錯誤使用造成的。洩露的原因也可能是 I/O 操作上的堵塞,例如發送請求到 API 服務器,而沒有使用超時。另一種原因是,程序可以單純地陷入死循環中。

分析

runtime.NumGoroutine

簡單的方式是使用由 runtime.NumGoroutine 返回的值。

net/http/pprof

import (

"log"

"net/http"

_ "net/http/pprof"

)

...

log.Println(http.ListenAndServe("localhost:6060", nil))

調用 http://localhost:6060/debug/pprof/goroutine?debug=1 ,將會返回帶有堆棧跟蹤的 goroutine 列表。

runtime/pprof

要將現有的 goroutine 的堆棧跟蹤打印到標準輸出,請執行以下操作:

import (

"os"

"runtime/pprof"

)

...

pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)

gops

> go get -u github.com/google/gops

集成到你的程序中:

import "github.com/google/gops/agent"

...

if err := agent.Start(); err != nil {

log.Fatal(err)

}

time.Sleep(time.Hour)

> ./bin/gops

12365 gops (/Users/mlowicki/projects/golang/spec/bin/gops)

12336* lab (/Users/mlowicki/projects/golang/spec/bin/lab)

> ./bin/gops vitals -p=12336

goroutines: 14

OS threads: 9

GOMAXPROCS: 4

num CPU: 4

leaktest

這是用測試來自動檢測洩露的方法之一。它基本上是在測試的開始和結束的時候,利用 runtime.Stack 獲取活躍 goroutine 的堆棧跟蹤。如果在測試完成後還有一些新的 goroutine,那麼將其歸類為洩露。


分析甚至已經在運行的程序的 goroutine 管理,以避免可能會導致內存不足的洩露,這至關重要。代碼在生產上運行數日後,這樣的問題通常就會出現,因此它可能會造成真正的損害。

點擊原文中的 ❤ 以幫助其他人發現這個問題。如果你想實時獲得新的更新,請關注原作者哦~

資源

  • 包 —— Go 編程語言
  • bufio 包實現了緩存 I/O。它封裝一個 io.Reader 或者 io.Writer 對象,創建其他對象(Reader 或者……)
  • google/gops
  • gops —— 一個列出和診斷當前運行在你的系統上的 Go 進程的工具。
  • runtime:檢測殭屍 goroutine · 問題 #5308 · golang/go
  • runtime 可以檢測不可達 channel / mutex 等上面的 goroutine 阻塞,然後報告此類問題。這需要一個接口……
  • fortytw2/leaktest
  • leaktest - goroutine 洩露檢測器。


分享到:


相關文章: