记一次 .NET 医疗布草 API 程序 内存暴涨分析

存储 存储软件
我在年前写过一篇关于CPU爆高的分析文章 再记一次 应用服务器 CPU 暴高事故分析 ,当时是给同济做项目升级,看过那篇文章的朋友应该知道,最后的结论是运维人员错误的将 IIS 应用程序池设成 32bit 导致了事故的发生,这篇算是后续,拖了好久才续上哈。

[[396727]]

本文转载自微信公众号「一线码农聊技术」,作者一线码农聊技术。转载本文请联系一线码农聊技术公众号。

一:背景

1. 讲故事

我在年前写过一篇关于CPU爆高的分析文章 再记一次 应用服务器 CPU 暴高事故分析 ,当时是给同济做项目升级,看过那篇文章的朋友应该知道,最后的结论是运维人员错误的将 IIS 应用程序池设成 32bit 导致了事故的发生,这篇算是后续,拖了好久才续上哈。

犹记得那些天老板天天找我们几个人开会,大概老板是在传导甲方给过来的压力,人倒霉就是这样,你说 CPU 爆高可怕吧,我硬是给摁下去了,好了,Memory 又爆高了,尼玛我又给摁下去了,接着数据库死锁又来了,你能体会到这种压力吗??? 就像我在朋友圈发的那样,程序再不跑我就要跑了。

[[396728]]

所以有时候敬敬风水还是很有必要的,有点扯远了哈,这篇我们来看看程序的内存暴涨如何去排查,为了让你更有兴趣,来一张运维发的内存监控图。

从图中可以看出,9点开始内存直线暴涨,绝对惊心动魄,要是我的小诺安这样暴涨就好了,接下来 windbg 说话。

二:windbg 分析

1. 说一下思路

内存暴涨了,最怕的就是 非托管层 出了问题,它的排查难度相比 托管层 要难10倍以上,所以遇到这类问题,先祈祷一下吧,gc堆也罢,loader堆也不怕,所以先看看是否 进程内存 ≈ gc堆内存 ?

2. 排查托管还是非托管

排查方式也很简单,通过 !address -summary 看看进程的已提交内存,如下输出:

  1. 0:000> !address -summary 
  2.  
  3. --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal 
  4. MEM_FREE                                261      7fb`4b151000 (   7.982 TB)           99.77% 
  5. MEM_RESERVE                             278        2`6aafc000 (   9.667 GB)  51.35%    0.12% 
  6. MEM_COMMIT                             2199        2`4a3a3000 (   9.160 GB)  48.65%    0.11% 

可以看到已提交内存是 9.1G,接下来看下 gc 堆的大小,使用 !eeheap -gc 即可。

  1. 0:000> !eeheap -gc 
  2. Number of GC Heaps: 8 
  3. ------------------------------ 
  4. Heap 0 (0000000002607740) 
  5. generation 0 starts at 0x00000001aaaa5500 
  6. generation 1 starts at 0x00000001aa3fd070 
  7. generation 2 starts at 0x0000000180021000 
  8. Heap 7 (0000000002713b40) 
  9. generation 0 starts at 0x000000053b3a2c28 
  10. generation 1 starts at 0x000000053a3fa770 
  11. generation 2 starts at 0x0000000500021000 
  12.  
  13. ------------------------------ 
  14. GC Heap Size:            Size: 0x1fdfe58c8 (8556271816) bytes. 

从最后一行输出中可以看到当前的占用是 8556271816 / 1024 /1024 /1024 = 7.9G ,太幸运了,果然是托管层出了问题,gc 上有大块脏东西,这下有救了 。

3. 查看托管堆

知道托管堆出了问题后,接下来就可以用 !dumpheap -stat 去gc堆上翻一翻。

  1. 0:000> !dumpheap -stat 
  2. Statistics
  3.               MT    Count    TotalSize Class Name 
  4. 000007fef7b5c308    32867       788808 System.DateTime 
  5. 000007fef7b5e598    33049       793176 System.Boolean 
  6. 000007feec7301f8    30430      1217200 System.Web.HttpResponseUnmanagedBufferElement 
  7. 000007fef7b56020     2931      3130928 System.Object[] 
  8. 000007fef7b580f8   219398      5265552 System.Int32 
  9. 000007fe9a0c5428    46423      7799064 xxx.Laundry.Entities.V_InvoiceInfo 
  10. 000007fef7b59638   164418      7892064 System.Text.StringBuilder 
  11. 000007fef7b56980   164713     10059852 System.Char[] 
  12. 000007fef7b5a278     7351     26037217 System.Byte[] 
  13. 000007fe9a0d8758       35     28326856 xxx.Laundry.Entities.V_ClothesTagInfo[] 
  14. 0000000002536f50    76837     77016088      Free 
  15. 000007fe9a327ab0    46534    312964608 xxx.Laundry.Entities.V_InvoiceClothesInfo[] 
  16. 000007fe9a0c4868  2068912    397231104 xxx.Laundry.Entities.V_ClothesTagInfo 
  17. 000007fef7b55b70 98986851   3483764540 System.String 
  18. 000007fe9a10ef80 23998759   3839801440 xxx.Laundry.Entities.V_InvoiceClothesInfo 
  19. Total 126039641 objects 

我去,托管堆上的 xxx.Laundry.Entities.V_InvoiceClothesInfo 对象居然高达 2399w ,占了大概 3.6G,这还不算其附属对象,对了,如果直接用 !dumpheap -mt xxx 输出 address 的话,很难进行UI中止,所以这里有一个小技巧,用 range 来限定一下,如下代码所示:

  1. 0:000> !dumpheap -mt 000007fe9a10ef80 0 0000000180027b30 
  2.          Address               MT     Size 
  3. 0000000180027800 000007fe9a10ef80      160      
  4. 0000000180027910 000007fe9a10ef80      160      
  5. 0000000180027a20 000007fe9a10ef80      160      
  6. 0000000180027b30 000007fe9a10ef80      160      
  7.  
  8. Statistics
  9.               MT    Count    TotalSize Class Name 
  10. 000007fe9a10ef80        4          640 xxx.Laundry.Entities.V_InvoiceClothesInfo 
  11. Total 4 objects 

4. 查找引用根

接下来用 !gcroot 随便找一个 address 查看它的引用链,看看它到底被谁引用着?

  1. 0:000> !gcroot 0000000180027800 
  2. HandleTable: 
  3.     00000000013715e8 (pinned handle) 
  4.     -> 000000058003c038 System.Object[] 
  5.     -> 00000004800238a0 System.Collections.Generic.List`1[[xxx.Laundry.APIService.Models.Common.BaseModel, xxx.Laundry.APIService]] 
  6.     -> 0000000317e01ae0 xxx.Laundry.APIService.Models.Common.BaseModel[] 
  7.     -> 000000028010caf0 xxx.Laundry.APIService.Models.Common.BaseModel 
  8.     -> 00000003014cbbd0 System.Collections.Generic.List`1[[xxx.Laundry.Entities.V_InvoiceInfo, xxx.Laundry.Entities]] 
  9.     -> 00000003014f3580 xxx.Laundry.Entities.V_InvoiceInfo[] 
  10.     -> 00000003014cd7f0 xxx.Laundry.Entities.V_InvoiceInfo 
  11.     -> 000000038cc49bf0 System.Collections.Generic.List`1[[xxx.Laundry.Entities.V_InvoiceClothesInfo, xxx.Laundry.Entities]] 
  12.     -> 000000038cc49c18 xxx.Laundry.Entities.V_InvoiceClothesInfo[] 
  13.     -> 0000000180027800 xxx.Laundry.Entities.V_InvoiceClothesInfo 
  14.  
  15. Found 1 unique roots (run '!GCRoot -all' to see all roots). 

