golang使用pprof排查协程泄露
golang使用pprof排查协程泄露
什么是协程泄露?
协程泄露指的是程序中启动的 goroutine 由于未能正常退出或被回收,持续占用资源,最终导致程序运行效率降低、内存占用增加,系统崩溃的一种问题。
导致协程泄露的场景?
- 死循环,没有退出机制,协程会一直存在一直到资源耗尽系统崩溃;
- chan使用不合理,生产者与消费者不匹配,导致协程阻塞,还有一点就是使用 range chan 当时chan没有合理关闭时;
- 未能正确使用context,超时处理,比如使用context.WithTimeout时,没有正确处理context.Canceled错误;
- 外部依赖,中间件阻塞;
- 复用的协程池销毁策略不完善,导致池内协程未正常退出。
在协程泄露场景下系统一般会有以下表现:
- 业务请求处理变慢,接口响应超时甚至无响应,这条可以从网关监控中查看平均响应时长被拉伸;
- 出现问题的服务连接数显著升高一直到连接数上限,导致无法处理新的请求。
- 网关日志出现大量502/504错误日志。
如何去排查协程泄露?
想要找到根因就要通过各个指标去着手排查,有哪些指标能够排查是关键,使用pprof排查的前提是打开了pprof。
除了pprof,还可以看系统日志,以及如数据库等重要中间件的运行情况,也可以从最近的改动代码中排查。
这里只记录一下如何通过pprof排查协程泄露。
开启pprof
- 标准库方式:
import _ "net/http/pprof"
go func() {
// 启动一个6060端口服务
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
- 自定义路由,这里可以指定pprof服务路由:
import "net/http/pprof"
mux := http.NewServeMux()
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
如果是标准库的方式是会将pprof的分析组件默认都开启,开启后主要是CPU Profiling对性能有5%-10%左右的损耗,这个组件是分析CPU使用情况,其余监听性能消耗比较轻微,所以可以有选择的开启部分pprof组件,如下是在gin框架注册pprof路由示例:
package main
import (
"github.com/gin-gonic/gin"
"net/http/pprof"
)
func main() {
r := gin.Default()
// 注册 pprof 路由
pprofGroup := r.Group("/debug/pprof")
{
pprofGroup.GET("/", gin.WrapF(pprof.Index))
pprofGroup.GET("/cmdline", gin.WrapF(pprof.Cmdline))
pprofGroup.GET("/profile", gin.WrapF(pprof.Profile))
pprofGroup.GET("/symbol", gin.WrapF(pprof.Symbol))
pprofGroup.GET("/trace", gin.WrapF(pprof.Trace))
pprofGroup.GET("/heap", gin.WrapF(pprof.Handler("heap").ServeHTTP))
pprofGroup.GET("/goroutine", gin.WrapF(pprof.Handler("goroutine").ServeHTTP))
pprofGroup.GET("/block", gin.WrapF(pprof.Handler("block").ServeHTTP))
pprofGroup.GET("/threadcreate", gin.WrapF(pprof.Handler("threadcreate").ServeHTTP))
}
// 启动服务器
r.Run(":8080")
}
还要注意安全问题,不要将pprof端口暴露到公网。
如何使用
可以通过 go tool 工具去分析运行状态,例如现在是排查goroutine运行状态那么执行:
go tool pprof http://localhost:6060/debug/pprof/goroutine
另外也可以将数据下载下来再用 tool 进入:
curl -o goroutine.log localhost:11014/debug/pprof/goroutine
go tool pprof goroutine.log
可以本地运行如下示例代码来试试:
package main
import (
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"runtime"
"time"
)
// 模拟一个会泄露的 goroutine
func leakyFunction() {
ch := make(chan int)
go func() {
// 这个 goroutine 永远不会退出
for {
select {
case <-ch:
return
default:
time.Sleep(time.Second)
}
}
}()
// 注意:这里没有关闭 channel,也没有发送数据
}
func main() {
// 启动 pprof 服务器
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// 打印初始 goroutine 数量
fmt.Printf("初始 goroutine 数量: %d\n", runtime.NumGoroutine())
// 模拟创建多个泄露的 goroutine
for i := 0; i < 10; i++ {
leakyFunction()
time.Sleep(time.Second)
fmt.Printf("当前 goroutine 数量: %d\n", runtime.NumGoroutine())
}
// 保持程序运行
select {}
}
进入后就可以输入命令去得到对应的分析数据:

这里也可以输入top命令去得到排名靠前的goroutine数据。

- flat 代表我这个函数自己占用的资源(这里就是 goroutine 数)
- sum 则代表我这个函数和调用我的函数占用的资源(这里就是 gorourine 数)
- cum 则代表我这个函数和我调用的函数占用的资源(这里就是 gouroutine 数)。
输入tarces可以打印出跟踪的堆栈数据:

这里可以看到示例代码中 leakyFunction 方法的调用堆栈,以及占用情况。
如上是 goroutine 分析的方式,我们是调用了:
http://localhost:6060/debug/pprof/goroutine
这里是restful的方式设计,goroutine换成其它名字后就是对应模块的功能,这里不赘述了,详细可以使用时候再看下。

浙公网安备 33010602011771号