goroutine泄漏指的是goroutine启动之后没有退出导致goroutine的数量持续上升,或者是在实际应用中goroutine占用了很长时间才退出导致在一段时间内goroutine的数量急剧上升。通常可以采用Go自带的pprof工具来定位问题,如下面这个示例:
这是一个简单的HTTP服务,当接收到请求时另起一个goroutine来输出日志,同时返回“Hello, World!\n”。在记录日志之前可能要处理一个耗时很长的业务逻辑,如通过公网请求第三方的API接口,这里为了简化问题用time.Sleep来示意。
goroutineleak.go
package main
import (
"io"
"log"
"net/http"
_ "net/http/pprof"
"time"
)
func writeLog(msg string) {
go func() {
time.Sleep(1 * time.Second)
log.Println(msg)
}()
}
func handler(w http.ResponseWriter, r *http.Request) {
writeLog(r.URL.RequestURI())
io.WriteString(w, "Hello, world!\n")
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
利用Grinder来压测,开8个进程,每个进程开3个线程,同时请求上述服务。在请求过程中,通过浏览器查看http://pub.pengpengzhou.com:8080/debug/pprof/goroutine?debug=1, 域名是上述服务所在的地址。
从pprof的返回结果中,我们可以看到协程总数是5605,下面列出了5组产生协程的代码stack trace,按产生的协程数量倒排序。可以看到第一组产生了5597个协程,产生位置在goroutineleak.go的13行“time.Sleep(1 * time.Second)”对应的函数是main.writeLog。问题很快得到定位,把这一行注释掉,重新再压测,可以得到如下结果, 协程总数一下就降到了8,泄漏的问题得到了解决。
当然,在实际应用中,耗时是不可避免的,通常是用设置超时的办法来规避长时间等待。
相关文章: