电脑系统内存泄露

发布时间: 2023-04-16 17:43 阅读: 文章来源:转载
现象

某线上运行的C++服务,在一台机器上访问失败率很高,监控显示如下图:

登录问题机器,发现该服务占用的内存高达30G,这是不正常的,疑似内存泄露。为了减少服务运行损失,先把该机器摘掉,再进行问题分析。

分析

从现象上来看,服务的失败率,主要原因是内存泄露,导致内存占用高,从而影响到服务的正常运行。内存泄露大体可以分为两种:

1、申请了内存,没有释放,代码实现上的bug。

2、全局(或常驻内存)变量,因为某种原因申请了大量的内存,由于其生命周期未结束,暂时还没有释放。这可能是一种设计上的bug。

于是,查看了线上其他机器,并没有发现内存占用过高的情况,同时,也没有发现内存持续增长。从这个方面来看,更倾向于第二种情况,或者是机器自身问题。查看了问题机器上,该服务的运行日志及系统日志,并没有发现明显的问题。

因此,需要从服务进程去定位问题,分析步骤:

dump服务

为了不影响线上业务,需要快速恢复服务。于是,先使用gcore命令将运行的进程dump下来,保存现场,命令如下:

# gcore 24564(进程pid)

执行命令后,会在当前目录下生成一个该进程的core dump文件了。接着,重启服务后,观察了一段时间,并没有发现内存持续增长。

查看内存映射

使用gdb调试刚才dump下来的core dump文件,查看进程中内存分配情况,如下:

(gdb) maintenance info sections………… 0x7f8c18000000->0x7f8c1c000000 at 0x3af828854: load398 ALLOC LOAD HAS_CONTENTS0x7f8c1c000000->0x7f8c1ffff000 at 0x3b3828854: load399 ALLOC LOAD HAS_CONTENTS0x7f8c1ffff000->0x7f8c20000000 at 0x3b7827854: load400 ALLOC LOAD READONLY HAS_CONTENTS0x7f8c20000000->0x7f8c23fe0000 at 0x3b7828854: load401 ALLOC LOAD HAS_CONTENTS0x7f8c23fe0000->0x7f8c24000000 at 0x3bb808854: load402 ALLOC LOAD READONLY HAS_CONTENTS0x7f8c28000000->0x7f8c2bffa000 at 0x3bb828854: load403 ALLOC LOAD HAS_CONTENTS0x7f8c2bffa000->0x7f8c2c000000 at 0x3bf822854: load404 ALLOC LOAD READONLY HAS_CONTENTS0x7f8c30000000->0x7f8c34000000 at 0x3bf828854: load405 ALLOC LOAD HAS_CONTENTS0x7f8c38000000->0x7f8c3c000000 at 0x3c3828854: load406 ALLOC LOAD HAS_CONTENTS0x7f8c40000000->0x7f8c43ffe000 at 0x3c7828854: load407 ALLOC LOAD HAS_CONTENTS0x7f8c43ffe000->0x7f8c44000000 at 0x3cb826854: load408 ALLOC LOAD READONLY HAS_CONTENTS………………

可以看到分配了大量的内存,猜测这些内存块就是泄露的。为了定位到内存泄露的代码,尝试查看了这些几个内存块中的内容,看看是否能看到一些字符串或者有规律的内容,但是,并没有发现有价值的线索。

排查全局变量

在gdb中,使用info variables命令,可以将进程中的全局变量,打印出来。但是由于引用了很多的第三方库等,可能输出内容会比较多。因此,可以考虑通过脚本进行过滤,重点排查该服务相关的变量。这里并不是说,第三方库不会有问题,但是,目前嫌疑最大的仍然是我们写的代码。针对筛选出来的全局变量,进行排查。重点检查的内容有,数组、vector、map等各种容器大小。

在排查中,发现其中一个全局变量中queue对象的_M_map_size = 10485758,这是不正常的。

(gdb) p Singleton::instance_->query_queue_$2 = {queue_ = {c = { = {_M_impl = { = { = {}, },members of std::_Deque_base::_Deque_impl:_M_map = 0x7f8ac2fff010,_M_map_size = 10485758,_M_start = {_M_cur = 0x7f8c08040f68,_M_first = 0x7f8c08040d70,_M_last = 0x7f8c08040f70,_M_node = 0x7f8ac5c17e80},_M_finish = {_M_cur = 0x7f903532af30,_M_first = 0x7f903532ade0,_M_last = 0x7f903532afe0,_M_node = 0x7f8ac7822960}}}, }},}

可以看到,这是一个使用std::queue实现的队列,queue的内存结构由一个中控器(map)、多个缓冲区和两个迭代器(start和finish)组成。其中,中控器是一个连续的内存块,每个元素指向一个缓冲区。start和finish迭代器分别指向队列中头和尾。缓冲区中,存放队列中元素地址。如下:

根据调试信息,可以计算出:

map中node个数为:(0x7f8ac7822960-0x7f8ac5c17e80)/8=3675484个。

每个node对应一个缓冲区,缓冲区的大小为0x7f8c08040f70 - 0x7f8c08040d70 = 512字节。

由于地址占8字节,因此一个缓冲区中有:512/8=64个元素。

当前队列中,元素总数为:3675484*64=235230976

通过查看内存中字符串占的内存,可以发现,缓存的字符串占用内存平均在100字符左右,计算这部分内存为:(235230976*100)/(1024*1024*1024.0)=21.9G

与泄露的内存大小基本符合。基本可以认为,是由于队列中缓存元素过多,导致了服务的内存泄露。

阅读源码

通过阅读源码,可以看到该变量的所在模块的功能是:实现一个带过期时间的缓存。具体实现逻辑是:每次请求从该缓存模块中取得一个key的缓存值,若该key已经在缓存中,且未过期,则直接返回对应的缓存值。若没有在缓存中(或已经过期),该将该key写入到一个队列中,由另一个更新线程去后台查询更新该key对应的缓存。

造成队列中数据大量堆积的可能原因是,某一时刻,更新线程查询后台时,出现了延时,引发堆积。由于访问量很大,且过期时间设定得较短,再加上,队列中重复key也并不会去重。因此,只要发生抖动,就会发生大量堆积,最终导致内存激增,服务异常。

为了验证问题,重新查看运行日志,确实能找到查询后台服务失败的相关日志,只是报错日志量不大,在最开始查看日志时,并未引起注意。至此,基本可以确认该问题。

总结

对于线上问题处理来说,一般的原则是,先快速恢复服务,后定位问题。比如,因为新功能上线,导致服务异常,首先做的是代码回滚。本文中通过gcore将进程的内存dump下来,能较好地保存现场,同时,重启进程,恢复服务。因为已经保存了进程当时的内存状态,可以给分析问题提供较大地便利和更多地依据。

内存泄露的排查方法和工具有很多,如何在不重启服务、不重新编译、最小性能影响等方式下,快速定位到进程内存泄露点,仍有一定的挑战。

•••展开全文
相关文章