第二章 Goroutine泄漏的調試

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

在我們談論協程(Goroutines)泄漏之前,我們先看看並發編程的概念。並發編程處理常式的並發執行。多個連續流任務通過並發編程同時執行,得到更快的執行完成。對於運行在多核處理器上的現代軟體,並發編程是必要的,它有助於更好地利用多核處理器的功能,實現更快的並發/並行程式。

協程 (Goroutines)

協程實現了並發執行,協程是Go運行時輕量級線程,協程和線程之間並無一對一的關係,協程由Go管理調度,運行在不同的線程上。Go協程的設計隱藏了許多線程建立和管理方面的複雜工作。

關於並發/並行程式,並發程式可能是並行的,也可能不是。並行是一種通過使用多處理器以提高速度的能力。一個設計良好的並發程式在並行方面的表現也非常出色。在Go語言中,為了使你的程式可以使用多個核心運行,這時協程就真正的是並行運行了,你必須使用GOMAXPROCS變數。詳細參考:https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/14.1.md

同步 (synchronize)

進程、線程、協程協作都有一個共同的目標:同步和通訊。

Go語言中,Channels用於協程的同步。傳統線程模式通訊是共用記憶體。Go鼓勵使用Channel在協程之間傳遞引用,而不是顯式地使用鎖來協調對共用資料的訪問。 這種方法確保在給定時間只有一個goroutine可以訪問資料。

如下面的例子所示,每個worker執行完成後,他們需要與main協程協作,將返回結果通過channels傳遞給main協程,之後main協程退出程式。

同步出錯

請注意,每次使用go關鍵字時,Go常式將如何退出。有時候同步可能出現錯誤,導致一些goroutine永遠等待。在Go語言中,如下情況可能導致同步出錯:

Channel沒有接受者

沒有一個接受者來接受寄件者發送的資料,Channel是阻塞的。沒有接受者的Channel會引起程式掛起。下面的例子,ch1沒有接受者,將導致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沒有寫入者

如下情況會出現channel沒有寫入者的情況,會出現goroutine泄漏。

例 1: for-select

for {

select {

case <-c:

// process here

}

}

例 2: channel迴圈

go func() {

for range ch { }

}()

例3: 示範tasks迴圈,導致channel沒有寫入者,需要主程式調用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可以用來優雅地結束常式甚至逾時

泄漏檢測

儀器(instrumentation)端點

檢測Web伺服器泄漏的辦法是添加儀器端點,並將其與負載測試一起使用。

// 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)

}

在負載測試之前和之後,通過儀器端點響應在系統中存在的goroutines數量。以下是負載測試程式的流程:

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.

如果負載測試後系統中存在異常增加的goroutine數量,則證明存在泄漏。這是一個具有漏洞端點的Web伺服器的小例子。 通過簡單的測試我們可以確定伺服器是否存在泄漏。

// 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個並發請求到泄漏端點,系統中增加了50個程式。

讓我們再次運行負載測試。

$ go run load.go

53 Go routines before the load test in the system.

104 Go routines after the load test in the system.

很清楚,在每次啟動並執行負載測試中,伺服器中的執行次數都在增加,而不是下降。 這是一個明顯的泄漏證據。

識別泄漏的起因

使用棧跟蹤端點

一旦發現Web伺服器中存在泄漏,需要確定泄漏的來源。可以通過添加返回Web伺服器的棧跟蹤端點可以協助識別泄漏的來源。

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)

}

在確定泄漏的存在之後,使用端點在負載之前和之後擷取棧跟蹤資訊,以識別泄漏的來源。

將棧跟蹤工具添加到泄漏伺服器並再次執行負載測試。

如下棧跟蹤資訊清楚地指出泄漏的震中:

// 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通常被阻止去嘗試讀取或寫入channel或甚至可能睡眠,profilling分析將協助識別泄漏的起因。參見benchmarks and profiling談論基準測試和分析,或https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/13.10.md。

避免泄漏,趕早不趕晚

單元測試和功能測試中使用instrument機制可以協助早期識別泄漏。計數實驗前後的goroutine數。

func TestMyFunc() {

// get count of go routines. perform the test.

// get the count diff.

// alert if there's an unexpected rise.

}

測試中的棧差異

棧差異是一個簡單的程式,它在測試之前和之後對棧跟蹤進行差異比較,並在任何不期望的goroutine遺留的系統情況下發出警報。 將將其與單元測試和功能測試整合,可以協助在開發過程中識別泄漏。

import (

github.com/fortytw2/leaktest

)

func TestMyFunc(t *testing.T) {

defer leaktest.Check(t)()

go func() {

for {

time.Sleep(time.Second)

}

}()

}

安全設計

當系統受到一個端點/服務受到泄漏或資源中斷影響的時候,微服務架構的服務做為獨立容器/過程運行可以保護整個系統。推薦使用容器編排工具,如Kubernetes,Mesosphere和Docker Swarm。

Goroutine泄漏就像慢性自殺。設想擷取整個系統的棧跟蹤,並嘗試識別哪些服務導致數百個服務中的泄漏! 真的嚇人!!!! 他們在一段時間浪費你的計算資源,慢慢積累,你甚至不會注意到。 真的很重要去意識到泄漏並儘早調試它們!

Go will make you love programming again. I promise.

Go會讓你再次愛編程。 我承諾。

參考:

《The Way to Go》中文譯本《Go入門指南》https://github.com/Unknwon/the-way-to-go_ZH_CN

Debugging go routine leaks:https://youtu.be/hWo0FEVr92A

https://github.com/fortytw2/leaktest

http://www.tuicool.com/articles/2AZf63J

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.