关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

Goroutine泄露的危害、成因、检测与防治

发布时间:2023-06-26 19:00:29

goroutine泄露的危害


Go内存泄露,相当多数都是goroutine泄露导致的。 虽然每个goroutine仅占用少量(栈)内存,但当大量goroutine被创建却不会释放时(即发生了goroutine泄露),也会消耗大量内存,造成内存泄露。

另外,如果goroutine里还有在堆上申请空间的操作,则这部分堆内存也不能被垃圾回收器回收

坊间有说法,Go 10次内存泄漏,8次goroutine泄漏,1次是真正内存泄漏,还有1次是cgo导致的内存泄漏 (“才高八斗”的既视感..)

关于单个Goroutine占用内存,可参考Golang计算单个Goroutine占用内存, 在不发生栈扩张情况下, 新版本Go大概单个goroutine 占用2.6k左右的内存

massiveGoroutine.go:

package main  import (  "net/http"  "runtime/pprof" )  var quit chan struct{} = make(chan struct{})  func f() {   // 从无缓冲的channel中读取数据,如果没有写入,会一直阻塞  <-quit  }  func getGoroutineNum(w http.ResponseWriter, r *http.Request) {  w.Header().Set("Content-Type", "text/plain")   p := pprof.Lookup("goroutine")  p.WriteTo(w, 1) }  func deal0() {   // 创建100w协程; 协程中 从一个无缓冲的channel中读取数据,因为没有写入,会一直阻塞,goroutine得不到释放  for i := 0; i < 100_0000; i++ {  go f()  }   http.HandleFunc("/", getGoroutineNum)  http.ListenAndServe(":11181", nil) }  func main() {  deal0() }

   

参考 golang使用pprof检查goroutine泄露




造成goroutine泄露的原因 && 检测goroutine泄露的工具


原因:


goroutine泄露:原理、场景、检测和防范 比较全面总结了造成goroutine泄露的几个原因:

    1. 从 channel 里读,但是同时没有写入操作
    2. 向 无缓冲 channel 里写,但是同时没有读操作
    3. 向已满的 有缓冲 channel 里写,但是同时没有读操作
    4. select操作在所有case上都阻塞()
    5. goroutine进入死循环,一直结束不了

可见,很多都是因为channel使用不当造成阻塞,从而导致goroutine也一直阻塞无法退出导致的。


检测:


可以使用pprof做分析,但大多数情况都是发生在事后,无法在开发阶段就把问题提早暴露(即“测试左移”)

而uber出品的goleak可以 集成到单元测试中,能快速检测 goroutine 泄露,达到避免和排查的目的


channel使用不当造成的泄露:


例如以下代码 (2.向 无缓冲 channel 里写,但是同时没有读操作)

因涉及一些执行语句,禁止写入,请联系客服获取

使用goleak检测,

leak_test.go:

因涉及一些执行语句,禁止写入,请联系客服获取

执行:

最初的协程数1 3 random ints: 1: 5577006791947779410 2: 8674665223082153551 3: 6129484611666145821 当前协程数2 10s后的协程数2

   

leak_test.go:

因涉及一些执行语句,禁止写入,请联系客服获取

解决方案:

因涉及一些执行语句,禁止写入,请联系客服获取

输出:

最初的协程数1 3 random ints: 1: 5577006791947779410 2: 8674665223082153551 3: 6129484611666145821 当前协程数2 newRandStream closure exited. 最后的协程数1

   

详细代码及解决方案,参考 Go并发编程--goroutine leak的产生和解决之道

goroutine leak 往往是由于协程在channel上发生阻塞,或协程进入死循环,在使用channel和goroutine时要注意:

  • 创建goroutine时就要想好该goroutine该如何结束
  • 使用channel时,要考虑到channel阻塞时协程可能的行为
  • 要注意平时一些常见的goroutine leak的场景,包括:master-worker模式,producer-consumer模式等等。


另外几种(1. 从 channel 里读,但是同时没有写入操作; 3. 向已满的 有缓冲 channel 里写,但是同时没有读操作)使用channel不当造成阻塞的情况与之类似


select操作在所有case上都阻塞造成的泄露


其实本质上还是channel问题, 因为 select..case只能处理 channel类型, 即每个 case 必须是一个通信操作, 要么是发送要么是接收

select 将随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。*

Golang中select的四大用法

4. select操作在所有case上都阻塞 的情况:

因涉及一些执行语句,禁止写入,请联系客服获取

解决方案:

有个独立 goroutine去做某些操作的场景下,为了能在外部结束它,通常有两种方法:

a. 同时传入一个用于控制goroutine退出的 quit channel,配合 select,当需要退出时close 这个 quit channel,该 goroutine 就可以退出

因涉及一些执行语句,禁止写入,请联系客服获取

b. 使用 context 包的WithCancel,可参考 context.WithCancel()的使用

time.After和select搭配使用时存在的坑

因涉及一些执行语句,禁止写入,请联系客服获取

关于goleak的更具体使用及简单源码分析,可参考 远离P0线上事故,一个可以事前检测 Go 泄漏的工具


/template/Home/leiyu/PC/Static