协程-泄露

什么是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泄露了

猜你喜欢

转载自blog.csdn.net/ma2595162349/article/details/113004144