从输出中可以看到,它貌似被一个 List 所持有,哈哈,总算找到了,接下来就简单了,直接用 !objsize 看一看它的 size 有多大?

  1. 0:000> !objsize 00000004800238a0 
  2. sizeof(00000004800238a0) = -1972395312 (0x8a6fa2d0) bytes (System.Collections.Generic.List`1[[xxx.Laundry.APIService.Models.Common.BaseModel, xxx.Laundry.APIService]]) 

看到上面的 -1972395312 了吗?我去,int 类型的 size 直接给爆掉了,果然是个大对象,就是你了。。。如果非要看大小也可以,写一个脚本遍历一下。

三:总结

知道是 List 做的孽后,仔细阅读了源码才知道,原来是给用户第一次数据全量同步的时候,服务端为了加速将数据缓存在 List 这个静态变量中,很遗憾的是并没有在合适的时机进行释放,造成了高峰期内存直线暴增,优化方案很简单,就是修改业务逻辑咯,增加释放内存的时机。

题外话

如果你遇到的是这种 Strong Handles 的静态变量,也可以直接用可视化的 dotMemory 查看。

当然你要保证你有足够的内存,毕竟也算是小10G的dump ??, 我的 16G 内存一下子就被吃掉了。。。

善于用 String 驻留池机制来优化,看看它的源码定义吧。

  1. public sealed class String 
  2.    { 
  3.        [SecuritySafeCritical] 
  4.        public static string Intern(string str) 
  5.        { 
  6.            if (str == null
  7.            { 
  8.                throw new ArgumentNullException("str"); 
  9.            } 
  10.            return Thread.GetDomain().GetOrInternString(str); 
  11.        } 
  12.    } 

 

责任编辑:武晓燕 来源: 一线码农聊技术
相关推荐

2022-10-25 14:17:01

.NET代码程序

2023-07-06 10:11:38

.NET模式dump

2024-09-14 10:28:56

.NET卡死程序

2023-07-31 22:29:20

CPU.NETAPI

2024-07-12 11:20:34

.NET崩溃视觉程序

2021-11-02 07:54:41

内存.NET 系统

2021-04-21 07:38:41

CPU游戏站程序

2024-05-28 10:18:30

WPF程序数据

2021-10-09 10:24:08

NET爬虫内存

2023-04-06 10:52:18

2024-03-28 12:56:36

2023-04-26 12:48:58

.NET程序类型

2023-06-26 00:12:46

2022-01-17 21:28:36

管理系统.NET

2023-05-15 11:15:50

.NET门诊语句

2024-07-01 13:00:24

.NET网络边缘计算

2024-05-31 12:56:06

.NET代码方法

2023-10-07 13:28:53

.NET软件账本

2023-06-29 17:55:00

.NET日志WinDbg

2024-07-09 11:51:20

Windows线程池源码
点赞
收藏

51CTO技术栈公众号