程序在运转一段时间后,内存因泄露而持续增长,或者因碎片化占用导致分配内存不足,最后被系统 Kill 出现 OOM 报错的问题十分常见,无论是什么语言编写的代码,只要业务流量足够大,或者用户输入足够复杂,就比较容易出现此类问题。

内存管理一般会涉及到三层:用户程序层、C 运行时库层和内核层。

如果是因为用户程序层导致的内存使用不当,这类问题是比较好排查的,一般可以在 OOM 之前,通过 heap profiling 将大内存块给分析出来,例如 Node.js 可以使用 llnode/andb 等工具进行排查,Python 可以使用 Heapy/objgraph 等工具排查。

但如果内存问题出现在 C 运行时库层(glibc),定位起来就较为麻烦了。glibc 是介于用户程序层和内核层之间的一个内存管理器,用户程序一般不会直接向内核申请内存,因为两个 context 之间的切换开销比较大,而是通过 glibc 预先从内核申请一大块内存,然后用户程序再向 glibc 申请资源,只有资源不足的时候,glibc 才会再次向内核继续申请资源。

这也意味着 glibc 需要完成复杂的内存分配和回收工作,好在业界已经有非常成熟的实现,如 ptmalloc/tcmalloc/jemalloc 等等,其中 ptmalloc 是 glibc 默认内存管理器,它是一个标准实现,tcmalloc 是 Google 家提供的,jemalloc 是 Facebook 家提供的,后两者在多线程/进程情况下,以及小内存的分配效率上,都有着明显的优势。

如果内存问题出现在 glibc 上,使用 gdb 一般都可以找到问题,但存在一个缺陷是,gdb 可以定位到 glibc 上具体的内存开销问题,但无法与上层(如 Node.js/Python)业务代码进行关联。好在不同的语言都有自己的配套工具,例如 Node.js 可以使用 andb 来排查 glibc 内存,它针对 ptmalloc 实现了一份内存调试指令,再例如 Python 可以使用 tracemalloc 来定位问题。

工具的使用存在一定的门槛,需要对内存的结构有深入了解,之前分享过一个调试案例:网页链接,可以参考。

在近期正好又经历了一次比较有趣的碎片化内存占用问题的排查,现象如图所示,十分诡异,最终发现是 ptmalloc 的内存分配机制的问题,简单来说,就是碎片化内存在收回时,由于与 top chunk 相邻的内存块还存在引用,没有及时回收,导致一整串碎片化内存都无法回收,这是 ptmalloc 自身的一大缺陷,而 tcmalloc 和 jemalloc 上都有较大的改善。

解决方案也比较简单,尝试将 ptmalloc 直接替换成 jemalloc 后,OOM 问题立马就消失了,就连日常内存的占用都降低了很多。

如果你的业务堆外内存也经常飙高,甚至出现 OOM,也可以考虑更换成其他内存管理器,大概率也是可以解决问题,关于这一点,Node.js 社区存在一些有价值的讨论和问题复现代码:网页链接

另外,也推荐阅读这篇文章:《ptmalloc、tcmalloc与jemalloc对比分析》,网页链接,这是“微信看一看”团队将 ptmalloc 更换成 tcmalloc 后,机器 CPU 陡增造成业务停摆引发的一次排查和研究,最后发现是 tcmalloc 的自旋锁带来的性能问题,更换成 jemalloc 就恢复了,这篇文章从系统视角和用户视角分别分析了不同内存管理器的实现原理,讲的很精彩