背景
在上篇文章中
进程状态可视化方案
搭建了可视化界面,对于目前运行环境下的几百个服务器状态就非常清晰了,比如服务的虚拟内存,这时就发现一个服务存在问题:
红色线为virtual Bytes ,大概在1.4G左右
绿色线为Private Bytes , 大概在350M左右
那么应该是出现了严重的内存碎片(Fragmentation)。
往事
在众多疑难问题定位过程中,我一直对刚参加工作时分析的一个内存碎片问题印象非常深刻,因为这个问题锻炼了我坚韧的耐力和大幅度技能的提升。
刚工作的时候被分配到一个视频组件组,主要功能就是网络收流、报文解析、音视频解码、视频渲染、音频同步播放,和当时的视频播放软件最大的区别就是多窗口,最大支持128个视频流同时播放。
当时一个大版本发布之后,长时间运行(1天)组件就会出现内存申请失败,这个问题刚好就交给我处理,当时整个公司都没有人会用分析dump,大家都是用vs进行调试,还有代码的回滚是不可能的,这个版本合入太多功能,其他团队、客户都等着这个版本呢。
所以我的思路
第一步是减少问题复现的时间,通过3天左右的观察、验证,找到了业务流程,写了一个测试demo,将原来需要1天才复现的问题,减少到只需要2个小时左右。
在观察、验证的3天时间,查资料,找原因,把《windows核心编程》 内存方面仔细研读,上网找资料。大概可以锁定是内存碎片导致,而不是内存泄漏。
进行二分法对模块进行分解测试。所以短时间内我又要把整个组件的模块设计、流程学习一遍。
建立execl对测试结果数据进行分析
3~4 步骤坚持了1周左右。
大家可能都觉得应该能找到问题了吧? 结论是 no, 没找到原因。
我们可以梳理下内存碎片的原因,大致就是 a、b 两个大小内存随机申请,最后会导致一个连续内存会被a、b大小内存随机分裂,最后有一个更大的C就无法申请到内存。如果采用二分划分,那么很有可能我们把C给排除了,把a、b 模块保留,这时候程序当然稳定运行,execl表格体现出来的就是这个a、b模块正常,但实际是存在问题的。
现实的程序远比上面的例子复杂的多,模块、开源库、第三方依赖都得考虑,这也是当时整理了1周左右的数据仍然定位不到原因。
问题的分析已经过去了1周半,却没有任何进展,手上堆积的事情也越来越多,这1周半几乎都是7~23点,身体、精神也吃不消,所以就换了一个同事重新整理execl;一周过后也是没有头绪。在这一周中,我赶完落后的项目进度,然后继续查找资料,在网上看到了一本《软件调试》的书籍,学习堆的知识,这一周客户、boss都催的紧,粗略的看完“堆”章节,里面有句话吸引到了我:
1 | Heap中有两个参数 |
刚好我们的组件中有一个大小申请65535大小的空间,会不会因为刚好没到64K导致的? 立刻把模块中的65535修改为65535+1, 经过验证后,程序还真稳定了……….其实也没根本解决问题,只是怀疑,所以内心一直觉的有所欠缺。
工作实在太忙,能力也有限,解决之后也没时间总结这个问题,还有好多事情要加班干呢………
之后的windows上项目基本就很少出现内存碎片的问题,所以就没有这方面的分析经验。
问题再次分析
刚好有程序又出现了类似问题,所以试着用dump来分析。首先我对出现问题的程序代码一点不熟悉,所以我很难去构建复现条件,只能暂时通过dump来分析。
上面截图显示了7天的内存变化,很稳定,有查看了30天的内存,基本也是一样,virtual size 还足够,Private Bytes也不高,所以问题的紧急程度不高,估摸着程序再继续运行一个月也没问题。保存了fulldump。
基础知识
我们大致要了解内存申请的过程,commit、reserv、virtual size、Private Bytes的概念。
堆的三个重要结构体:HEAP、Segment、Enty。
不了解的话,可以先照猫画虎 来按步骤分析一遍。
dump下载
下载
提取码:ykgb
windbg
heap -s
加载dump信息,输入
1 | 0:000> !heap -s |
也可以看出 002a0000 的resevr 比 commit 大了很多。
ps:里面有“Virtual block:” 代表的是超过508K的内存是使用HeapVirtualAlloc申请的,其中(size 00000000)指的是找不到HEAP_VIRTUAL_ALLOC_ENTRY的结构体定义(不知道怎么解决)。
!heap -a 002a0000
打印该堆段所有的内存信息,(截取一部分)
1 | Segment80 at 54790000: |
简单的含义
1 | Segment80 at 54790000: ----- 第80个堆块链表 |
1 | address : psize . size [flags] state (requested size) <debug flags> |
那么把所有链表统计一下大概出现了 1300多次的uncommitted bytes,其中大致浏览存在着1000多次的40000内存busy状态,基本上每个uncommitted 的内存附件都有一个40000的内存,那么是否可以怀疑是40000导致的呢?
40000的堆块大小实际用户申请大小是 3fff8,刚好是256K-8 。逐一查看内存内容也没有找到任何线索。
至此dump的分析导致为此,剩下的过程就需要查看是哪里申请了这么大的内存?
- 代码review
- 用gflags打开栈回溯,问题复现后,通过内存地址进行栈回溯找到内存申请的代码
总结
问题其实没有被定位,后续有时间会继续跟踪此问题。
事实上即使找到内存申请的地方,也不一定能解决。实际对于这种大块(>4KB)的内存申请应该需要谨慎,写代码中也应该尽量避免大块内存的申请,可以考虑使用内存池;
windows本身是有低块堆来解决小内存的申请,但申请的内存大小有限;
偷懒可以直接使用jemalloc/tcmalloc库
本文只是提供了一种分析思路。
后续分析原因 内存碎片分析(2)