记一次 .NET 某三甲医院HIS系统 内存暴涨分析
一:背景
1. 讲故事
前几天有位朋友加wx说他的程序遭遇了内存暴涨,求助如何分析?
和这位朋友聊下来,这个dump也是取自一个HIS系统,如朋友所说我这真的是和医院杠上了,这样也好,给自己攒点资源,好了,不扯了,上windbg说话。
二: windbg 分析
1. 托管还是非托管?
既然是内存暴涨,那就看看当前进程的 commit 内存有多大?
0:000> !address -summary
--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE 174 7ffe`baac0000 ( 127.995 TB) 100.00%
MEM_COMMIT 1153 1`33bd3000 ( 4.808 GB) 94.59% 0.00%
MEM_RESERVE 221 0`1195d000 ( 281.363 MB) 5.41% 0.00%
可以看出大概占了 4.8G
,接下来再看看托管堆内存。
0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00000207a4fc48c8
generation 1 starts at 0x00000207a3dc3138
generation 2 starts at 0x0000020697fc1000
ephemeral segment allocation context: none
------------------------------
GC Heap Size: Size: 0x1241b3858 (4900730968) bytes.
从最后一行可以看出托管堆占用 4900730968/1024/1024/1024=4.5G
,两个指标一比对,原来是托管内存出问题了,这下好办了。。。
2. 查看托管堆
既然内存是被托管堆吃掉了,那就看看托管堆上到底都有些什么东西???
0:000> !dumpheap -stat
Statistics:
MT Count TotalSize Class Name
...
00007ffd00397b98 1065873 102323808 System.Data.DataRow
00000206978b8250 1507805 223310768 Free
00007ffd20d216b8 4668930 364025578 System.String
00007ffd20d22aa8 797 403971664 System.String[]
00007ffd20d193d0 406282 3399800382 System.Byte[]
Total 9442152 objects
不看不知道,一看吓一跳,System.Byte[]
差不多占用了 3.3 G 内存,也就是说 gc 堆差不多都被它吃掉了,根据经验肯定是有个什么大对象,那接下来怎么分析呢?除了用脚本对 byte[]
进行暴力分组统计之外,纯人肉还有其他的技巧吗? 当然有,可以用 !heapstat
观察下这些对象在托管堆上的代信息。
0:000> !heapstat
Heap Gen0 Gen1 Gen2 LOH
Heap0 2252000 18880400 3968704192 910894376
Free space: Percentage
Heap0 43128 770160 185203264 39849984SOH: 4% LOH: 4%
从图中可以看出,当前的大头在 Gen2 上,接下来可以用 eeheap -gc
去找 Gen2 的段地址区间,从而最小化的显示heap上内容。
0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00000207a4fc48c8
generation 1 starts at 0x00000207a3dc3138
generation 2 starts at 0x0000020697fc1000
ephemeral segment allocation context: none
segment begin allocated size
0000020697fc0000 0000020697fc1000 00000206a7fbec48 0xfffdc48(268426312)
00000206bbeb0000 00000206bbeb1000 00000206cbeaef50 0xfffdf50(268427088)
00000206ccc40000 00000206ccc41000 00000206dcc3f668 0xfffe668(268428904)
00000206dcc40000 00000206dcc41000 00000206ecc3f098 0xfffe098(268427416)
0000020680000000 0000020680001000 000002068ffff8c0 0xfffe8c0(268429504)
00000206ff4d0000 00000206ff4d1000 000002070f4cf588 0xfffe588(268428680)
000002070f4d0000 000002070f4d1000 000002071f4cf9f0 0xfffe9f0(268429808)
000002071f4d0000 000002071f4d1000 000002072f4cfef0 0xfffeef0(268431088)
000002072f4d0000 000002072f4d1000 000002073f4cf748 0xfffe748(268429128)
000002073f4d0000 000002073f4d1000 000002074f4ce900 0xfffd900(268425472)
00000207574d0000 00000207574d1000 00000207674cfe70 0xfffee70(268430960)
00000207674d0000 00000207674d1000 00000207774ceaf8 0xfffdaf8(268425976)
00000207774d0000 00000207774d1000 00000207874cf270 0xfffe270(268427888)
00000207874d0000 00000207874d1000 00000207974cf7a8 0xfffe7a8(268429224)
00000207974d0000 00000207974d1000 00000207a51ea5a8 0xdd195a8(231839144)
一般来说,第一个 segment 是给 gen0 + gen1
的,后续的 segment 就是 gen2
,接下来我就选 segment: 00000206dcc41000 - 00000206ecc3f098
,然后使用 !dumpheap
导出该区间的所有对象。
0:000> !dumpheap -stat 00000206dcc41000 00000206ecc3f098
Statistics:
MT Count TotalSize Class Name
00007ffd00397b98 191803 18413088 System.Data.DataRow
00007ffd20d216b8 662179 37834152 System.String
00007ffd20d193d0 23115 187896401 System.Byte[]
从这个内存段上看,Byte[]
有 2.3w 个,还不算多,全部dump出来看看有什么特征。
0:000> !dumpheap -mt 00007ffd20d193d0 00000206dcc41000 00000206ecc3f098
Address MT Size
00000206dcc410e8 00007ffd20d193d0 8232
00000206dcc43588 00007ffd20d193d0 8232
00000206dcc45a48 00007ffd20d193d0 8232
00000206dcc47d78 00007ffd20d193d0 8232
00000206dcc4a028 00007ffd20d193d0 8232
00000206dcc4c4b0 00007ffd20d193d0 8232
00000206dcc4eb08 00007ffd20d193d0 8232
00000206dcc50e88 00007ffd20d193d0 8232
00000206dcc535b0 00007ffd20d193d0 8232
00000206dcc575d8 00007ffd20d193d0 8232
00000206dcc5a5a8 00007ffd20d193d0 8232
00000206dcc5cbf8 00007ffd20d193d0 8232
00000206dcc5eef8 00007ffd20d193d0 8232
00000206dcc611f8 00007ffd20d193d0 8232
00000206dcc634e8 00007ffd20d193d0 8232
00000206dcc657f0 00007ffd20d193d0 8232
00000206dcc67af8 00007ffd20d193d0 8232
00000206dcc69e00 00007ffd20d193d0 8232
...
我去,99% 都是 8232byte
,原来都是些 8k
的byte数组,那到底谁在使用它,用 !gcroot
查一下引用根。
0:000> !gcroot 00000206dcc410e8
Thread 8c1c:
rsi:
-> 00000206983d5730 System.ServiceProcess.ServiceBase[]
...
-> 000002069dcb6d38 OracleInternal.ConnectionPool.OraclePool
...
-> 000002069dc949c0 OracleInternal.TTC.OraBufReader
-> 000002069dc94a70 System.Collections.Generic.List`1[[OracleInternal.Network.OraBuf, Oracle.ManagedDataAccess]]
-> 00000206ab8c2200 OracleInternal.Network.OraBuf[]
-> 00000206dcc41018 OracleInternal.Network.OraBuf
-> 00000206dcc410e8 System.Byte[]
从引用链来看,貌似是被 OracleInternal.Network.OraBuf[]
持有着,这就很疑惑了,难道是 Oracle Sdk 出的bug把内存给搞崩了? 好奇心来了,看一下元素个数和size各是多少?
0:000> !do 00000206ab8c2200
Name: OracleInternal.Network.OraBuf[]
MethodTable: 00007ffcc7833c68
EEClass: 00007ffd20757728
Size: 4194328(0x400018) bytes
Array: Rank 1, Number of elements 524288, Type CLASS (Print Array)
Fields:
None
0:000> !objsize 00000206ab8c2200
sizeof(00000206ab8c2200) = -1086824024 (0xbf3861a8) bytes (OracleInternal.Network.OraBuf[])
当前数组有 52w ,totalsize直接负数了。
3. 寻找问题代码
知道现象之后,接下来用 ILSpy 把 Oracle SDK 反编译看看,最终一比对,如下图所示:
原来m_tempOBList
是内存暴涨的罪魁祸首,这就很尴尬了,它为什么会暴涨? 为什么不释放? 由于我对 Oracle 也不熟悉,只能求助于神奇的 StackOverflow,我去,还真有天涯沦落人,Huge managed memory allocation when reading (iterating) data with DbDataReader
大概是说这种现象是 Oracle SDK 在读取 Clob 类型的字段有一个bug,解决办法也很简单,用完后就释放,详情参见如下图:
4. 寻找真相
既然帖子上是说读取 Clob 类型出的问题,那就把所有线程栈都调出来,看看此时的线程栈中是否有 Clob 的踪影?
从线程栈上看,代码是通过 ToDataTable
方法将 IDataReader 转成 DataTable,在转换过程中读取了大字段,自然就有了 GetCompleteClobData
,也就是说完美命中帖子所说,为了让结论更准确,我就去挖一下当前的 DataReader 已经读了多少行了?
0:028> !clrstack -a
OS Thread Id: 0xbab0 (28)
000000e78ef7d520 00007ffd00724458 System.Data.DataTable.Load(System.Data.IDataReader, System.Data.LoadOption, System.Data.FillErrorEventHandler)
PARAMETERS:
this = <no data>
reader (<CLR reg>) = 0x00000206a530ac20
loadOption = <no data>
errorHandler = <no data>
0:028> !do 0x00000206a530ac20
Name: Oracle.ManagedDataAccess.Client.OracleDataReader
MethodTable: 00007ffcc7933b10
EEClass: 00007ffcc78efd30
Size: 256(0x100) bytes
File: D:\xxx.dll
Fields:
00007ffd20d23e98 4000337 d0 System.Int32 1 instance 1061652 m_RowNumber
从 m_RowNumber 看,已经读取了 106w 行,一次性读取100w+的记录不常见,如果还有大字段的话,那也是了。
三:总结
综合来看这次事故是因为一次性读取含有大字段的百万级数据到DataTable引发,解决方案很简单,自己通过 for 读取 DataReader,在处理完 OracleClob 类型之后马上释放,参考帖子代码:
var item = oracleDataReader.GetOracleValue(columnIndex);
if (item is OracleClob clob)
{
if (clob != null)
{
// use clob.Value ...
clob.Close();
}
}
更多高质量干货:参见我的 GitHub: dotnetfly
记一次 .NET 某三甲医院HIS系统 内存暴涨分析的更多相关文章
- 记一次 .NET 某WMS仓储打单系统 内存暴涨分析
一:背景 1. 讲故事 七月中旬有一位朋友加wx求助,他的程序在生产上跑着跑着内存就飙起来了,貌似没有回头的趋势,询问如何解决,截图如下: 和这位朋友聊下来,感觉像是自己在小县城当了个小老板,规律的生 ...
- 记一次 .NET 医院CIS系统 内存溢出分析
一:背景 1. 讲故事 前几天有位朋友加wx求助说他的程序最近总是出现内存溢出,很崩溃,如下图: 和这位朋友聊下来,发现他也是搞医疗的,哈哈,.NET 在医疗方面还是很有市场的,不过对于内存方面出的问 ...
- 记一次 .NET医疗布草API程序 内存暴涨分析
一:背景 1. 讲故事 我在年前写过一篇关于CPU爆高的分析文章 再记一次 应用服务器 CPU 暴高事故分析 ,当时是给同济做项目升级,看过那篇文章的朋友应该知道,最后的结论是运维人员错误的将 IIS ...
- 记一次 .NET 某招聘网后端服务 内存暴涨分析
一:背景 1. 讲故事 前段时间有位朋友wx找到我,说他的程序存在内存阶段性暴涨,寻求如何解决,和朋友沟通下来,他的内存平时大概是5G 左右,在某些时点附近会暴涨到 10G+, 画个图大概就是这样. ...
- 记一次 .NET 某医院HIS系统 CPU爆高分析
一:背景 1. 讲故事 前几天有位朋友加 wx 抱怨他的程序在高峰期总是莫名其妙的cpu爆高,求助如何分析? 和这位朋友沟通下来,据说这问题困扰了他们几年,还请了微软的工程师过来解决,无疾而终,应该还 ...
- 记一次 .NET 某智能服装智造系统 内存泄漏分析
一:背景 1. 讲故事 上个月有位朋友找到我,说他的程序出现了内存泄漏,不知道如何进一步分析,截图如下: 朋友这段话已经说的非常言简意赅了,那就上 windbg 说话吧. 二:Windbg 分析 1. ...
- 记一次 WinDbg 分析 .NET 某工厂MES系统 内存泄漏分析
一:背景 1. 讲故事 上个月有位朋友加微信求助,说他的程序跑着跑着就内存爆掉了,寻求如何解决,截图如下: 从聊天内容看,这位朋友压力还是蛮大的,话说这貌似是我分析的第三个 MES 系统了,看样子 . ...
- 记一次 .NET 某消防物联网 后台服务 内存泄漏分析
一:背景 1. 讲故事 去年十月份有位朋友从微信找到我,说他的程序内存要炸掉了...截图如下: 时间有点久,图片都被清理了,不过有点讽刺的是,自己的程序本身就是做监控的,结果自己出了问题,太尴尬了 二 ...
- 记一次 .NET 某电厂Web系统 内存泄漏分析
一:背景 1. 讲故事 前段时间有位朋友找到我,说他的程序内存占用比较大,寻求如何解决,截图就不发了,分析下来我感觉除了程序本身的问题之外,.NET5 在内存管理方面做的也不够好,所以有必要给大家分享 ...
随机推荐
- 这一次,彻底搞懂 Go Cond
hi,大家好,我是 haohongfan. 本篇文章会从源码角度去深入剖析下 sync.Cond.Go 日常开发中 sync.Cond 可能是我们用的较少的控制并发的手段,因为大部分场景下都被 Cha ...
- C#入门到精通系列课程——第2章编写C#程序
◆本章内容 (1)熟悉Visual Studio 2017开发环境 (2)编写第一个C#程序 (3)C#程序结构预览 (4)程序编写规范 (5)难点解答 ◆本章简述 要学习C#编程,必然要熟悉C#程序 ...
- C#搞个跨平台的桌面NES游戏模拟器
支持Windows,Mac,Linux NES模拟器内核源码来自 https://github.com/colinvella/EmuNes 他这边的源码功能很完善了的,支持视频录制,手柄,金 ...
- 把握好集成测试大关,ERP就成功了一大半
欢迎关注微信公众号:sap_gui (ERP咨询顾问之家) 前段时间收到一个朋友的信息,说他们目前正在实施ERP系统,已经到了集成测试环节了,但整个测试过程下来并不是太理想,很多接口不通,功能也还在开 ...
- vuex 引用方法
引入Vuex(前提是已经用Vue脚手架工具构建好项目) 1.利用npm包管理工具,进行安装 vuex.在控制命令行中输入下边的命令就可以了. npm install vuex --save 要注意的是 ...
- Django Ajax序列化与反序列化
序列化与反序列是最常用的功能,有时我们需要将一个表单组打包成Json格式等然后再提交给服务端,这样可以提高效率节约带框,如下是Django配合Ajax实现的序列化与反序列化,文件上传等操作. Ajax ...
- hdu4118
题意: 给你一颗无向带权树,每个定点上有一个人,问所有定点都不在自己位置上的最长路径总和是多少.. 思路: 其实很简单,贪心的想下,既然要求全局最大,那么对于每一条边用的次 ...
- Linux中的SSH服务
目录 SSH 使用scp在两台Linux间传数据: 基于SSH做远程访问,可以使用ftp服务的相关指令sftp root@192.168.10.10 Openssh公私钥验证: SSH SSH(Sec ...
- Python脚本抓取京东手机的配置信息
以下代码是使用python抓取京东小米8手机的配置信息 首先找到小米8商品的链接:https://item.jd.com/7437788.html 然后找到其配置信息的标签,我们找到其配置信息的标签为 ...
- 手脱UPX3.91壳(练习)
0x01 准备 OD UPX加壳程序 可以加壳的软件 0x02 给软件加壳 我找了半天发现winhex不错,而且是没壳的可以直接加壳 1.复制一份可执行文件 将赋值好的文件用UPX3.91加壳 0x0 ...