ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] # 什么是goroutine leak goroutine leak,是go协程泄漏,什么是go协程泄漏,通俗来说,开启了一个goroutine,用完后,我们要正确让其结束。如果它没用了,还没结束,那就是goroutine leak。 泄漏的goroutine占用一部分cpu,还可能占着一些其他资源,从而影响主协程效率,有时甚至产生异常。 我们看下面的一个例子。 例子中我们的主协程需要通过某远程服务查询到一个结果。使用一个multiQuery的函数启动多个协程,分别向不同的服务器发起查询,只要收到一个服务器返回,multiQeury就返回结果 ~~~ package main import ( "fmt" "math/rand" "time" ) func queryFromSrc(src string) (ret string) { nanoSec := time.Now().Nanosecond() rand.Seed(int64(nanoSec)) sec := (rand.Int31() % 10) + 1 // time sleep simulates dns lookup and query time.Sleep(time.Second * time.Duration(sec)) ret = fmt.Sprintf("src=%s use sec=%d", src, sec) fmt.Println("a query ok, ret=", ret) return ret } func multiQuery() (ret string) { res := make(chan string, 3) go func() { res <- queryFromSrc("ns1.dnsserver.com") }() go func() { res <- queryFromSrc("ns2.dnsserver.com") }() go func() { res <- queryFromSrc("ns3.dnsserver.com") }() return <-res } func main() { fmt.Println("start multi query:") res := multiQuery() fmt.Println("res=", res) //time.Sleep(time.Second * 20) } ~~~ 本案例使用了一个带缓冲区的channel,multiQuery中的三个并行go func不分先后从远程获取一个结果返回。获取的结果写入channel res,在第一个结果收到后,multiQuery就返回。返回的结果肯定是三个go func中最快返回的。(go func 中的queryFromSrc使用time.Sleep(random)来模拟不同请求延时)。显然,当第一个结果返回后,multiQuery函数就结束了,而其他两个go func还在等待返回。 **如果我们使用不带缓冲区的channel,两个慢的goroutine将会卡在尝试去发送他们的结果到同一个channel,而这个channel将没有任何一个goroutine去读**。因为multiQeury已经执行结束。这种情况叫做goroutine leak。与gc回自动回收的变量不同,泄漏的goroutine不会自动被回收。 所以编程中一定要注意,不使用的goroutine要让其正确地终止 # GC 在runtime的doc中描述了,通过设置环境变量GODEBUG='gctrace=1'可以让go的运行时把gc信息打印到stderr。 ~~~ GODEBUG='gctrace=1' ./sentinel-agent >gc.log & ~~~ gc.log的输出如下: ~~~ gc781(1): 1+2385+17891+0 us, 60 -> 60 MB, 21971 (3503906-3481935) objects, 13818/14/7369 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields gc782(1): 1+1794+18570+1 us, 60 -> 60 MB, 21929 (3503906-3481977) objects, 13854/1/7315 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields gc783(1): 1+1295+20499+0 us, 59 -> 59 MB, 21772 (3503906-3482134) objects, 13854/1/7326 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields gc781:从程序启动开始,第781次gc ~~~ (1):参与gc的线程个数 1+2385+17891+0:分别是1)stop-the-world的时间,即暂停所有goroutine;2)清扫标记对象的时间;3)标记垃圾对象的时间;4)等待线程结束的耗时。单位都是us,4者之和就是gc暂停的整体耗时 60 -> 60 MB:gc后,堆上存活对象占用的内存,以及整个堆大小(包括垃圾对象) 21971 (3503906-3481935) objects:gc后,堆上的对象数量,gc前分配的对象以及本次释放的对象 13818/14/7369 sweeps:描述对象清扫阶段。一共有13818个memory span,其中14在后台被清扫,7369在stop-the-world期间被清扫 0(0) handoff,0(0) steal:描述并行标记阶段的负载均衡特性。当前在不同线程间传送操作数和总传送操作数,以及当前steal操作数和总steal操作数 0/0/0 yields:描述并行标记阶段的效率。在等待其他线程的过程中,一共有0次yields操做 经过观察gc的输出,发现当前堆上对象总数不断增多,没有减少的趋势,这说明存在对象的泄露,从而导致内存泄露 # memory profile 根据golang官网profile指南 http://blog.golang.org/profiling-go-programs ,在代码中添加 ~~~ import _ "net/http/pprof" func main() { go func() { http.ListenAndServe("localhost:6060", nil) }() } ~~~ 可以在运行时对程序进行profile,通过http访问: ~~~ go tool pprof http://localhost:6060/debug/pprof/heap ~~~ 进行memory profile,默认是`--inuse_space`,显示当前活跃的对象(不包括垃圾对象)占用的空间。使用`--alloc_space`可以显示所有分配的对象(包括垃圾对象)。不过这两种方式都没有发现异常 # 监控goroutine个数 通过runtime.NumGoroutine()可以获取当前的goroutine的个数。通过给程序添加http server获取一些统计信息来了解程序的运行状态,这是Jeff Dean推崇的方法。通过添加下述代码来实时查看goroutine的个数 ~~~ // goroutine stats and pprof go func() { http.HandleFunc("/goroutines", func(w http.ResponseWriter, r *http.Request) { num := strconv.FormatInt(int64(runtime.NumGoroutine()), 10) w.Write([]byte(num)) }); http.ListenAndServe("localhost:6060", nil) glog.Info("goroutine stats and pprof listen on 6060") }() ~~~ 通过命令: ~~~ curl localhost:6060/goroutines ~~~ 查询当前的goroutine的个数。通过不程序运行期间,不断查看,发现goroutine个数不断增加,没有销毁的迹象 # goroutine泄露 通过上面的观察,发现存在goroutine泄露,即goroutine没有正常退出。由于每轮(每隔10秒执行一次)都会创建多个goroutine,如果不能正常退出,则会存在大量的goroutine。go的gc使用的是mark and sweep,会从全局变量、goroutine的栈为根集合扫描所有的存活对象,如果goroutine不退出,就会泄露大量内存。 在确定是由于goroutine没有正常退出后,重新review代码,发现了泄露的根本原因。在重构前,在信号处理程序中,为了正常结束程序,对于每个goroutine都有一个channel,用于主goroutine等待所有goroutine正常结束后再退出。主goroutine中,信号处理程序用于等待所有goroutine的代码: ~~~ waiters = make([]chan int, Num) for _, w := range waiters { <- w } ~~~ 执行检查逻辑的goroutine在结束后,会调用`ag.w <- 1`,用于向主goroutine发送消息。 重构后,由于每轮都会创建goroutine,由于用于主goroutine和检查逻辑的goroutine之间的channel的大小是1,所以所有创建的检查goroutine都阻塞在`ag.w <- 1`上,不能正常退出。最后,把channel逻辑去掉,就不存在goroutine泄露了