问题描述
Go语言应用程序性能及稳定性测试过程中,在观察、统计应用程序资源占用时发现内存使用量呈现持续线性增长趋势,由此断定应用程序可能存在内存泄露情况,如果不处理,最终结果就是内存溢出,应用程序崩溃。
- 统计应用程序资源(CPU、内存)占用脚本如下
#!/bin/sh
PID=‘Application Process ID’
pidstat -r -u -p $PID 5 > ./cpumem.log &
- 从cpumem.log统计内存利用率、内存占用量,结果分别保存为mem_perc.csv、mem_rss.csv,awk脚本如下:
awk -F' +' '
/%CPU/ { found_cpu = 1; next }
/VSZ/ { found_cpu = 0; next }
!found_cpu && /^[0-9]{2}:[0-9]{2}:[0-9]{2} [AP]M/ { print $9 }
' cpumem.log > mem_perc.csv
awk -F' +' '
/%CPU/ { found_cpu = 1; next }
/VSZ/ { found_cpu = 0; next }
!found_cpu && /^[0-9]{2}:[0-9]{2}:[0-9]{2} [AP]M/ { print $8 }
' cpumem.log > mem_rss.csv
- Excel图表分析mem_perc.csv、mem_rss.csv
从趋势图很容易发现应用程序内存使用量持续线性增长,随着运行时长的增加,必然会导致内存溢出程序崩溃。
问题排查
排查内存泄露问题首先需要获取内存堆栈,Go语言中常用性能分析工具是pprof,下面介绍如何使用pprof来分析内存堆栈。
启用pprof
确保应用程序导入 net/http/pprof 包,并在main函数中启动 HTTP 服务器。
import (
_ "net/http/pprof"
"net/http"
"log"
)
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
}
采集内存快照
- 实时分析
如果应用程序服务器已安装go tool可以使用该方法,直接在浏览器中查看内存分配热点。
go tool pprof -http=:8080 localhost:6060/debug/pprof/heap
- 离线分析
如果应用程序服务器服务器未安装go tool,可以先产生离线内存快照文件,将内存快照文件传输到本地开发环境进行分析
# 采集内存快照到文件
curl localhost:6060/debug/pprof/heap > heap.pprof
将内存快照文件传输到本地开发环境,在终端中运行如下命令:
go tool pprof heap.pprof
进入 pprof 的交互式命令行界面后,可以使用如下命令进行分析:
(pprof) top # 显示内存使用最多的函数
(pprof) list main # 显示 main 函数的内存分配情况
(pprof) web # 生成图形化视图
(pprof) quit # 退出 pprof
通过pprof的子命令web生成图形化内存占用视图,如下截图:
重点分析标红部分,这是内存占比大的部分,可能存在内存泄露问题。
解决方案
通过分析内存快照中内存占用较大的代码区,发现如下代码存在资源未释放问题:
for {
select {
case req := <-taskChan:
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
responses, err1 := sc.SubscrGetSend(ctx, &request_0)
...
}
}
上述代码段中defer cancel()
只会在方法结束返回或超时的情况下才执行,这段代码业务处理逻辑是一直循环获取taskChan
数据并执行没有方法结束返回的情况,而且在网络正常的情况下绝大多少请求不会超时,所以defer cancel()
基本上不会执行,这导致ctx
占用的资源一直不释放,随着处理taskChan
数据越来越多,内存也一直持续增长。 一旦定位问题发生原因,解决方法就显而易见,在业务逻辑处理完之后主动调用cancel()
进行资源及时释放,根据context.Context
文档所述多次重复调用cancel()
并不会产生问题,只在第一次调用会释放资源,后续调用没有任何效果,不会引起错误或异常。
代码修改如下:
for {
select {
case req := <-taskChan:
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
responses, err1 := sc.SubscrGetSend(ctx, &request_0)
...
// avoid memory leak, mandatory cancel the context
cancel()
}
}