Goroutine 泄漏的调试

栏目: Go · 发布时间: 7年前

内容简介:Goroutine 泄漏的调试

在我们谈论协程(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协程退出程序。

  1. package main

  1. import (

  1. "fmt"

  1. "time"

  1. )

  1. func main () {

  1. ch := make ( chan string )

  1. go sendData(ch)

  1. go getData(ch)

  1. // 注释一

  1. time.Sleep( 1e9 )

  1. }

  1. // 往通道中塞进数据 func sendData (ch chan string ) {

  1. ch <- "A"

  1. ch <- "g"

  1. ch <- "f"

  1. ch <- "e"

  1. ch <- "d"

  1. ch <- "c"

  1. }

  1. // 提取通道中的数据 func getData (ch chan string ) {

  1. var input string

  1. //time.Sleep(1e9)

  1. for {

  1. input = <-ch

  1. fmt.Printf( "%s" , input)

  1. }

  2. }

同步出错

请注意,每次使用go关键字时,Go例程 如何退出。 有时候同步可能出现错误,导致一些goroutine永远等待。 在Go语言中,如下情况可能导致同步出错:

Channel没有接受者

没有一个接受者来接受发送者发送的数据, Channel 是阻塞的。 没有接受者的Channel会引起程序挂起。下面的例子,ch1没有接受者,将导致Channel是阻塞的。

  1. package main

  1. import "fmt"

  1. func main () {

  1. ch1 := make ( chan int )

  1. go pump(ch1)       // pump hangs

  1. fmt.Println(<-ch1) // prints only 0

  1. }

  1. func pump (ch chan int ) {

  1. for i := 0 ; ; i++ {

  1. ch <- i

  1. }

  1. }

Channel没有写入者

如下情况会出现channel没有写入者的情况,会出现goroutine泄漏。

例 1: for-select

  1. for {

  1. select { 

  1. case <-c: 

  1. // process here

  1. }

  1. }

例 2: channel循环

  1. go func() {

  2. for range ch { }

  1. }()

例3: 演示tasks循环,导致channel没有写入者,需要主程序调用close(tasks)来避免goroutine泄漏问题。

  1. import "fmt"

  2. package main

  1. func concurrency() {

  1. // lets first create a channel with a buffer

  1. tasks := make(chan string, 20)

  1. // create another one to receive the results

  1. results := make(chan string, 20)

  1. workers := []int{1, 2, 3, 4}

  1. // inserting tasks inside the channel

  1. for task := 0; task < 10; task++ {

  1. tasks <- fmt.Sprintf("Task %d", task)

  1. }

  1. for _, w := range workers {

  1. // starging one goroutine for each worker

  1. go work(w, tasks, results)

  1. }

  1. close(tasks)

  1. // lets print the resutls

  1. fmt.Println("Will print the results")

  1. for res := 0; res < 10; res++ {

  1. fmt.Println("Result:", <-results)

  1. }

  1. }

  1. func work(workerID int, tasks chan string, results chan string) {

  1. // worker will block util a new task arrives in the channel

  1. for t := range tasks {

  1. // simple task as example

  1. results <- fmt.Sprintf("Worker %d got %v", workerID, t)

  1. }

  1. }

  1. func main() {

  1. concurrency()

  1. }

好的做法

使用timeOut

  1. timeout := make(chan bool, 1)

  1. go func() {

  1. time.Sleep(1e9) // one second

  1. timeout <- true

  1. }()

  1. select {

  1. case <- ch:

  1. // a read from ch has occurred

  1. case <- timeout:

  1. // the read from ch has timed out

  1. }           OR select {

  1. case res := <-c1:

  1. fmt.Println(res)

  1. case <-time.After(time.Second * 1):

  1. fmt.Println("timeout 1")

  1. }

使用 Golang context package

Golang context package 可以 用来优雅地结束例程甚至超时

泄漏检测

仪器( instrument)端点

检测Web服务器泄漏的办法是添加仪器端点,并将其与负载测试一起使用。

  1. // get the count of number of go routines in the system.

  1. func countGoRoutines() int {

  1. return runtime.NumGoroutine()

  1. }      

  1. func getGoroutinesCountHandler(w http.ResponseWriter, r *http.Request) {

  1. // Get the count of number of go routines running.

  1. count := countGoRoutines()

  1. w.Write([]byte(strconv.Itoa(count)))

  1. }

  1. func main() {

  1. http.HandleFunc("/_count", getGoroutinesCountHandler)

  1. }

在负载测试之前和之后,通过 仪器端点响应 在系统中存在的goroutines数量。以下是负载测试程序的流程:

  1. Step 1: Call the instrumentation endpoint and get the count of number of goroutines alive in your webserver.

  1. Step 2: Perform load test.Lets the load be concurrent. 

  1. for i := 0; i < 100 ; i++ {

  1. go callEndpointUnderInvestigation()

  1. }

  1. Step 3: Call the instrumentation endpoint and get the count of number of goroutines alive in your webserver.

如果负载测试后系统中存在异常增加的goroutine数量,则证明存在泄漏。这是一个具有漏洞端点的Web服务器的小例子。 通过简单的测试我们可以确定服务器 是否存在 泄漏。

  1. // First run the leaky server $ go run leaky-server.go

  1. // Run the load test now.$ go run load.go

  1. 3 Go routines before the load test in the system.

  1. 54 Go routines after the load test in the system.

您可以清楚地看到,通过50个并发请求到泄漏端点,系统中增加了50个程序。

