golang使用pprof排查协程泄露

golang使用pprof排查协程泄露

什么是协程泄露?

协程泄露指的是程序中启动的 goroutine 由于未能正常退出或被回收,持续占用资源,最终导致程序运行效率降低、内存占用增加,系统崩溃的一种问题。

导致协程泄露的场景?

  1. 死循环,没有退出机制,协程会一直存在一直到资源耗尽系统崩溃;
  2. chan使用不合理,生产者与消费者不匹配,导致协程阻塞,还有一点就是使用 range chan 当时chan没有合理关闭时;
  3. 未能正确使用context,超时处理,比如使用context.WithTimeout时,没有正确处理context.Canceled错误;
  4. 外部依赖,中间件阻塞;
  5. 复用的协程池销毁策略不完善,导致池内协程未正常退出。

在协程泄露场景下系统一般会有以下表现:

  1. 业务请求处理变慢,接口响应超时甚至无响应,这条可以从网关监控中查看平均响应时长被拉伸;
  2. 出现问题的服务连接数显著升高一直到连接数上限,导致无法处理新的请求。
  3. 网关日志出现大量502/504错误日志。

如何去排查协程泄露?

想要找到根因就要通过各个指标去着手排查,有哪些指标能够排查是关键,使用pprof排查的前提是打开了pprof。
除了pprof,还可以看系统日志,以及如数据库等重要中间件的运行情况,也可以从最近的改动代码中排查。
这里只记录一下如何通过pprof排查协程泄露。

开启pprof

  1. 标准库方式:
import _ "net/http/pprof"

go func() {
    // 启动一个6060端口服务
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()
  1. 自定义路由,这里可以指定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换成其它名字后就是对应模块的功能,这里不赘述了,详细可以使用时候再看下。

参考

https://www.cnblogs.com/yjf512/p/18120513

https://segmentfault.com/a/1190000041261187#item-3

posted @ 2025-05-26 18:00  Fang20  阅读(193)  评论(0)    收藏  举报