前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >Goroutine泄露的危害、成因、检测与防治

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

作者头像
fliter
发布2023-06-18 11:10:28
发布2023-06-18 11:10:28
1.1K00
代码可运行
举报
文章被收录于专栏:旅途散记旅途散记
运行总次数:0
代码可运行

Goroutine泄露的危害

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

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

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

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

massiveGoroutine.go:

代码语言:javascript
代码运行次数:0
复制
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()
}
代码语言:javascript
代码运行次数:0
复制

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

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

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

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

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

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

channel使用不当造成的泄露:

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

代码语言:javascript
代码运行次数:0
复制
// 只写不读
package main

import (
  "fmt"
  "log"
  "net/http"
  "runtime"
  "strconv"
  "time"
)

// 把数组s中的数字加起来
func sumInt(s []int, c chan int) {
  sum := 0
  for _, v := range s {
    sum += v
  }
  c <- sum
}

// HTTP handler for /sum
func sumConcurrent(w http.ResponseWriter, r *http.Request) {
  x := deal()
  // write the response.
  fmt.Fprintf(w, strconv.Itoa(x))
}

func deal() int {
  s := []int{7, 2, 8, -9, 4, 0}

  c1 := make(chan int)
  c2 := make(chan int)

  go sumInt(s[:len(s)/2], c1) // 即s[0:3],即7,2,8  [a:b]均为左开右闭
  go sumInt(s[len(s)/2:], c2) // 即s[3:6],即-9,4,0

  // 这里故意不在c2中读取数据,导致向c2写数据的协程阻塞。
  x := <-c1

  fmt.Println("x is:", x)
  return x
}

func main() {

  StasticGroutine := func() {
    for {
      time.Sleep(1e9)
      total := runtime.NumGoroutine()
      fmt.Println("当前协程数:", total)
    }
  }

  go StasticGroutine()

  http.HandleFunc("/sum", sumConcurrent)
  err := http.ListenAndServe(":8001", nil)
  if err != nil {
    log.Fatal("ListenAndServe: ", err)
  }
}
代码语言:javascript
代码运行次数:0
复制

使用goleak检测,

leak_test.go:

代码语言:javascript
代码运行次数:0
复制
package main

import (
  "go.uber.org/goleak"
  "testing"
)

func TestLeak(t *testing.T) {
  defer goleak.VerifyNone(t)
  deal()
}

每次都会新建两个协程去处理 但对其中一个无缓冲的channel c2只写不读,在这里发生了阻塞,如报错提示:

Goroutine 21 in state chan send,这个协程一直在通道发送状态(因为没有读取,所以一直阻塞着)

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

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

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

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

代码语言:javascript
代码运行次数:0
复制
package main

import (
  "fmt"
  "runtime"
  "time"
)

func fibonacci(c chan int) {

  fmt.Println("进入协程,开始计算")
  x, y := 0, 1
  for {
    select {
    case c <- x:
      x, y = y, x+y
    }
  }
}

func deal4() {
  c := make(chan int)

  go fibonacci(c)

  for i := 0; i < 10; i++ {
    fmt.Println(<-c)
  }

  // 执行10次后,就不再从channel中读取数据,fibonacci()里select唯一一个case不可运行,这个select被阻塞,从而deal4方法执行结束这个协程也得不到释放

}

func main() {

  fmt.Println("开始时goroutine的数量:", runtime.NumGoroutine())

  deal4()

  time.Sleep(3e9)
  fmt.Println("结束时goroutine的数量:", runtime.NumGoroutine())
}
代码语言:javascript
代码运行次数:0
复制

解决方案:

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

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

代码语言:javascript
代码运行次数:0
复制
package main

import (
  "fmt"
  "runtime"
  "time"
)

func fibonacci(c, quit chan int) {

  fmt.Println("进入协程,开始计算")
  x, y := 0, 1
  for {
    select {
    case c <- x:
      x, y = y, x+y

    case <-quit:
      fmt.Printf("收到退出的信号,信号值为(%d)\n", <-quit)
      return
    }
  }
}

func deal4() {
  c := make(chan int)
  quit := make(chan int)

  go fibonacci(c, quit)

  for i := 0; i < 10; i++ {
    fmt.Println(<-c)
  }

  // 执行10次后,就不再从channel中读取数据,fibonacci()里select唯一一个case不可运行,这个select被阻塞,从而deal4方法执行结束这个协程也得不到释放;

  // 如果close掉一个无缓冲的channel,可从中读到 对应channel类型的零值,从而满足了第二个case的条件,进而return

  fmt.Println("未close时goroutine的数量:", runtime.NumGoroutine())
  close(quit)
  time.Sleep(1e9)
  fmt.Println("close后goroutine的数量:", runtime.NumGoroutine())

}

func main() {

  fmt.Println("开始时goroutine的数量:", runtime.NumGoroutine())

  deal4()

  time.Sleep(3e9)
  fmt.Println("结束时goroutine的数量:", runtime.NumGoroutine())
}

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

代码语言:javascript
代码运行次数:0
复制
package main

import (
  "context"
  "fmt"
  "runtime"
  "time"
)

func fibonacci(c chan int, ctx context.Context) {

  fmt.Println("进入协程,开始计算")
  x, y := 0, 1
  for {
    select {
    case c <- x:
      x, y = y, x+y

    case <-ctx.Done():
      fmt.Printf("收到取消的信号,cancel!,信号值为(%#v)\n", <-ctx.Done())
      return
    }
  }
}

func deal4() {
  ctx, cancel := context.WithCancel(context.Background())
  defer cancel()

  c := make(chan int)

  go fibonacci(c, ctx)

  for i := 0; i < 10; i++ {
    fmt.Println(<-c)
  }

  // 执行10次后,就不再从channel中读取数据,fibonacci()里select唯一一个case不可运行,这个select被阻塞,从而deal4方法执行结束这个协程也得不到释放

  // 执行cancel后,见满足第二个case,进而return

  fmt.Println("未close时goroutine的数量:", runtime.NumGoroutine())
  cancel()
  time.Sleep(1e9)
  fmt.Println("close后goroutine的数量:", runtime.NumGoroutine())

}

func main() {

  fmt.Println("开始时goroutine的数量:", runtime.NumGoroutine())

  deal4()

  time.Sleep(3e9)
  fmt.Println("结束时goroutine的数量:", runtime.NumGoroutine())
}
代码语言:javascript
代码运行次数:0
复制

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

---

https://blog.csdn.net/cbmljs/article/details/83005749

https://blog.csdn.net/zg_hover/article/details/81097410

https://mp.weixin.qq.com/s/9oVns6gTO92rsNIhkeCQDg

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-08-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 旅途散记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 造成goroutine泄露的原因 && 检测goroutine泄露的工具
    • channel使用不当造成的泄露:
    • select操作在所有case上都阻塞造成的泄露
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档