让我们再次运行负载测试。

  1. $ go run load.go

  1. 53 Go routines before the load test in the system.

  1. 104 Go routines after the load test in the system.

很清楚,在每次运行的负载测试中,服务器中的执行次数都在增加,而不是下降。 这是一个明显的泄漏证据。

识别泄漏的起因

使用栈跟踪端点

一旦发现Web服务器中存在泄漏,需要确定泄漏的来源。可以通过添加返回Web服务器的栈跟踪端点可以帮助识别泄漏的来源。

  1. import (

  1. "runtime/debug"

  1. "runtime/pprof"

  1. )

  1. func getStackTraceHandler(w http.ResponseWriter, r *http.Request) {       

  1. stack := debug.Stack()       

  1. w.Write(stack)       

  1. }

  1. func main() {

  1. http.HandleFunc("/_stack", getStackTraceHandler)

  1. }

在确定泄漏的存在之后,使用端点在负载之前和之后获取栈跟踪信息,以识别泄漏的来源。 将栈跟踪 工具 添加到泄漏服务器并再次执行负载测试。 

如下 栈跟踪信息清楚地指出泄漏的震中:

  1. // First run the leaky server$ go run leaky-server.go

  1. // Run the load test now.$ go run load.go

  1. 3 Go routines before the load test in the system.

  1. 54 Go routines after the load test in the system. goroutine 149 [chan send]:

  1. main.sum(0xc420122e58, 0x3, 0x3, 0xc420112240)

  1. /home/karthic/gophercon/count-instrument.go:39 +0x6c

  1. created by main.sumConcurrent

  1. /home/karthic/gophercon/count-instrument.go:51 +0x12b

  1. goroutine 243 [chan send]:

  1. main.sum(0xc42021a0d8, 0x3, 0x3, 0xc4202760c0)

  1. /home/karthic/gophercon/count-instrument.go:39 +0x6c

  1. created by main.sumConcurrent

  1. /home/karthic/gophercon/count-instrument.go:51 +0x12b

  1. goroutine 259 [chan send]:

  1. main.sum(0xc4202700d8, 0x3, 0x3, 0xc42029c0c0)

  1. /home/karthic/gophercon/count-instrument.go:39 +0x6c

  1. created by main.sumConcurrent

  1. /home/karthic/gophercon/count-instrument.go:51 +0x12b

  1. goroutine 135 [chan send]:

  1. main.sum(0xc420226348, 0x3, 0x3, 0xc4202363c0)

  1. /home/karthic/gophercon/count-instrument.go:39 +0x6c

  1. created by main.sumConcurrent

  1. /home/karthic/gophercon/count-instrument.go:51 +0x12b

  1. goroutine 166 [chan send]:

  1. main.sum(0xc4202482b8, 0x3, 0x3, 0xc42006b8c0)

  1. /home/karthic/gophercon/count-instrument.go:39 +0x6c

  1. created by main.sumConcurrent

  1. /home/karthic/gophercon/count-instrument.go:51 +0x12b

  1. goroutine 199 [chan send]:

  1. main.sum(0xc420260378, 0x3, 0x3, 0xc420256480)

  1. /home/karthic/gophercon/count-instrument.go:39 +0x6c

  1. created by main.sumConcurrent

  1. /home/karthic/gophercon/count-instrument.go:51 +0x12b

  1. ........

使用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数。

  1. func TestMyFunc() {

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

  1. // get the count diff. 

  1. // alert if there's an unexpected rise.

  1. }

测试中的栈差异

栈差异是一个简单的程序,它在测试之前和之后对栈跟踪进行 差异 比较,并在任何不期望的goroutine遗留的系统情况下发出警报。 将将其与单元测试和功能测试集成,可以帮助在开发过程中识别泄漏。

  1. import (

  1. github.com/fortytw2/leaktest

  1. )

  1. func TestMyFunc(t *testing.T) {

  1. defer leaktest.Check(t)()

  1. go func() {

  1. for {

  1. time.Sleep(time.Second)

  1. }

  1. }()

  1. }

安全设计

当系统受到一个端点/服务受到泄漏或资源中断影响的时候,微服务架构的服务做为 独立容器/过程运行可以保护整个系统。推荐使用容器编排工具,如Kubernetes,Mesosphere和Docker Swarm。

Goroutine泄漏就像慢性自杀。  设想获取整个系统的栈跟踪,并尝试识别哪些服务导致数百个服务中的泄漏! 真的吓人!!!! 他们在一段时间浪费你的计算资源,慢慢积累,你甚至不会注意到。 真的很重要去意识到泄漏并尽早调试它们!

Go会让你再次爱编程。 我承诺。 Go will make you love programming again. I promise.

参考:

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

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


以上所述就是小编给大家介绍的《Goroutine 泄漏的调试》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

产品经理的20堂必修课

产品经理的20堂必修课

徐建极 / 人民邮电出版社 / 2013-9-1 / 59.00元

《产品经理的20堂必修课》以作者八年的产品经理工作实践为基础,通过系统的理论结合丰富的实例的方法,全面地总结了作为一名互联网产品经理所应掌握的知识。 《产品经理的20堂必修课》分为三大部分。 讲产品:深入剖析互联网产品成功的要素,分别从需求导向、简单原则、产品运营、战略布局等维度,分析如何让产品在残酷的互联网竞争中脱颖而出。 讲方法:着重分析优秀的产品团队运作的工作方法和程序,详......一起来看看 《产品经理的20堂必修课》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具