searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

记一次Go程序内存泄露及排查解决过程

2025-05-06 01:24:18
7
0

问题描述

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

image.png

image-1.png

从趋势图很容易发现应用程序内存使用量持续线性增长,随着运行时长的增加,必然会导致内存溢出程序崩溃。

问题排查

排查内存泄露问题首先需要获取内存堆栈,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生成图形化内存占用视图,如下截图:
image-2.png

重点分析标红部分,这是内存占比大的部分,可能存在内存泄露问题。

解决方案

通过分析内存快照中内存占用较大的代码区,发现如下代码存在资源未释放问题:

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()
    }
}
0条评论
0 / 1000
张军
4文章数
0粉丝数
张军
4 文章 | 0 粉丝
原创

记一次Go程序内存泄露及排查解决过程

2025-05-06 01:24:18
7
0

问题描述

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

image.png

image-1.png

从趋势图很容易发现应用程序内存使用量持续线性增长,随着运行时长的增加,必然会导致内存溢出程序崩溃。

问题排查

排查内存泄露问题首先需要获取内存堆栈,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生成图形化内存占用视图,如下截图:
image-2.png

重点分析标红部分,这是内存占比大的部分,可能存在内存泄露问题。

解决方案

通过分析内存快照中内存占用较大的代码区,发现如下代码存在资源未释放问题:

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()
    }
}
文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0