PouchContainer Goroutine Leak 检测实践
0. 引言PouchContainer?是阿里巴巴集团开源的一款容器运行时产品,它具备强隔离和可移植性等特点,可用来帮助企业快速实现存量业务容器化,以及提高企业内部物理资源的利用率。 PouchContainer 同时还是一款 golang 项目。在此项目中,大量运用了 goroutine 来实现容器管理、镜像管理和日志管理等模块。goroutine 是 golang 在语言层面就支持的用户态 “线程”,这种原生支持并发的特性能够帮助开发者快速构建高并发的服务。 虽然 goroutine 容易完成并发或者并行的操作,但如果出现 channel 接收端长时间阻塞却无法唤醒的状态,那么将会出现?goroutine leak?。 goroutine leak 同内存泄漏一样可怕,这样的 goroutine 会不断地吞噬资源,导致系统运行变慢,甚至是崩溃。为了让系统能健康运转,需要开发者保证 goroutine 不会出现泄漏的情况。 接下来本文将从什么是 goroutine leak,如何检测以及常用的分析工具来介绍 PouchContainer 在 goroutine leak 方面的检测实践。 1. Goroutine Leak在 golang 的世界里,你能支配的土拨鼠有很多,它们既可以同时处理一大波同样的问题,也可以协作处理同一件事,只要你指挥得当,问题就能很快地处理完毕。没错,土拨鼠就是我们常说的? func main() { waitCh := make(chan struct{}) go func() { fmt.Println("Hi,Pouch. I‘m new gopher!") waitCh <- struct{}{} }() <-waitCh } 正常情况下,一只土拨鼠完成任务之后,它将会回笼,然后等待你的下一次召唤。但是也有可能出现这只土拨鼠很长时间没有回笼的情况。 func main() { // /exec?cmd=xx&args=yy runs the shell command in the host http.HandleFunc("/exec",func(w http.ResponseWriter,r *http.Request) { defer func() { log.Printf("finish %vn",r.URL) }() out,err := genCmd(r).CombinedOutput() if err != nil { w.WriteHeader(500) w.Write([]byte(err.Error())) return } w.Write(out) }) log.Fatal(http.ListenAndServe(":8080",nil)) } func genCmd(r *http.Request) (cmd *exec.Cmd) { var args []string if got := r.FormValue("args"); got != "" { args = strings.Split(got," ") } if c := r.FormValue("cmd"); len(args) == 0 { cmd = exec.Command(c) } else { cmd = exec.Command(c,args...) } return } 上面这段代码会启动 HTTP Server,它将允许客户端通过 HTTP 请求的方式来远程执行 shell 命令,比如可以使用? 但是有些时候,请求需要土拨鼠花很长的时间处理,而请求者却没有等待的耐心,比如? 这些流离在外、不受控制的土拨鼠,就是我们常说的?goroutine leak?。造成 goroutine leak 的原因有很多,比如 channel 没有发送者。运行下面的代码之后,你会发现 runtime 会稳定地显示目前共有 2 个 goroutine,其中一个是? func main() { logGoNum() // without sender and blocking.... var ch chan int go func(ch chan int) { <-ch }(ch) for range time.Tick(2 * time.Second) { logGoNum() } } func logGoNum() { log.Printf("goroutine number: %dn",runtime.NumGoroutine()) } 造成 goroutine leak 有很多种不同的场景,本文接下来会通过描述 Pouch Logs API 场景,介绍如何对 goroutine leak 进行检测并给出相应的解决方案。 2. Pouch Logs API 实践2.1 具体场景为了更好地说明问题,本文将 Pouch Logs HTTP Handler 的代码进行简化: func logsContainer(ctx context.Context,w http.ResponseWriter,r *http.Request) { ... writeLogStream(ctx,w,msgCh) return } func writeLogStream(ctx context.Context,msgCh <-chan Message) { for { select { case <-ctx.Done(): return case msg,ok := <-msgCh: if !ok { return } w.Write(msg.Byte()) } } } Logs API Handler 会启动 goroutine 去读取日志,并通过 channel 的方式将数据传递给?
2.2 如何检测 goroutine leak?对于 HTTP Server 而言,我们通常会通过引入包? # step 1: create background job pouch run -d busybox sh -c "while true; do sleep 1; done" # step 2: follow the log and stop it after 3 seconds curl -m 3 {ip}:{port}/v1.24/containers/{container_id}/logs?stdout=1&follow=1 # step 3: after 3 seconds,dump the stack info curl -s "{ip}:{port}/debug/pprof/goroutine?debug=2" | grep -A 10 logsContainer github.com/alibaba/pouch/apis/server.(*Server).logsContainer(0xc420330b80,0x251b3e0,0xc420d93240,0x251a1e0,0xc420432c40,0xc4203f7a00,0x3,0x3) /tmp/pouchbuild/src/github.com/alibaba/pouch/apis/server/container_bridge.go:339 +0x347 github.com/alibaba/pouch/apis/server.(*Server).(github.com/alibaba/pouch/apis/server.logsContainer)-fm(0x251b3e0,0x3) /tmp/pouchbuild/src/github.com/alibaba/pouch/apis/server/router.go:53 +0x5c github.com/alibaba/pouch/apis/server.withCancelHandler.func1(0x251b3e0,0xc42091dad0) /tmp/pouchbuild/src/github.com/alibaba/pouch/apis/server/router.go:114 +0x57 github.com/alibaba/pouch/apis/server.filter.func1(0x251a1e0,0xc4203f7a00) /tmp/pouchbuild/src/github.com/alibaba/pouch/apis/server/router.go:181 +0x327 net/http.HandlerFunc.ServeHTTP(0xc420a84090,0xc4203f7a00) /usr/local/go/src/net/http/server.go:1918 +0x44 github.com/alibaba/pouch/vendor/github.com/gorilla/mux.(*Router).ServeHTTP(0xc4209fad20,0xc4203f7a00) /tmp/pouchbuild/src/github.com/alibaba/pouch/vendor/github.com/gorilla/mux/mux.go:133 +0xed net/http.serverHandler.ServeHTTP(0xc420a18d00,0xc4203f7800) 我们会发现当前进程中还存留着? 2.3 怎么解决?golang 提供的包? // HTTP Handler Interceptors func withCancelHandler(h handler) handler { return func(ctx context.Context,rw http.ResponseWriter,req *http.Request) error { // https://golang.org/pkg/net/http/#CloseNotifier if notifier,ok := rw.(http.CloseNotifier); ok { var cancel context.CancelFunc ctx,cancel = context.WithCancel(ctx) waitCh := make(chan struct{}) defer close(waitCh) closeNotify := notifier.CloseNotify() go func() { select { case <-closeNotify: cancel() case <-waitCh: } }() } return h(ctx,rw,req) } } 当请求还没执行完毕时,客户端主动退出了,那么?
那么这样的检测可以做成自动化吗?下面会结合常用的分析工具来进行说明。 3. 常用的分析工具3.1 net/http/pprof在开发 HTTP Server 的时候,我们可以引入包? goroutine 93 [chan receive]: github.com/alibaba/pouch/daemon/mgr.NewContainerMonitor.func1(0xc4202ce618) /tmp/pouchbuild/src/github.com/alibaba/pouch/daemon/mgr/container_monitor.go:62 +0x45 created by github.com/alibaba/pouch/daemon/mgr.NewContainerMonitor /tmp/pouchbuild/src/github.com/alibaba/pouch/daemon/mgr/container_monitor.go:60 +0x8d goroutine 94 [chan receive]: github.com/alibaba/pouch/daemon/mgr.(*ContainerManager).execProcessGC(0xc42037e090) /tmp/pouchbuild/src/github.com/alibaba/pouch/daemon/mgr/container.go:2177 +0x1a5 created by github.com/alibaba/pouch/daemon/mgr.NewContainerManager /tmp/pouchbuild/src/github.com/alibaba/pouch/daemon/mgr/container.go:179 +0x50b goroutine stack 通常第一行包含着 Goroutine ID,接下来的几行是具体的调用栈信息。有了调用栈信息,我们就可以通过?关键字匹配?的方式来检索是否存在泄漏的情况了。 在 Pouch 的集成测试里,Pouch Logs API 对包含? 总的来说, 3.2 runtime.NumGoroutine当测试用例和目标函数/服务在同一个进程里时,可以通过 goroutine 的数目变化来判断是否存在泄漏问题。 func TestXXX(t *testing.T) { orgNum := runtime.NumGoroutine() defer func() { if got := runtime.NumGoroutine(); orgNum != got { t.Fatalf("xxx",orgNum,got) } }() ... } 3.3 github.com/google/gopsgops?与包? 4. 小结开发 HTTP Server 时, 5. 相关链接
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |