goroutine leak
什么是泄露?
额……,这个解释对程序员来说好像不太贴近生活,那我们换一个
内存泄露(Memory Leak)
1 2 3 4 5 6 7 8
| 对于java程序员来说,这个词汇肯定不陌生. 内存泄露本意是申请的内存空间没有被正确释放, 导致后续程序里这块内存被永远占用(不可达), 而且指向这块内存空间的指针不再存在时, 这块内存也就永远不可达了,内存空间就这么一点点被蚕食. 借用别人的比喻就是: 比如有10张纸,本来一人一张,画完自己擦了还回去,别人可以继续画,现在有个坏蛋要了纸不擦不还,然后还跑了找不到人了,如此就只剩下9张纸给别人用了,这样的人多起来后,最后大家一张纸都没有了;
|
go协程泄露(goroutine leak)
1 2
| 在Go中,goroutine很轻量级,随便创建成千上万个goroutine不是问题, 但要注意,要是这么多的goroutine一致递增,而不退出,不释放资源,也会造成资源耗尽、服务不可达的情况;
|
如何发现
1 2
| runtime.NumGoroutine()
|
我们可以把这个数据封装成一个服务,通过查看每次的协程数量的变化和增减,我们可以判断是否有goroutine泄露发生
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package main
import ( "fmt" "log" "net/http" "runtime" "strconv" )
func getGCntHandler(w http.ResponseWriter, r *http.Request) { cnt := runtime.NumGoroutine() fmt.Fprintf(w, strconv.Itoa(cnt)) } func main() { http.HandleFunc("/_gc", getGCntHandler) err := http.ListenAndServe(":8080", nil) if err != nil { log.Fatal("ListenAndServe: ", err) } }
|
如何确认泄露处
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| package main
import ( "fmt" "log" "net/http" "runtime" "runtime/debug" "runtime/pprof" "strconv" )
func getStackTraceHandler(w http.ResponseWriter, r *http.Request) { stack := debug.Stack() w.Write(stack) pprof.Lookup("goroutine").WriteTo(w, 2) }
func getGCntHandler(w http.ResponseWriter, r *http.Request) { cnt := runtime.NumGoroutine() fmt.Fprintf(w, strconv.Itoa(cnt)) } func main() { http.HandleFunc("/_stack", getStackTraceHandler) http.HandleFunc("/_gc", getGCntHandler) err := http.ListenAndServe(":8080", nil) if err != nil { log.Fatal("ListenAndServe: ", err) } }
|
常见的产生原因
- 由于channel的读/写端退出,导致channel的写/读端goroutine一直阻塞,而无法退出
- goroutine内部进入死循环,导致资源一直无法释放
解决方案
基于以上原因,开发时需要重点注意以下几点:
- 创建goroutine时就要想好该goroutine该如何结束.
比如:
- goroutine完成它的工作,正常return
- 由于发生了没有处理的错误,运行时异常
- 有其他的协程告诉它终止
- 使用channel时,要考虑到channel阻塞时协程可能的行为
- 在做 master-worker模式、producer-consumer模式等的开发时