翼度科技»论坛 编程开发 .net 查看内容

记一次 .NET 某游戏服务后端 内存暴涨分析

6

主题

6

帖子

18

积分

新手上路

Rank: 1

积分
18
一:背景

1. 讲故事

前几天有位朋友找到我,说他们公司的后端服务内存暴涨,而且CPU的一个核也被打满,让我帮忙看下怎么回事,一般来说内存暴涨的问题都比较好解决,就让朋友抓一个 dump 丢过来,接下来我们用 WinDbg 一探究竟。
二:WinDbg 分析

1. 到底是谁在暴涨

拿到 dump 之后,首先要判断是托管还是非托管问题,这决定了我们后续的探究方向,我们直接用 !address -summary + !dumpheap -stat  即可。
  1. 0:000> !address -summary
  2. --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
  3. MEM_FREE                                212     7dfe`fb4e7000 ( 125.996 TB)           98.43%
  4. MEM_RESERVE                             368      200`1dbd6000 (   2.000 TB)  99.82%    1.56%
  5. MEM_COMMIT                             1741        0`e6f33000 (   3.609 GB)   0.18%    0.00%
  6. 0:000> !dumpheap -stat
  7. Statistics:
  8.               MT    Count    TotalSize Class Name
  9. ...
  10. 7ff9858ad8e0   409,869   258,383,328 System.Collections.Generic.Dictionary<System.Int64, xxx.xxx>+Entry[]
  11. 0225cc98e1f0   283,654   479,330,568 Free
  12. 7ff9858ab160         8 2,147,484,480 xxx.xxxUnit[]
  13. 0:000> !dumpheap -mt 7ff9858ab160
  14.          Address               MT           Size
  15.     022585dd26b8     7ff9858ab160             24
  16.     02258beb3c78     7ff9858ab160             24
  17.     02259f272aa8     7ff9858ab160            152
  18.     0225a8ae0858     7ff9858ab160            152
  19.     0225a8d015c8     7ff9858ab160            152
  20.     0225a91da130     7ff9858ab160            152
  21.     0225a9395ad0     7ff9858ab160            152
  22.     022694c91020     7ff9858ab160  2,147,483,672
  23. Statistics:
  24.           MT Count     TotalSize Class Name
  25. 7ff9858ab160     8 2,147,484,480 xxx.xxxUnit[]
  26. Total 8 objects, 2,147,484,480 bytes
复制代码
从卦象看,3.6G 的提交内存,xxx.xxxUnit[] 就占用了 2.1G,可以确定当前是托管内存暴涨,并且也看到了内存都被 022694c91020 这个对象给吃掉了,接下来就是看下这个对象到底被谁持有着? 使用 !gcroot 即可。
  1. 0:000> !gcroot 022694c91020  
  2. Caching GC roots, this may take a while.
  3. Subsequent runs of this command will be faster.
  4. Found 0 unique roots.
复制代码
我去,从卦中看当前的 022694c91020 没有引用根,也就表明这个对象应该会在后续过程中被回收,但这里有一个问题,这个 xxx.xxxUnit[] 到底定义在代码何处呢? 知道在何处,就可以完美的解决问题。
2. 数组到底定义在何处

可以仔细想一想,xxx.xxxUnit[] 没有被 GC 回收,从侧面也表明它可能刚分配不久,并且是一个局部变量,既然是局部变量,就可以反向找到是哪一个线程分配的,如果线程栈还残留着 返回地址 信息,就可以反推出是哪一个方法,有了这个思路,接下来就可以动手挖了。
按照编码人的习惯, xxx.xxxUnit[] 肯定是某一个 List 集合,可以用内存搜索解决。
  1. 0:000> s-q 0 L?0xffffffffffffffff 022694c91020
  2. 00000225`a89530a0  00000226`94c91020 0cca3690`0cca3690
  3. 0:000> !lno 00000225`a89530a0
  4. Before:  00000225a8953098           32 (0x20)        System.Collections.Generic.List`1[[xxx.xxxUnit, xx.xx]]
  5. After:   00000225a89530b8         1224 (0x4c8)        Free
  6. Heap local consistency confirmed.
