频繁申请释放内存导致进程被OOM掉

477 阅读3分钟

问题背景和表现

后台程序长时间运行,内存占用很高,最终进程被系统OOM掉了。

[Wed Feb 16 00:20:37 2022] Memory cgroup out of memory: Killed process 54509 
(live_main) total-vm:79190316kB, anon-rss:6376768kB, file-rss:5592kB, shmem-rss:0kB, 
UID:0 pgtables:154232kB oom_score_adj:0

调查过程

此问题调查花费的时候比较长,总结起来原因有:

  1. 程序需要运行很久才会出现内存占用高,被OOM掉
  2. 非必现,线上出现概率较大,测试机器很难出现
  3. 业务层代码全部采用智能指针,内存申请在底层基础库代码,不应该有内存泄露的问题
  4. 对操作系统内存回收机制不了解

程序长时间运行内存越来越高,首先想到的是内存泄露
需要在此声明:因为已知程序会频繁向操作系统申请和释放内存,所以会造成操作系统内存碎片的问题,也同时在思考内存碎片是否会导致内存(RSS)占用高而被操作系统kill掉。

调查过程总体分为两个步骤:

  1. 梳理出程序中所有申请内存的地方,逐一check,尽量避免内存申请。使用到了jemalloc
  2. 验证申请大量内存,然后释放掉,进程内存占用是否降低

jemalloc工具使用

jemalloc工具使用起来很简单,可参考链接
一般情况下,程序启动的时候都会初始化一些东西,从而占用一些内存,如果只想监控程序运行时的内存情况,则在完成初始化后通过设置prof.active为true即可。

    bool active = true;
    mallctl(prof.active , NULL, NULL, &active, sizeof(bool));
    mallctl(prof.dump , NULL, NULL, NULL, 0);

通过jeprof生成的函数调用图可以很清楚的看到内存的申请情况,逐一修改,避免申请内存,同时也优化了程序性能。
突然想到需要做一个测试,申请大量内存,然后释放掉,看进程的内存占用情况。

猜测验证

#include <stdio.h>
#include <vector>
#include <malloc.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>

void printMemory()
{
    system("ps afux | egrep test_main");
    system("free -h");
}

int main()
{
    std::vector<char*> vv;
    for (size_t i = 0; i < 1024*1024*10; i++)
    {
        char* ptr = new char[1024];
        vv.push_back(ptr);
    }
    printMemory();
    sleep(10);

    for (auto &it : vv)
    {
        delete[] it;
    }
    printMemory();   // 第二次输出内存占用
    sleep(1000);
    return 0;
}
g++ -o test_main -g test_main.cpp

代码思路很简单,就是先申请10G的内存,然后在释放掉,但是进程不退出。
查看进程内存占用情况:进程仍然占用内存10G+

USER         PID %CPU %MEM    VSZ   RSS      TTY   STAT START   TIME COMMAND
xx       2849290 29.3 68.9 11295116 11234800 pts/0 S+   11:05   0:16  |           \_ ./test_main

查看系统内存情况:系统已占用内存超过10G+

# free  -h
              total        used        free      shared  buff/cache   available
Mem:            15G         12G        2.3G        162M        1.1G        3.0G
Swap:            0B          0B          0B

基本看到这个情况,本文调查的问题已经有了结论。

但是:为什么已经free掉的内存,进程的内存占用(RSS)还是没有降低呢,我的简单理解是:

free掉的内存没有立刻还给操作系统,操作系统后续会通过kswapd来回收,但是回收的时间不定

这个问题比较大,在此不做详细说明。详看链接

一个比较有趣的点是:修改每次申请内存的大小和总量,在不同机器测试,结果是不一样的。如果每次申请100MB,共申请10G,最终free掉之后,进程仅占用极少内存,free命令查看进程释放的10G内存已经还给了操作系统了

之后,打开两个控制台,先后运行测试程序,则会出现操作系统OOM掉程序的情况,详见:

[Fri Feb 18 15:58:44 2022] Out of memory: Kill process 1596380 (test_main) score 640 or sacrifice child
[Fri Feb 18 15:58:44 2022] Killed process 1596380 (test_main) total-vm:10793688kB, anon-rss:10731700kB, file-rss:1424kB, shmem-rss:0kB
[Fri Feb 18 15:58:44 2022] oom_reaper: reaped process 1596380 (test_main), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

其中 anon-rss: 没有文件背景的页面,即匿名内存

总结

总体来说还是对操作系统内存模块理解不深,不清楚操作系统内存回收的机制。不清楚代码中调用了free和用linux命令free看到内存之间的关系,导致此问题调查时间比较长。