第二章 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协程退出程序。

同步出错

请注意,每次使用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会让你再次爱编程。 我承诺。

参考:

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

3. https://github.com/fortytw2/leaktest

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 228,546评论 6 533
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,570评论 3 418
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 176,505评论 0 376
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,017评论 1 313
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,786评论 6 410
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,219评论 1 324
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,287评论 3 441
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,438评论 0 288
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,971评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,796评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,995评论 1 369
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,540评论 5 359
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,230评论 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,662评论 0 26
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,918评论 1 286
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,697评论 3 392
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,991评论 2 374

推荐阅读更多精彩内容

  • 01.{ 换行: Opening Brace Can't Be Placed on a Separate Lin...
    码农不器阅读 2,420评论 0 14
  • 当潮流爱新鲜 当旁人爱标签 幸得伴着你我 是窝心的自然
    寒夜龙吟阅读 169评论 0 0
  • 2017-7-26 --著:江涵小子 丁环小宝 今生少年缘梦吟 得遇碧佳思美人 且知儿于曾梦时 还有丁环...
    江涵少年阅读 184评论 0 2
  • 灯光闪烁 我心失落 一切还是没结果 有酒有歌 无你只我 我心仍旧很执着 当初你总要如何如何 如今看来全都是错 爱是...
    三碗再过岗阅读 209评论 2 2