一个生产环境C工程内存泄漏问题的排查及重现工程

Posted 李迟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一个生产环境C工程内存泄漏问题的排查及重现工程相关的知识,希望对你有一定的参考价值。

最近遇到一个C工程内存泄漏问题,经过排查,能较好的解决当前出现的问题。

起因

某日,运维反馈生产环境某台设备出现问题,经组长排查,有两个工程服务占用内存较多,出现 OOM 被 Linux 系统干掉了。其中一个是我接手的工程,竟达到了 6GB,随即安排我排查。此事惊动了主任工程师,必须要解决。

排查

首先在本地虚拟机用 cppcheck、valgrind 测试,但没有发现容易看得懂的问题点,像 cppcheck 提示了很多不怎么要紧的问题——其实有大半问题已经在前两个月修正了。而 valgrind提示多的都是第三方库,比如 curl、xml、ssl 等。

因为没有头绪,也不敢随便动生产环境,所以写了个简单的 shell 脚本,用于监控程序的内存使用情况,并放在生产环境上,观察半天,发现隔1分钟就有少量内存泄漏,大概几十 KB 左右。因此得到存在内存泄漏的结论,但这只是验证猜测而已,因为在问题发现之初就已经把问题引致这方面了。

由于代码年代久远,错综复杂,几天过去也没头绪,还好发现概率比较小,还有时间排查。

后经同事指点,将监控程序频率提高,输出内存的同时打印日期时间,将其与工程日志的日期对比,缩小可疑范围,最后定位到传输模块的一个函数。

该函数使用 malloc 根据某个数据表名称为一个结构体变量指针申请内存,再放到 map 全局变量中,由于外部函数使用到,故未释放,跟踪发现在类的析构函数中调用,但在程序运行过程并没有进行析构。存放到 map 的目的是防止多次申请内存,因为数据表的数量有限——不到十个,因此使用 map,在申请之前会查找,如不存在则申请,再存起来。

业务逻辑上并无问题,后在某个不起眼的地方看到了对该 map 变量的清除操作,即调用 clear 函数。怀疑此函数使用有误,于是写了一个简单的测试程序重现问题。最终得到结论,使用 clear 函数并不会释放申请的内存。

重现问题

用于重现问题的测试程序如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <string>
#include <map>

typedef struct {
    char Name[1024];
    char Name1[1024];
    char Name3[1024];
} TTableStruct;

class CMapLeak {
public:
    CMapLeak();
    ~CMapLeak();
    
    TTableStruct *GetTable(const char *TableName);
    void TableTest();

private:
    std::map < std::string, TTableStruct * >m_mTable;
};

CMapLeak::CMapLeak()
{
    
}

CMapLeak::~CMapLeak()
{
    std::map < std::string, TTableStruct * >::iterator iter;

    for (iter = m_mTable.begin(); iter != m_mTable.end(); iter++)
    {
        TTableStruct *pStruct = iter->second;

        if (pStruct != NULL)
        {
            delete pStruct;
        }
    }
}

TTableStruct* CMapLeak::GetTable(const char *TableName)
{
    TTableStruct *pStruct = NULL;
    std::map < std::string, TTableStruct * >::iterator iter;
    iter = m_mTable.find(TableName);
    if (iter == m_mTable.end())
    {
        pStruct = new TTableStruct[100];
        m_mTable[TableName] = pStruct;
        printf("NEW!!! struct ptr: %p\\n", pStruct);
    }
    else
    {
        pStruct = iter->second;
        printf("struct ptr: %p\\n", pStruct);
    }
    return pStruct;
}

void CMapLeak::TableTest()
{
    TTableStruct *pStruct = NULL;
    int i = 0;
    char tablename[32] = {0};
    while (1)
    {
        //m_mTable.clear();
        sprintf(tablename, "table_%d", (i++)&0x03);
        pStruct = GetTable(tablename);
        printf("%s: struct ptr: %p\\n", tablename, pStruct);
        printf("----------------\\n");
        sleep(1);
    }
}

int main(void)
{
    CMapLeak* pLeak = new CMapLeak();
    pLeak->TableTest();

    return 0;
}

代码逻辑比较简单,先查找 m_mTable,如果不存在则申请内存,否则直接返回已申请的内存。为了方便观察内存使用情况,在结构体中多加了几个数组。

当对 map 进行 clear 操作时,出现内存泄漏,监控脚本输出如下:

有内存泄漏的:
2021-10-23 23:14:31
dataserver ps mem: 13596 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2413468 kB Cached: 637216 kB
-------------
2021-10-23 23:14:36
dataserver ps mem: 15116 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2412884 kB Cached: 637216 kB
-------------
2021-10-23 23:14:41
dataserver ps mem: 16636 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2413492 kB Cached: 637216 kB
-------------
2021-10-23 23:14:46
dataserver ps mem: 18460 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2413144 kB Cached: 637216 kB
-------------
2021-10-23 23:14:52
dataserver ps mem: 19980 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2412740 kB Cached: 637216 kB
-------------
2021-10-23 23:14:57
dataserver ps mem: 21500 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2413104 kB Cached: 637216 kB
-------------
2021-10-23 23:15:02
dataserver ps mem: 23020 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2413364 kB Cached: 637216 kB
-------------

如果不调用 clear 函数,则内存占用较稳定:

2021-10-23 23:10:12
dataserver ps mem: 13900 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2413616 kB Cached: 637212 kB
-------------
2021-10-23 23:10:17
dataserver ps mem: 13900 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2412888 kB Cached: 637212 kB
-------------
2021-10-23 23:10:22
dataserver ps mem: 13900 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2413504 kB Cached: 637212 kB
-------------
2021-10-23 23:10:27
dataserver ps mem: 13900 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2413092 kB Cached: 637212 kB
-------------
2021-10-23 23:10:32
dataserver ps mem: 13900 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2413536 kB Cached: 637212 kB
-------------
2021-10-23 23:10:37
dataserver ps mem: 13900 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2412988 kB Cached: 637212 kB
-------------
2021-10-23 23:10:43
dataserver ps mem: 13900 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2413348 kB Cached: 637212 kB
-------------
2021-10-23 23:10:48
dataserver ps mem: 13900 VmRSS: 1068 kB
System memory info:  MemTotal: 3861496 kB MemFree: 2413796 kB Cached: 637212 kB
-------------

小结

就目前排查结果看,只需要将原工程清除 map 的 clear 函数去掉即可。

李迟 2021.10.23 夜

以上是关于一个生产环境C工程内存泄漏问题的排查及重现工程的主要内容,如果未能解决你的问题,请参考以下文章

一个生产环境C++工程段错误的排查

一个生产环境C++工程段错误的排查

一个C++工程CPU占用100%问题的排查

一个C++工程CPU占用100%问题的排查

Linux生产环境下---问题指标面试

如何在生产环境排查 Rust 内存占用过高问题