部署的一个 Go 语言写的程序出现内存占用逐渐上升的情况,排查了一段时间终于找到了原因。

pprof

当出现内存占用不断上升的情况时,首先考虑是不是出现了内存泄露的问题。于是用 pprof 对程序进行持续的监控,从结果来看,goroutine 的数量一直在一个稳定的范围内,排除 goroutine 泄漏的情况。
接着讲目光转移到 inuse_space,然后看到了让我当时很疑惑的结果:
pprof 的结果与实际的内存占用结果出现了很大的差异 ,pprof 看到的内存占用只有个位数的 MB,而无论是 docker 占用的内存亦或是在容器里 top 看到的内存占用都达到了上百兆。

在一番 google 之下找到了原因:
pprof 看到的内存占用,其实只是 golang 逻辑上正在使用的内存量,不包括已被 GC 回收但尚未返还给操作系统的内存,同样也不包含内核态的内存占用,而 top 是站在操作系统层面看到的进程内存占用,理论上就是会比 pprof 看到的内存占用量更多。如果在工作中发现 top 看到占用的内存很大,而 pprof 看到的内存占用不多,有可能是两个问题导致的:

  • 有大量内存被 GC 但还没有来得及返还给操作系统
  • 某些内核态操作(比如 I/O )消耗了大量内存

[]byte 导致的问题?

我的程序在运行中会频繁的创建大量的 []byte,并且 []byte 的长度也不小。而用 pprof 发现这部分代码占用的内存确实较多,猜测是不是这部分占用的内存被 GC 后没有及时反还给操作系统。
stackoverflow 上有这样一个问题:无法释放由 bytes.Buffer 占用的内存。高赞回答:

Go 是一种垃圾回收语言,这意味着当这些变量变得不可访问时,垃圾回收器会自动释放变量分配和使用的内存。释放的内存并不意味着将其返回给操作系统,释放的内存意味着可以回收该内存,并在需要时将其重新用于另一个变量。因此在操作系统中,不会仅由于某些变量变得不可访问而垃圾回收器检测到此变量并释放它所使用的内存就立刻看到内存减少。

但是如果 Go 运行一段时间(通常为5分钟)不使用,它将把内存返回给 OS。如果在此期间内存使用量增加,则很有可能不会将内存返回给操作系统。如果等待一段时间而不重新分配内存,则释放的内存最终将返回给操作系统(不是所有内存,而是未使用的“大块”内存)。如果想迫不及待发生这种情况,可以调用 debug.FreeOSMemory() 强制执行。

这篇文章同样指出了类似的问题,但是通过运行作者的示例程序发现 HeapReleased 也即回收到操作系统的内存,并非像作者描述的不会经常变化,相反其变化的非常快,也就是 Go 在不断在操作系统中回收内存。
原因应该是这两篇文章都已经年代久远,Go 的更新迭代已经解决了上述问题。

go-python

之后我又将目光转向 go-python,程序中会不断通过 go-python 调用一个 python 脚本,而 go-python 是通过 cgo 调用 libpython 实现的。
问题是否出在这儿呢?首先我进行了验证,即注释掉了相关代码并且使 CGO_ENABLED=0,测试结果发现程序没有出现之前的内存泄漏情况,于是终于找到了问题所在。
在 go-python 项目中有一个 memory leak 的 issue 吸引了我的注意:

When to call GC?Do users need manual GC?

go-python is calling the CPython API through Cgo.
so you have to program as in C and follow the reference counting of the CPython API (calling python.Object.IncRef() and python.Object.DecRef().)

there’s no GC involved.
you need to manually manage memory.

问题在于使用者需要手动管理内存,PyObject 对象使用结束后需要主动调用 DecRef ,通过减少引用计数的方式释放,否则就会发生内存泄漏的情况。实测 DecRef 可能会引发某些问题,可以根据 go-python 文档的推荐改用 Clear

Reference

https://segmentfault.com/a/1190000016412013
https://segmentfault.com/a/1190000019929993
https://github.com/wolfogre/go-pprof-practice/issues/1
https://stackoverflow.com/questions/37382600/cannot-free-memory-once-occupied-by-bytes-buffer/37383604#37383604
https://stackoverflow.com/questions/45509538/freeing-unused-memory
https://blog.cloudflare.com/recycling-memory-buffers-in-go/