发现

  一般情况下,厂内都有对应的监控工具(本文不展开讨论监控),当节点出现:cpu 和 内存持续上升,且重启后表现相同,此时大概率发生内存泄漏;

排查 & 分析

  使用 pprof 采集相关资源信息(一般只用单台机器进行分析);

eg: 1、使用 Gin 的插件,github.com/gin-contrib/pprofpprof.Register(r),此处的 r 是指 gin 的实例,然后利用开发机(注意网络和权限 需要可访问服务节点),通过 go tool 获取内存和 cpu 的占用信息;

场景

长期持有引用

  1. 重复建立数据库连接,并且没有释放,即内存中有两份连接,但是只有一个 defer 的释放导致内存泄漏;

  2. db.Prepare 返回的 *sql.Stmt 也需要被显示关闭,因为它是一个预编译的 SQL 语句(可被多次执行,提供对应参数即可),是一种资源,不关闭同样导致内存泄漏;

    1. 执行 query 后获取的 rows 也是一种资源,即在底层它维护了一个数据库连接,同样需要 close,否则内存泄漏;(注:通常 close 指将连接放回连接池 而不是真的干掉这个连接)
      1. 之所以是一个连接的形式,而不是一个单纯的 value,因为查询获取的数据大小是不确定的,有可能写入内存(个人认为数据库和客户端的内存都可能有影响)会造成问题,所以数据库会将结果数据分块后逐块发送给客户端;

注:gc 不会回收这种 外部资源 (数据库连接句柄),还有比如 os.OpenFile 对应打开的文件句柄,所以必须手动释放,close 掉;(defer 通过指定函数的结束管理资源释放,RAII 通过对象的生命周期管理资源)

  1. 获取长字符串的子串导致长字符串无法释放(切片同理); 解决方案:
subStrCopy := string([]byte(subStr))
var builder strings.Builder
builder.WriteString(subStr)
subStrCopy := builder.String()
subStrCopy := (" " + str[:100])[1:]
  1. goroutine 未正常回收   如 :在 1.23.0 之前,经典 for + time.After() 在 select 时,每次执行 select 时都会重新初始化全新的计时器,但此时计时器触发前, GC 不会回收底层的计时器,导致内存泄漏;

需要注意 goroutine 的生命周期;

总结优化