网站首页 > 文章精选 正文
现象
某线上运行的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_CONTENTS
0x7f8c1c000000->0x7f8c1ffff000 at 0x3b3828854: load399 ALLOC LOAD HAS_CONTENTS
0x7f8c1ffff000->0x7f8c20000000 at 0x3b7827854: load400 ALLOC LOAD READONLY HAS_CONTENTS
0x7f8c20000000->0x7f8c23fe0000 at 0x3b7828854: load401 ALLOC LOAD HAS_CONTENTS
0x7f8c23fe0000->0x7f8c24000000 at 0x3bb808854: load402 ALLOC LOAD READONLY HAS_CONTENTS
0x7f8c28000000->0x7f8c2bffa000 at 0x3bb828854: load403 ALLOC LOAD HAS_CONTENTS
0x7f8c2bffa000->0x7f8c2c000000 at 0x3bf822854: load404 ALLOC LOAD READONLY HAS_CONTENTS
0x7f8c30000000->0x7f8c34000000 at 0x3bf828854: load405 ALLOC LOAD HAS_CONTENTS
0x7f8c38000000->0x7f8c3c000000 at 0x3c3828854: load406 ALLOC LOAD HAS_CONTENTS
0x7f8c40000000->0x7f8c43ffe000 at 0x3c7828854: load407 ALLOC LOAD HAS_CONTENTS
0x7f8c43ffe000->0x7f8c44000000 at 0x3cb826854: load408 ALLOC LOAD READONLY HAS_CONTENTS
………………
可以看到分配了大量的内存,猜测这些内存块就是泄露的。为了定位到内存泄露的代码,尝试查看了这些几个内存块中的内容,看看是否能看到一些字符串或者有规律的内容,但是,并没有发现有价值的线索。
- 排查全局变量
在gdb中,使用info variables命令,可以将进程中的全局变量,打印出来。但是由于引用了很多的第三方库等,可能输出内容会比较多。因此,可以考虑通过脚本进行过滤,重点排查该服务相关的变量。这里并不是说,第三方库不会有问题,但是,目前嫌疑最大的仍然是我们写的代码。针对筛选出来的全局变量,进行排查。重点检查的内容有,数组、vector、map等各种容器大小。
在排查中,发现其中一个全局变量中queue对象的_M_map_size = 10485758,这是不正常的。
(gdb) p Singleton<ECached>::instance_->query_queue_
$2 = {
queue_ = {
c = {
<std::_Deque_base<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >> = {
_M_impl = {
<std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >> = {
<__gnu_cxx::new_allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >> = {<No data fields>}, <No data fields>},
members of std::_Deque_base<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >::_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
}
}
}, <No data fields>}
},
}
可以看到,这是一个使用std::queue<std::string>实现的队列,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下来,能较好地保存现场,同时,重启进程,恢复服务。因为已经保存了进程当时的内存状态,可以给分析问题提供较大地便利和更多地依据。
内存泄露的排查方法和工具有很多,如何在不重启服务、不重新编译、最小性能影响等方式下,快速定位到进程内存泄露点,仍有一定的挑战。
- 上一篇: c++基础知识汇总
- 下一篇: Keil 生成的 Map 文件有什么作用?
猜你喜欢
- 2025-01-08 使用 Vector 将 PostgreSQL 日志输出为 Prometheus 指标
- 2025-01-08 java的list和map区别,list和map的区别是什么
- 2025-01-08 界面组件DevExtreme v22.2亮点——UI模板库升级换代!
- 2025-01-08 Unity Shaders学习笔记--SurfaceShader(九)Cubemap
- 2025-01-08 谷歌地图API的三大开源替代品
- 2025-01-08 linux下GDB使用方法
- 2025-01-08 一文读懂map和hash_map的差异原理
- 2025-01-08 C/C++从0到1系统精讲 项目开发综合基础课
- 2025-01-08 《叛乱:沙漠风暴》PC版更新上线!追加新地图/模式
- 2025-01-08 C++游戏客户端/服务器端开发需要掌握什么?
- 05-16一文学完《图解HTTP》
- 05-16您未被授权查看该页
- 05-16快码住!带你十分钟搞懂HTTP与HTTPS协议及请求的区别
- 05-16一张图带你了解HTTP 9个请求方法,收藏!
- 05-16Java 里的基本类型和引用类型
- 05-16新手小白学Java|零基础入门笔记|原来学Java可以这么简单
- 05-16深度学习CV方向高频算法面试题6道|含解析
- 05-16C语言结构体成员变量名后加冒号和数字的含义
- 最近发表
- 标签列表
-
- newcoder (56)
- 字符串的长度是指 (45)
- drawcontours()参数说明 (60)
- unsignedshortint (59)
- postman并发请求 (47)
- python列表删除 (50)
- 左程云什么水平 (56)
- 计算机网络的拓扑结构是指() (45)
- 编程题 (64)
- postgresql默认端口 (66)
- 数据库的概念模型独立于 (48)
- 产生系统死锁的原因可能是由于 (51)
- 数据库中只存放视图的 (62)
- 在vi中退出不保存的命令是 (53)
- 哪个命令可以将普通用户转换成超级用户 (49)
- noscript标签的作用 (48)
- 联合利华网申 (49)
- swagger和postman (46)
- 结构化程序设计主要强调 (53)
- 172.1 (57)
- apipostwebsocket (47)
- 唯品会后台 (61)
- 简历助手 (56)
- offshow (61)
- mysql数据库面试题 (57)