背景
在上篇文章中
进程状态可视化方案
搭建了可视化界面,对于目前运行环境下的几百个服务器状态就非常清晰了,比如服务的虚拟内存,这时就发现一个服务存在问题:

红色线为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
2
3
4
5
6
7
8
9Heap中有两个参数
1、HeapDeCommitFreeBlockThreshold
2、HeapDecommitTotalFreeThreshold
考虑到应用程序很可能还会立刻申请内存并减少与内存管理器的交互次数,内存释放时需要同时满足两个条件才能被立即解除提交
1、释放的堆块超过HeapDeCommitFreeBlockThreshold的值(比如4KB)
2、堆上的总空闲空间达到HeapDecommitTotalFreeThreshold(比如64K)
否则,堆管理器会将这个块加到空闲块列表中,并更新堆管理器的总空闲值。
刚好我们的组件中有一个大小申请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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
270:000> !heap -s
SEGMENT HEAP ERROR: failed to initialize the extention
LFH Key                   : 0x58b8f7f8
Termination on corruption : DISABLED
  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
002a0000 00000002 1246976 510132 1246976 261431  2450  1382    0    5d6   LFH
    External fragmentation  51 % (2450 free blocks)
    Virtual address fragmentation  59 % (1382 uncommited ranges)
00590000 00001002    3136   1636   3136    860     6     3    0   471b   LFH
    External fragmentation  52 % (6 free blocks)
00490000 00001002     256      4    256      1     1     1    0      0      
004d0000 00001002    1280    420   1280    122    50     2    0      0   LFH
Virtual block: 032f0000 - 032f0000 (size 00000000)
Virtual block: 05310000 - 05310000 (size 00000000)
00910000 00001002    1280    556   1280    136     4     2    2      0   LFH
00b20000 00001002      64     12     64      4     2     1    0      0      
00110000 00001002      64      4     64      2     1     1    0      0      
03120000 00001002      64      4     64      2     1     1    0      0      
00520000 00011002     256      4    256      1     2     1    0      0      
032b0000 00001002     256      4    256      1     2     1    0      0      
034d0000 00001002    3328   1996   3328   1050    49     3    0      6   LFH
    External fragmentation  52 % (49 free blocks)
05420000 00001002      64     16     64     13     1     1    0      0      
00960000 00001002    1088    212   1088     77     2     2    0      0      
-----------------------------------------------------------------------------
也可以看出 002a0000 的resevr 比 commit 大了很多。
ps:里面有“Virtual block:” 代表的是超过508K的内存是使用HeapVirtualAlloc申请的,其中(size 00000000)指的是找不到HEAP_VIRTUAL_ALLOC_ENTRY的结构体定义(不知道怎么解决)。
!heap -a 002a0000
打印该堆段所有的内存信息,(截取一部分)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55Segment80 at 54790000:
       Flags:           00000000
       Base:            54790000
       First Entry:     54790040
       Last Entry:      55760000
       Total Pages:     00000fd0
       Total UnCommit:  00000b0f
       Largest UnCommit:00000000
       UnCommitted Ranges: (9)
   Heap entries for Segment80 in Heap 002a0000
        address: psize . size  flags   state (requested size)
       54790000: 00000 . 00040 [101] - busy (3f)
       54790040: 00040 . 40000 [101] - busy (3fff8) Internal 
       547d0040: 40000 . 40000 [101] - busy (3fff8) Internal 
       54810040: 40000 . 40000 [101] - busy (3fff8) Internal 
       54850040: 40000 . 00fa0 [100]
       54850fe0: 00fa0 . 00020 [111] - busy (1d)
       54851000:      0063d000      - uncommitted bytes.
       54e8e000: 00000 . 01000 [100]
       54e8f000: 01000 . 40000 [101] - busy (3fff8) Internal 
       54ecf000: 40000 . 40000 [101] - busy (3fff8) Internal 
       54f0f000: 40000 . 40000 [101] - busy (3fff8) Internal 
       54f4f000: 40000 . 40fe0 [100]
       54f8ffe0: 40fe0 . 00020 [111] - busy (1d)
       54f90000:      000fe000      - uncommitted bytes.
       5508e000: 00000 . 40000 [100]
       550ce000: 40000 . 01000 [101] - busy (ff8) Internal 
       550cf000: 01000 . 40000 [100]
       5510f000: 40000 . 20000 [101] - busy (1fff8) Internal 
       5512f000: 20000 . 10000 [101] - busy (fff8) Internal 
       5513f000: 10000 . 03fe0 [100]
       55142fe0: 03fe0 . 00020 [111] - busy (1d)
       55143000:      0000b000      - uncommitted bytes.
       5514e000: 00000 . 40000 [100]
       5518e000: 40000 . 40000 [101] - busy (3fff8) Internal 
       551ce000: 40000 . 00fe0 [100]
       551cefe0: 00fe0 . 00020 [111] - busy (1d)
       551cf000:      001ff000      - uncommitted bytes.
       553ce000: 00000 . 40840 [100]
       5540e840: 40840 . 40000 [101] - busy (3fff8) Internal 
       5544e840: 40000 . 00400 [101] - busy (3f8) Internal 
       5544ec40: 00400 . 003a0 [100]
       5544efe0: 003a0 . 00020 [111] - busy (1d)
       5544f000:      000be000      - uncommitted bytes.
       5550d000: 00000 . 01000 [100]
       5550e000: 01000 . 40000 [101] - busy (3fff8) Internal 
       5554e000: 40000 . 40fe0 [100]
       5558efe0: 40fe0 . 00020 [111] - busy (1d)
       5558f000:      0000e000      - uncommitted bytes.
       5559d000: 00000 . 00800 [100]
       5559d800: 00800 . 10000 [101] - busy (fff8) Internal 
       555ad800: 10000 . 10000 [101] - busy (fff8) Internal 
       555bd800: 10000 . 407e0 [100]
       555fdfe0: 407e0 . 00020 [111] - busy (1d)
简单的含义1
2
3
4
5
6
7
8
9Segment80 at 54790000:                ----- 第80个堆块链表
       Flags:           00000000
       Base:            54790000
       First Entry:     54790040
       Last Entry:      55760000
       Total Pages:     00000fd0
       Total UnCommit:  00000b0f
       Largest UnCommit:00000000
       UnCommitted Ranges: (9)    ------  有9个未提交的内存
| 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)
 
        