c++排查线程hang住_使用 pprof 排查 Golang 内存泄露

本文介绍了如何利用Golang自带的pprof工具排查和解决内存泄露及线程hang住的问题。在国庆期间,作者的Golang日志服务因内存未释放而频繁被Killed。通过pprof的Web页面和命令行工具分析goroutine和heap,发现一个goroutine长时间运行且创建了大量的channel和结构体导致内存泄露。经过代码审查,找到问题源头并进行了修复。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1eef49dafde6aa1ebba962e2a621dc2c.png

我使用 Golang 写的一个融合日志服务, 定时融合不同云厂商的日志, 该服务部署在 K8S 上.

在国庆期间日志量倍增, 该服务频繁被 Killed.

通过仪表盘, 发现该服务运行一次后, 内存不会释放, 怀疑是内存泄露导致的. 见下图.

0bd478dce28fa5103e5ec3e16d1c4008.png

最终, 我通过使用 pprof 解决了该问题.

1. 开启 pprof 服务

pprof 是 Golang 自带的性能分析工具. 可以 2 步 开启 pprof 服务.

// 1. 引入 net/http/pprof 包
_ import "net/http/pprof"

// 2. 开启 http 服务
go http.ListenAndServe(":9999", nil)

2. 通过 pprof 的 Web 页面分析 goroutine

执行完步骤 1 后, 程序启动后, 打开 [<http://127.0.0.1:9999/debug/pprof/>](<http://127.0.0.1:9999/debug/pprof/>) 以页面的形式获取程序当前的运行情况.

如图:

678445595643bf409f53a3b85b0863de.png
  • 使用 full goroutine stack dump 来排查是否有 goroutine 运行时间过长. 如下图, 如果有的 goroutine 运行的时间非常长, 就要查看对应的代码, 是否是代码 hang 住导致的.

d2299ac3ad12fc2d7caf8243e234c71c.png
  • 使用 goroutine 来排查是否创建了大量的 goroutine. 我的程序是定时任务, 正常情况下在 sleep 期间没有任务运行, goroutine 的数量应该非常少的. 可以使用这点进行排查. 如下图, 可以看到总的 goroutine 数量和每个 goroutine 实例的数量. 根据代码逻辑去推断 goroutine 实例的数量是否正常.

e509f761d3292b14b8a12d77649bafb8.png

我在 Web 页面上并未发现有价值的信息, 于是使用命令行分析.

3. 通过 pprof 的命令行分析 heap

命令行执行命令: go tool pprof -inuse_space [<http://127.0.0.1:9999/debug/pprof/heap>](<http://spark-master.x.upyun.com/debug/pprof/heap>)

这个命令的作用是, 抓取当前程序已使用的 heap. 抓取后, 就可以进行类似于 gdb 的交互操作.

  • top 命令, 默认能列出当前程序中内存占用排名前 10 的函数. 如图. 当时进行到这一步的时候, 我就非常惊讶, 因为 time.NewTimer 居然占据了 6 个多 G 的内存.

1e69b080b76c35d325b75f8a5abb5582.png
  • list <函数名>, 展现函数内部的内存占用. 使用 list time.NewTimer 查看了该函数的内部, 真相大白了, 原来每次调用 NewTimer 都会创建一个 channel, 还会生成一个结构体 runtimeTimer, 应该就是这两个地方内存没有释放造成的内存泄露.

f6b5da9f4a59388cc213a04cd425c470.png

修改 for ... select ... time.After 造成的内存泄露

原来程序中存在如下代码:

for {
		select {

		case a := <-chanA:
			...

		case b := <-chanB:
			....

		case <-time.After(20*time.Minutes):
			return nil, errors.New("download timeout")
	}

time.After 就是封装了一层的 NewTimer, time.After 的源码:

func After(d Duration) <-chan Time {
	return NewTimer(d).C
}

修复该错误, 只调用一次 NewTimer:

downloadTimeout := time.NewTimer(20 * time.Minute)
defer downloadTimeout.Stop()

for {
		select {

		case a := <-chanA:
			...

		case b := <-chanB:
			....

		case <-downloadTimeout.C:
			return nil, errors.New("download timeout")
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值