复制代码
从卦中看,果然用的是一个 List 集合,万事开头难,接下来继续反向搜索,如果线程栈还有残留的话,就可以找到它所属的线程栈。
  1. 0:000> s-q 0 L?0xffffffffffffffff 00000225a8953098
  2. 0000004c`417ecd98  00000225`a8953098 00000005`00000000
  3. 0000004c`417eceb8  00000225`a8953098 0cca3695`00000000
  4. 00000225`f1070180  00000225`a8953098 00000225`d7d287f8
  5. 00000225`f10701e0  00000225`a8953098 00000225`d7d287f8
  6. 0:000> !address 0000004c`417ecd98
  7. Usage:                  Stack
  8. Base Address:           0000004c`417d1000
  9. End Address:            0000004c`417f0000
  10. Region Size:            00000000`0001f000 ( 124.000 kB)
  11. State:                  00001000          MEM_COMMIT
  12. Protect:                00000004          PAGE_READWRITE
  13. Type:                   00020000          MEM_PRIVATE
  14. Allocation Base:        0000004c`41670000
  15. Allocation Protect:     00000004          PAGE_READWRITE
  16. More info:              ~0k
  17. Content source: 1 (target), length: 3268
复制代码
从卦中的 More info: 信息来看,它是属于 0 号线程,如果你不相信的话,可以拿 417d1000 去内存段验证下,输出如下:
  1. 0:000> !address -f:Stack
  2.         BaseAddress      EndAddress+1        RegionSize     Type       State                 Protect             Usage
  3. --------------------------------------------------------------------------------------------------------------------------
  4.       4c`41670000       4c`417cc000        0`0015c000 MEM_PRIVATE MEM_RESERVE                                    Stack      [~0; ec8.1584]
  5.       4c`417cc000       4c`417d1000        0`00005000 MEM_PRIVATE MEM_COMMIT  PAGE_READWRITE | PAGE_GUARD        Stack      [~0; ec8.1584]
  6.       4c`417d1000       4c`417f0000        0`0001f000 MEM_PRIVATE MEM_COMMIT  PAGE_READWRITE                     Stack      [~0; ec8.1584]
复制代码
既然找到了是 0 号线程,接下来可以用 !clrstack 观察下,奇怪的是 0 号线程啥都没有,我怀疑这个 dump 抓的有问题,可以截图为证。

看不到任何线程栈信息,这就难搞了,接下来的路在何方呢?
3. 还有希望吗

作为调试人,一定要在绝望中寻找希望,突破口就是考验线程栈布局的理解,可以在栈上往小地址找,会找到子函数的返回地址(returnAddress),即类似的格式: 0x00007ffxxxxxx,这个地址和 List 都同属一个方法,如果不清楚的话画个简图如下:

如图中所述找到 子方法 ReturnAddress 地址值即可,接下来使用windbg 的 dqs 命令外加 !ip2md 观察方法名即可。
  1. 0:000> dqs 0000004c`417ecd98 L-50
  2. 0000004c`417ecb18  0000004c`417ed678
  3. 0000004c`417ecb20  00000225`b9e78518
  4. 0000004c`417ecb28  00007ff9`85f3f861
  5. 0000004c`417ecb30  00000225`ba22b8c0
  6. 0000004c`417ecb38  0000001a`00000027
  7. 0000004c`417ecb40  00000225`00000027
  8. 0000004c`417ecb48  00000225`84aef0f8
  9. ...
  10. 0000004c`417ecd88  0000001a`00000000
  11. 0000004c`417ecd90  00000225`82278a68
  12. 0:000> !ip2md 00007ff9`85f3f861
  13. MethodDesc:   00007ff983ef1af0
  14. Method Name:          xxx.xxx.xxxRange(xxx,xxx,xxx,xxx)
  15. Class:                00007ff983ef1a58
  16. MethodTable:          00007ff983ef1b70
  17. mdToken:              0000000006000A47
  18. Module:               00007ff983d9c060
  19. IsJitted:             yes
  20. Current CodeAddr:     00007ff985f3f160
  21. Version History:
  22.   ILCodeVersion:      0000000000000000
  23.   ReJIT ID:           0
  24.   IL Addr:            00000225ef226c48
  25.      CodeAddr:           00007ff985f3f160  (MinOptJitted)
  26.      NativeCodeVersion:  0000000000000000
复制代码
在卦中获取到这些信息之后,接下来看下 xxx.xxx.xxxRange 中是否有 List 集合,为什么高达 2个G,经过仔细研读代码,终于发现了问题,截图如下:

从图中看,核心点就是这里的 num++,在某些情况下会导致在 for 中出不来继而不断的 List.Add ,最终导致问题的发生。
再回头结合朋友说的内存暴涨,伴随一个 CPU 核心被打满,完全就可以解释了。
三:总结

这是一个比较隐晦的逻辑bug导致的内存暴涨,如果仅仅从代码层面去分析,相信你可能要花费好久的时间,从高级调试的角度看,在 List 无根的情况下如何快速的找到 List 所属的代码块,也是对基础知识的一个考验。


来源:https://www.cnblogs.com/huangxincheng/archive/2023/07/13/17550195.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具