在我們談?wù)搮f(xié)程(Goroutines)泄漏之前,我們先看看并發(fā)編程的概念。并發(fā)編程處理程序的并發(fā)執(zhí)行。多個連續(xù)流任務(wù)通過并發(fā)編程同時執(zhí)行,得到更快的執(zhí)行完成。對于運行在多核處理器上的現(xiàn)代軟件,并發(fā)編程是必要的,它有助于更好地利用多核處理器的功能,實現(xiàn)更快的并發(fā)/并行程序。
協(xié)程 (Goroutines)
協(xié)程實現(xiàn)了并發(fā)執(zhí)行,協(xié)程是Go運行時輕量級線程,協(xié)程和線程之間并無一對一的關(guān)系,協(xié)程由Go管理調(diào)度,運行在不同的線程上。Go協(xié)程的設(shè)計隱藏了許多線程創(chuàng)建和管理方面的復(fù)雜工作。
關(guān)于并發(fā)/并行程序,并發(fā)程序可能是并行的,也可能不是。并行是一種通過使用多處理器以提高速度的能力。一個設(shè)計良好的并發(fā)程序在并行方面的表現(xiàn)也非常出色。在Go語言中,為了使你的程序可以使用多核運行,這時協(xié)程就真正的是并行運行了,你必須使用GOMAXPROCS變量。詳細(xì)參考:https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/14.1.md
同步 (synchronize)
進(jìn)程、線程、協(xié)程協(xié)作都有一個共同的目標(biāo):同步和通訊。
Go語言中,Channels用于協(xié)程的同步。傳統(tǒng)線程模式通訊是共享內(nèi)存。Go鼓勵使用Channel在協(xié)程之間傳遞引用,而不是顯式地使用鎖來協(xié)調(diào)對共享數(shù)據(jù)的訪問。 這種方法確保在給定時間只有一個goroutine可以訪問數(shù)據(jù)。
如下面的例子所示,每個worker執(zhí)行完成后,他們需要與main協(xié)程協(xié)作,將返回結(jié)果通過channels傳遞給main協(xié)程,之后main協(xié)程退出程序。
同步出錯
請注意,每次使用go關(guān)鍵字時,Go例程將如何退出。有時候同步可能出現(xiàn)錯誤,導(dǎo)致一些goroutine永遠(yuǎn)等待。在Go語言中,如下情況可能導(dǎo)致同步出錯:
Channel沒有接受者
沒有一個接受者來接受發(fā)送者發(fā)送的數(shù)據(jù),Channel是阻塞的。沒有接受者的Channel會引起程序掛起。下面的例子,ch1沒有接受者,將導(dǎo)致Channel是阻塞的。
package main
import "fmt"
func?main() {
ch1 :=make(chanint)
go pump(ch1)// pump hangs
fmt.Println(<-ch1)// prints only 0
}
funcpump(chchanint) {
fori :=0; ; i++ {
ch <- i
}
}
Channel沒有寫入者
如下情況會出現(xiàn)channel沒有寫入者的情況,會出現(xiàn)goroutine泄漏。
例 1: for-select
for {
select {
case <-c:
// process here
}
}
例 2: channel循環(huán)
go func() {
for range ch { }
}()
例3: 演示tasks循環(huán),導(dǎo)致channel沒有寫入者,需要主程序調(diào)用close(tasks)來避免goroutine泄漏問題。
package main
import "fmt"
func concurrency() {
// lets first create a channel with a buffer
tasks := make(chan string, 20)
// create another one to receive the results
results := make(chan string, 20)
workers := []int{1, 2, 3, 4}
// inserting tasks inside the channel
for task := 0; task < 10; task++ {
tasks <- fmt.Sprintf("Task %d", task)
}
for _, w := range workers {
// starging one goroutine for each worker
go work(w, tasks, results)
}
close(tasks)
// lets print the resutls
fmt.Println("Will print the results")
for res := 0; res < 10; res++ {
fmt.Println("Result:", <-results)
}
}
func work(workerID int, tasks chan string, results chan string) {
// worker will block util a new task arrives in the channel
for t := range tasks {
// simple task as example
results <- fmt.Sprintf("Worker %d got %v", workerID, t)
}
}
func main() {
concurrency()
}
好的做法
使用timeOut
timeout := make(chan bool, 1)
go func() {
time.Sleep(1e9) // one second
timeout <- true
}()
select {
case <- ch:
// a read from ch has occurred
case <- timeout:
// the read from ch has timed out
}?????????? OR select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
使用Golang context package
Golang context package可以用來優(yōu)雅地結(jié)束例程甚至超時
泄漏檢測
儀器(instrumentation)端點
檢測Web服務(wù)器泄漏的辦法是添加儀器端點,并將其與負(fù)載測試一起使用。
// get the count of number of go routines in the system.
func countGoRoutines() int {
returnruntime.NumGoroutine()
}
func getGoroutinesCountHandler(w http.ResponseWriter, r *http.Request) {
// Get the count of number of go routines running.
count := countGoRoutines()
w.Write([]byte(strconv.Itoa(count)))
}
func main() {
http.HandleFunc("/_count", getGoroutinesCountHandler)
}
在負(fù)載測試之前和之后,通過儀器端點響應(yīng)在系統(tǒng)中存在的goroutines數(shù)量。以下是負(fù)載測試程序的流程:
Step 1: Call the instrumentation endpoint and get the count of number of goroutines alive in your webserver.
Step 2: Perform load test.Lets the load be concurrent.
for i := 0; i < 100 ; i++ {
go callEndpointUnderInvestigation()
}
Step 3: Call the instrumentation endpoint and get the count of number of goroutines alive in your webserver.
如果負(fù)載測試后系統(tǒng)中存在異常增加的goroutine數(shù)量,則證明存在泄漏。這是一個具有漏洞端點的Web服務(wù)器的小例子。 通過簡單的測試我們可以確定服務(wù)器是否存在泄漏。
// First run the leaky server $ go run leaky-server.go
// Run the load test now.$ go run load.go
3 Go routines before the load test in the system.
54 Go routines after the load test in the system.
您可以清楚地看到,通過50個并發(fā)請求到泄漏端點,系統(tǒng)中增加了50個程序。
讓我們再次運行負(fù)載測試。
$ go run load.go
53 Go routines before the load test in the system.
104 Go routines after the load test in the system.
很清楚,在每次運行的負(fù)載測試中,服務(wù)器中的執(zhí)行次數(shù)都在增加,而不是下降。 這是一個明顯的泄漏證據(jù)。
識別泄漏的起因
使用棧跟蹤端點
一旦發(fā)現(xiàn)Web服務(wù)器中存在泄漏,需要確定泄漏的來源。可以通過添加返回Web服務(wù)器的棧跟蹤端點可以幫助識別泄漏的來源。
import (
"runtime/debug"
"runtime/pprof"
)
func getStackTraceHandler(w http.ResponseWriter, r *http.Request) {
stack := debug.Stack()
w.Write(stack)
pprof.Lookup("goroutine").WriteTo(w, 2)
}
func main() {
http.HandleFunc("/_stack", getStackTraceHandler)
}
在確定泄漏的存在之后,使用端點在負(fù)載之前和之后獲取棧跟蹤信息,以識別泄漏的來源。
將棧跟蹤工具添加到泄漏服務(wù)器并再次執(zhí)行負(fù)載測試。
如下棧跟蹤信息清楚地指出泄漏的震中:
// First run the leaky server$ go run leaky-server.go
// Run the load test now.$ go run load.go
3 Go routines before the load test in the system.
54 Go routines after the load test in the system. goroutine 149 [chan send]:
main.sum(0xc420122e58, 0x3, 0x3, 0xc420112240)
/home/karthic/gophercon/count-instrument.go:39 +0x6c
created by main.sumConcurrent
/home/karthic/gophercon/count-instrument.go:51 +0x12b
goroutine 243 [chan send]:
main.sum(0xc42021a0d8, 0x3, 0x3, 0xc4202760c0)
/home/karthic/gophercon/count-instrument.go:39 +0x6c
created by main.sumConcurrent
/home/karthic/gophercon/count-instrument.go:51 +0x12b
goroutine 259 [chan send]:
main.sum(0xc4202700d8, 0x3, 0x3, 0xc42029c0c0)
/home/karthic/gophercon/count-instrument.go:39 +0x6c
created by main.sumConcurrent
/home/karthic/gophercon/count-instrument.go:51 +0x12b
goroutine 135 [chan send]:
main.sum(0xc420226348, 0x3, 0x3, 0xc4202363c0)
/home/karthic/gophercon/count-instrument.go:39 +0x6c
created by main.sumConcurrent
/home/karthic/gophercon/count-instrument.go:51 +0x12b
goroutine 166 [chan send]:
main.sum(0xc4202482b8, 0x3, 0x3, 0xc42006b8c0)
/home/karthic/gophercon/count-instrument.go:39 +0x6c
created by main.sumConcurrent
/home/karthic/gophercon/count-instrument.go:51 +0x12b
goroutine 199 [chan send]:
main.sum(0xc420260378, 0x3, 0x3, 0xc420256480)
/home/karthic/gophercon/count-instrument.go:39 +0x6c
created by main.sumConcurrent
/home/karthic/gophercon/count-instrument.go:51 +0x12b
........
使用profiling
由于泄漏的goroutine通常被阻止去嘗試讀取或?qū)懭隿hannel或甚至可能睡眠,profilling分析將幫助識別泄漏的起因。參見benchmarks and profiling談?wù)摶鶞?zhǔn)測試和分析,或https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/13.10.md。
避免泄漏,趕早不趕晚
單元測試和功能測試中使用instrument機(jī)制可以幫助早期識別泄漏。計數(shù)試驗前后的goroutine數(shù)。
func TestMyFunc() {
// get count of go routines. perform the test.
// get the count diff.
// alert if there's an unexpected rise.
}
測試中的棧差異
棧差異是一個簡單的程序,它在測試之前和之后對棧跟蹤進(jìn)行差異比較,并在任何不期望的goroutine遺留的系統(tǒng)情況下發(fā)出警報。 將將其與單元測試和功能測試集成,可以幫助在開發(fā)過程中識別泄漏。
import (
github.com/fortytw2/leaktest
)
func TestMyFunc(t *testing.T) {
defer leaktest.Check(t)()
go func() {
for {
time.Sleep(time.Second)
}
}()
}
安全設(shè)計
當(dāng)系統(tǒng)受到一個端點/服務(wù)受到泄漏或資源中斷影響的時候,微服務(wù)架構(gòu)的服務(wù)做為獨立容器/過程運行可以保護(hù)整個系統(tǒng)。推薦使用容器編排工具,如Kubernetes,Mesosphere和Docker Swarm。
Goroutine泄漏就像慢性自殺。設(shè)想獲取整個系統(tǒng)的棧跟蹤,并嘗試識別哪些服務(wù)導(dǎo)致數(shù)百個服務(wù)中的泄漏! 真的嚇人!!!! 他們在一段時間浪費你的計算資源,慢慢積累,你甚至不會注意到。 真的很重要去意識到泄漏并盡早調(diào)試它們!
Go will make you love programming again. I promise.
Go會讓你再次愛編程。 我承諾。
參考:
1.《The Way to Go》中文譯本《Go入門指南》https://github.com/Unknwon/the-way-to-go_ZH_CN
2. Debugging go routine leaks:https://youtu.be/hWo0FEVr92A