新聞中心
本篇文章帶大家聊聊Golang的協(xié)程泄露,介紹一下Go怎么預(yù)防泄露,希望對(duì)大家有所幫助!

在網(wǎng)站制作、網(wǎng)站設(shè)計(jì)過程中,需要針對(duì)客戶的行業(yè)特點(diǎn)、產(chǎn)品特性、目標(biāo)受眾和市場(chǎng)情況進(jìn)行定位分析,以確定網(wǎng)站的風(fēng)格、色彩、版式、交互等方面的設(shè)計(jì)方向。創(chuàng)新互聯(lián)建站還需要根據(jù)客戶的需求進(jìn)行功能模塊的開發(fā)和設(shè)計(jì),包括內(nèi)容管理、前臺(tái)展示、用戶權(quán)限管理、數(shù)據(jù)統(tǒng)計(jì)和安全保護(hù)等功能。
協(xié)程在Go里面是一個(gè)常見的概念,伴隨著Go程序的生命周期開始至結(jié)束。今天來細(xì)聊Go的協(xié)程泄露?!鞠嚓P(guān)推薦:Go視頻教程】
關(guān)于協(xié)程泄露很多時(shí)候我們往往會(huì)忽略它,直到機(jī)器資源負(fù)載異常才引起重視。 之前排除生產(chǎn)環(huán)境異常的時(shí)候,曾經(jīng)遇到過go程序內(nèi)存泄露的場(chǎng)景,內(nèi)存泄漏和協(xié)程泄露有很大關(guān)系,本質(zhì)上都是資源不回收導(dǎo)致的。
這里列舉一個(gè)典型的泄露案例:
func JumpForSignal() int {
ch := make(chan int)
go func() {
ch <- bizMtx
}()
go func() {
ch <- bizMtx
}()
go func() {
ch <- bizMtx
}()
//一有輸入立刻返回
return <-ch
}
func main() {
// ...
JumpForSignal()
// ...
}
事后分析這個(gè)demo可以得知,這個(gè)函數(shù)調(diào)用會(huì)阻塞兩個(gè)子協(xié)程,預(yù)期只有一個(gè)協(xié)程會(huì)正常退出。
獲取協(xié)程信息
既然存在協(xié)程泄露,我們?cè)谌粘9ぷ髟趺幢苊饣蛘甙l(fā)現(xiàn)它呢?下面我們列舉幾個(gè)思路。
遵守準(zhǔn)則
由于Go是自帶GC的語言,很多時(shí)候?qū)懘a不需要關(guān)心變量的資源釋放,不像C程序員變量申請(qǐng)之后需要在結(jié)束處釋放。但是Go的chan在使用時(shí)候是有一些準(zhǔn)則的,當(dāng)確定chan不再使用時(shí)候,可以在輸出方進(jìn)行close,避免其他協(xié)程還在等待該chan的輸出。
協(xié)程數(shù)量
找到泄露的協(xié)程,第一個(gè)能夠想到的是協(xié)程數(shù)量,當(dāng)你的函數(shù)處理邏輯比較簡(jiǎn)單,除了主協(xié)程之外,預(yù)期協(xié)程應(yīng)該都在結(jié)束前返回,可以在main函數(shù)結(jié)束處調(diào)用runtime包的函數(shù):
// NumGoroutine returns the number of goroutines that currently exist.
func NumGoroutine() int {
return int(gcount())
}
通過它可以返回當(dāng)前協(xié)程總數(shù)量:
func Count() {
fmt.Printf("Number of goroutines:%d\n", runtime.NumGoroutine())
}
func main() {
defer Count()
Count()
JumpForSignal()
}
輸出:
Number of goroutines:1 Number of goroutines:3
協(xié)程函數(shù)棧
還有一種比較常見定位協(xié)程的形式,在Go里面,可以用于分析協(xié)程函數(shù)的上下文,常見的比如go自帶的pprof也是通過這種方式獲取,實(shí)際案例中,條件允許的情況可以開啟pprof方便分析。
下面來看一個(gè)示例,我們?cè)谏厦娴睦蛹右粋€(gè)http端口監(jiān)聽,用于接入go自帶的pprof分析工具。
隨后在瀏覽器輸入:
http://localhost:8899/debug/pprof/goroutine?debug=1
可以得到整個(gè)程序的協(xié)程列表:
goroutine profile: total 7 1 @ 0x165eb6 0x126465 0x126235 0x29341e 0x19de01 # 0x29341d pixelgo/leak.JumpForSignal.func1+0x3d F:/code/pixelGo/src/pix-demo/leak/leak.go:24 1 @ 0x165eb6 0x126465 0x126235 0x29347e 0x19de01 # 0x29347d pixelgo/leak.JumpForSignal.func2+0x3d F:/code/pixelGo/src/pix-demo/leak/leak.go:28 1 @ 0x165eb6 0x15bb3d 0x1975a5 0x228d05 0x229d8d 0x22c40d 0x321765 0x33437c 0x447c89 0x285239 0x285606 0x4493f3 0x450da8 0x19de01 # 0x1975a4 internal/poll.runtime_pollWait+0x64 D:/dev/go1.16/src/runtime/netpoll.go:227 # 0x228d04 internal/poll.(*pollDesc).wait+0xa4 D:/dev/go1.16/src/internal/poll/fd_poll_runtime.go:87 # 0x229d8c internal/poll.execIO+0x2ac D:/dev/go1.16/src/internal/poll/fd_windows.go:175 # 0x22c40c internal/poll.(*FD).Read+0x56c // ...
結(jié)論是:當(dāng)前程序一共有7個(gè)協(xié)程,可以看出分別有1個(gè)協(xié)程分配在F:/code/pixelGo/src/pix-demo/leak/leak.go:24 和F:/code/pixelGo/src/pix-demo/leak/leak.go:28,正是上文泄露的代碼塊。
有時(shí)候還可以多維度去分析,比如輸入:
http://localhost:8899/debug/pprof/goroutine?debug=2
可以通過協(xié)程后面的標(biāo)簽,看到當(dāng)前協(xié)程的不同狀態(tài),running/io wait/chan send
goroutine 9 [running]:
runtime/pprof.writeGoroutineStacks(0x7f7d00, 0xc0000aa000, 0x0, 0x0)
D:/dev/go1.16/src/runtime/pprof/pprof.go:693 +0xc5
net/http/pprof.handler.ServeHTTP(0xc000094011, 0x9, 0x7fba40, 0xc0000aa000, 0xc000092000)
//..
goroutine 1 [IO wait]:
internal/poll.runtime_pollWait(0x223debb10d8, 0x72, 0xc000152f48)
D:/dev/go1.16/src/runtime/netpoll.go:227 +0x65
internal/poll.(*pollDesc).wait(0xc0001530b8, 0x72, 0x93b400, 0x0, 0x0)
//...
goroutine 6 [chan send]:
pixelgo/rout.JumpForSignal.func1(0xc000053800)
F:/code/pixelGo/src/pix-demo/rout/leak.go:25 +0x10e
created by pixelgo/rout.JumpForSignal
F:/code/pixelGo/src/pix-demo/rout/leak.go:23 +0x71
goroutine 7 [chan send]:
pixelgo/rout.JumpForSignal.func2(0xc000053800)
F:/code/pixelGo/src/pix-demo/rout/leak.go:30 +0x10e
created by pixelgo/rout.JumpForSignal
F:/code/pixelGo/src/pix-demo/rout/leak.go:28 +0x93
協(xié)程id
接下來我們來探索協(xié)程標(biāo)識(shí):協(xié)程id,在Go中,每個(gè)運(yùn)行的協(xié)程都會(huì)分配一個(gè)協(xié)程id,一個(gè)常見的方式是從函數(shù)運(yùn)行棧獲取,引用之前網(wǎng)上其他同學(xué)的寫法:
func main() {
fmt.Println(getGID())
}
func getGID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
我們來看看runtime.stack() 會(huì)返回什么呢,其中真實(shí)內(nèi)容是這樣的:
goroutine 21 [running]: leaktest.interestingGoroutines(0xdb9980, 0xc00038e018, 0x0, 0x0, 0x0) F:/code/pixelGo/src/leaktest/leaktest.go:81 +0xbf leaktest.CheckContext(0xdbe398, 0xc000108040, 0xdb9980, 0xc00038e018, 0x0) F:/code/pixelGo/src/leaktest/leaktest.go:141 +0x6e leaktest.CheckTimeout(0xdb9980, 0xc00038e018, 0x3b9aca00, 0x0) F:/code/pixelGo/src/leaktest/leaktest.go:127 +0xe5 leaktest.TestCheck.func8(0xc000384780) F:/code/pixelGo/src/leaktest/leaktest_test.go:122 +0xaf testing.tRunner(0xc000384780, 0xc000100050) D:/dev/go1.16/src/testing/testing.go:1193 +0x1a3 created by testing.(*T).Run D:/dev/go1.16/src/testing/testing.go:1238 +0x63c goroutine 1 [chan receive]: testing.(*T).Run(0xc000037080, 0xd8486a, 0x9, 0xd9ebc8, 0x304bd824304bd800) D:/dev/go1.16/src/testing/testing.go:1239 +0x66a testing.runTests.func1(0xc000036f00) D:/dev/go1.16/src/testing/testing.go:1511 +0xbd testing.tRunner(0xc000036f00, 0xc00008fc00) D:/dev/go1.16/src/testing/testing.go:1193 +0x1a3 testing.runTests(0xc0000040d8, 0xf40460, 0x5, 0x5, 0x0, 0x0, 0x0, 0x21cbf1c0100) D:/dev/go1.16/src/testing/testing.go:1509 +0x448 testing.(*M).Run(0xc0000c0000, 0x0) D:/dev/go1.16/src/testing/testing.go:1417 +0x514 main.main() _testmain.go:51 +0xc8
可以發(fā)現(xiàn)這個(gè)棧和我們運(yùn)行panic拋出的信息非常類似,需要注意的是,通過這種方式獲取協(xié)程id并不是一個(gè)高效的方式。
實(shí)際生產(chǎn)使用過程并不提倡,值得一提的是,為了方便我們更好的定位問題上下文,有時(shí)候日志框架又需要我們打印出當(dāng)前協(xié)程id。
比如這是一個(gè)生產(chǎn)案例日志輸出:
// gid-1號(hào)協(xié)程用于初始化資源
[0224/162532.310:INFO:gid-1:yx_trace.go:66] cfg:&{ false false [] 0xc000295140 0xc0001d4e00 }
[0224/162532.320:INFO:gid-1:main.go:50] GameRoom Startup->
[0224/162532.320:INFO:gid-1:config_manager.go:107] configManager SetHttpListenAddr:8080
[0224/162532.320:INFO:gid-1:room_manager.go:57] roomManager Startup
[0224/162532.323:INFO:gid-1:room_manager.go:72] roomManager initPrx.
[0224/162532.330:INFO:gid-1:bootstrap.go:153] GameRoom START ok.
// gid-60號(hào)協(xié)程分配用于啟動(dòng)HTTP Server
[0224/162533.277:INFO:gid-60:expose.go:36] Start for HTTP server...
[0224/162533.277:INFO:gid-60:expose.go:39] register for debug server...
往往日志框架是力求對(duì)業(yè)務(wù)性能影響最低的,既然有性能顧慮,那么它是怎么獲取協(xié)程id的呢?只能曲線救國了。
還有一個(gè)解法,其實(shí)在Go中,每個(gè)協(xié)程綁定的系統(tǒng)線程結(jié)構(gòu)中,有一個(gè)g指針,拿到g指針的信息之后,根據(jù)g指針結(jié)構(gòu)的偏移量(注意不同go版本可能不同),指定獲取id。
匯編獲取
通過協(xié)程綁定的g指針,這里參考《Go高級(jí)編程》的做法
// 記錄各個(gè)版本的偏移量
var offsetDictMap = map[string]int64{
"go1.12": 152,
"go1.12.1": 152,
"go1.12.2": 152,
"go1.12.3": 152,
"go1.12.4": 152,
"go1.12.5": 152,
"go1.12.6": 152,
"go1.12.7": 152,
"go1.13": 152,
"go1.14": 152,
"go1.16.12": 152,
}
// offset for go1.12
var goid_offset uintptr = 152
//go:nosplit
func getG() interface{}
func GoId() int64
// 部分匯編代碼
// func getGptr() unsafe.Pointer
TEXT ·getGptr(SB), NOSPLIT, $0-8
MOVQ (TLS), BX
MOVQ BX, ret+0(FP)
RET
TEXT ·GoId(SB),NOSPLIT,$0-8
NO_LOCAL_POINTERS
MOVQ ·goid_offset(SB),AX
// get runtime.g
MOVQ (TLS),BX
ADDQ BX,AX
MOVQ (AX),BX
MOVQ BX,ret+0(FP)
RET
這里點(diǎn)到為止,大概思路是這樣。
性能比較:
我們來簡(jiǎn)單測(cè)試下兩種獲取go協(xié)程id方式性能差距:
// BenchmarkGRtId-8 1000000000 0.0005081 ns/op
func BenchmarkGRtId(b *testing.B) {
for n := 0; n < 1000000000; n++ {
// runtime獲取協(xié)程id
getGID()
}
}
// BenchmarkGoId-8 1000000000 0.05731 ns/op
func BenchmarkGoId(b *testing.B) {
for n := 0; n < 1000000000; n++ {
// 匯編方式獲取
GoId()
}
}
可以看到通過匯編方式獲取協(xié)程id的方式性能更優(yōu),相差幾個(gè)數(shù)量級(jí)。
限制協(xié)程
上面列舉了幾個(gè)定位協(xié)程信息的方法,那么在協(xié)程泄露之前有沒有其他方式對(duì)程序的go協(xié)程進(jìn)行管控呢,有個(gè)做法是使用強(qiáng)大的channel坐下限制。
拋磚引玉
這里先提供一個(gè)簡(jiǎn)單的思路,即再包裝一層channel進(jìn)行保護(hù),
// 限制數(shù)量
var LIMIT_G_NUM = make(chan struct{}, 100)
// 需要自定義的處理邏輯
type HandleFun func()
func AsyncGoForHandle(fn HandleFun) {
// 計(jì)數(shù)加一
LIMIT_G_NUM <- struct{}{}
go func() {
defer func() {
if err := recover(); err != nil {
log.Fatalf("AsyncGoForHandle recover from err: %v", err)
}
// 回收計(jì)數(shù)
<-LIMIT_G_NUM
}()
// 處理邏輯
fn()
}()
}
上面的思路比較簡(jiǎn)單,相信大家能看懂,每次需要異步創(chuàng)建協(xié)程只要調(diào)用AsyncGoForHandle()函數(shù)即可,不足之處可能是處理邏輯HandleFun()不夠通用,需要自己定義具體實(shí)現(xiàn)。
還有一種方式,就是引入協(xié)程池的概念,這里的池子和數(shù)據(jù)庫連接池有點(diǎn)像,即一開始就預(yù)創(chuàng)建好,業(yè)務(wù)層只要負(fù)責(zé)提交數(shù)據(jù),業(yè)界已經(jīng)有不少成熟的封裝。
成熟方案:tunny
之前看到社區(qū)有一個(gè)封裝得比較完善的協(xié)程池tunny,代碼行數(shù)不多,我們來試著拆解分析一下代碼,項(xiàng)目地址:https://github.com/Jeffail/tunny
1、定義處理邏輯接口:
type Worker interface {
// 自定義邏輯實(shí)現(xiàn),開發(fā)者只需要關(guān)心入?yún)⒑统鰠? Process(interface{}) interface{}
}
2、包裝worker的輸入源workRequest
type workerWrapper struct {
// 注入內(nèi)部實(shí)現(xiàn)邏輯
worker Worker
interruptChan chan struct{}
// 請(qǐng)求來源workRequest
reqChan chan<- workRequest
// ...
}
3、輸入源結(jié)構(gòu)
type workRequest struct {
// 輸入
jobChan chan<- interface{}
// 處理結(jié)果,即worker.Process()的返回值
retChan <-chan interface{}
// ...
}
4、編寫實(shí)現(xiàn)類:
我們知道Go的接口遵循鴨子模型: 只要它表現(xiàn)得像個(gè)鴨子,它就是鴨子
// Worker實(shí)現(xiàn)類
type closureWorker struct {
processor func(interface{}) interface{}
}
func (w *closureWorker) Process(payload interface{}) interface{} {
return w.processor(payload)
}
5、定義工作池結(jié)構(gòu)
type Pool struct {
queuedJobs int64
// 成員函數(shù),用于"鴨子"實(shí)體
ctor func() Worker
workers []*workerWrapper
reqChan chan workRequest
workerMut sync.Mutex
}
func NewFunc(n int, f func(interface{}) interface{}) *Pool {
return New(n, func() Worker {
return &closureWorker{
// 傳入真正的實(shí)現(xiàn)模塊
processor: f,
}
})
}
func New(n int, ctor func() Worker) *Pool {
p := &Pool{
ctor: ctor,
reqChan: make(chan workRequest),
}
// 批量創(chuàng)建協(xié)程,監(jiān)聽處理來自reqChan的任務(wù)
p.SetSize(n)
return p
}
相關(guān)實(shí)體結(jié)構(gòu)如下,配合源碼閱讀就比較清晰了。
這個(gè)框架相當(dāng)于把協(xié)程預(yù)先創(chuàng)建好做了池化,隨后業(yè)務(wù)層只需要源源不斷把"加工數(shù)據(jù)"輸入到workRequest這個(gè)chan即可,也就是process()函數(shù),process()模塊會(huì)把數(shù)據(jù)輸入到內(nèi)部channel進(jìn)行處理,池中的worker會(huì)進(jìn)行加工。
這種工廠模式還是值得借鑒的,Go也有很多成熟框架使用了這種寫法。
引用原項(xiàng)目README.md的用法示例:
numCPUs := runtime.NumCPU()
pool := tunny.NewFunc(numCPUs, func(payload interface{}) interface{} {
var result []byte
// 關(guān)心業(yè)務(wù)層的輸入、輸出即可
result = wrapSomething()
return result
})
defer pool.Close()
http.HandleFunc("/work", func(w http.ResponseWriter, r *http.Request) {
input, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
}
defer r.Body.Close()
// 提交任務(wù)給Process
result := pool.Process(input)
w.Write(result.([]byte))
})
http.ListenAndServe(":8080", nil)
總結(jié)
-
Go協(xié)程有幾個(gè)內(nèi)置信息,協(xié)程id、協(xié)程棧、協(xié)程狀態(tài)(running/io wait/chan send),通過這些信息可以幫助我們一定程度的避免或者定位問題
-
Go里面創(chuàng)建協(xié)程只需要一個(gè)Go關(guān)鍵字,但是要合理回收卻很關(guān)鍵,必要時(shí)可以用協(xié)程池做限制
網(wǎng)頁名稱:聊聊Golang的協(xié)程泄露,看看怎么預(yù)防泄露
分享路徑:http://www.dlmjj.cn/article/dhjddps.html


咨詢
建站咨詢
