记一次 .NET某道闸收费系统 内存溢出分析
一:背景1. 讲故事
前些天有位朋友找到我,说他的程序几天内存就要爆一次,不知道咋回事,找不出原因,让我帮忙看一下,这种问题分析dump是最简单粗暴了,拿到dump后接下来就是一顿分析。
二:WinDbg 分析
1. 程序为什么会暴
程序既然会爆,可能是虚拟地址受限,也可能是系统内存不足,可以用 !address -summary 观察下。
0:037> !address -summary
--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
<unknown> 866 53577000 ( 1.302 GB)69.38% 65.11%
Image 2244 16ee2000 ( 366.883 MB)19.09% 17.91%
Heap 222 8adc000 ( 138.859 MB) 7.23% 6.78%
Free 460 7e14000 ( 126.078 MB) 6.16%
Stack 255 5150000 (81.312 MB) 4.23% 3.97%
TEB 85 db000 ( 876.000 kB) 0.04% 0.04%
Other 20 79000 ( 484.000 kB) 0.02% 0.02%
PEB 1 3000 (12.000 kB) 0.00% 0.00%
...
--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_COMMIT 2900 64906000 ( 1.571 GB)83.72% 78.57%
MEM_RESERVE 793 138d6000 ( 312.836 MB)16.28% 15.28%
MEM_FREE 460 7e14000 ( 126.078 MB) 6.16%
...从卦中可以明显的看出,这又是一例经典的32bit程序受到了2G的内存限制,按往期经验来说解决办法比较简单,改成大地址或者x64即可。
哈哈,既然要分享这篇,自然就不是这么简单的事情,这需要我们排查这个溢出是不是程序的bug导致的,如果是那还得继续找原因。
2. 是程序bug导致的吗
要想搞清楚这个问题,需要去分析各处的内存占用,比如托管堆,可以用 !eeheap -gc 观察。
0:037> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x49fd10a8
generation 1 starts at 0x49fd1000
generation 2 starts at 0x03381000
ephemeral segment allocation context: none
segment beginallocated size
03380000033810000437ff880xffef88(16773000)
23e6000023e6100024e5ff880xffef88(16773000)
0b5100000b5110000c50ff880xffef88(16773000)
...
7be200007be210007cbbdb600xd9cb60(14273376)
49fd000049fd10004afcfe080xffee08(16772616)
Large object heap starts at 0x04381000
segment beginallocated size
043800000438100004a67b500x6e6b50(7236432)
Total Size: Size: 0x39738ad4 (963873492) bytes.
------------------------------
GC Heap Size: Size: 0x39738ad4 (963873492) bytes.从卦中可以看到,托管堆占用963M,并且产生了很多的16M的segment,这就表明当前的托管堆吃掉了内存,接下来的问题是为什么托管堆吃了那么多的内存呢?那就只能用 !dumpheap -stat 去观察下托管堆的对象布局咯。
0:037> !dumpheap -stat
Statistics:
MT Count TotalSize Class Name
...
717c8b4c 264594 11642136 System.Threading.ExecutionContext
717cd044 265930 13034088 System.Collections.Hashtable+bucket[]
717ccff4 265854 13824408 System.Collections.Hashtable
71761c34 268005 17152320 System.Threading.OverlappedData
70d73c10 264469 26446900 System.Net.Sockets.OverlappedAsyncResult
717cdd04 280225 293649193 System.Byte[]
013a9f98 269886 540566904 Free
Total 3880354 objects从卦中可以看到当前托管堆有 26.8w 的 OverlappedData 对象,这是一个非常明显的异常信号,熟悉这块的朋友应该知道,这个东西常常和异步打交道,也就表示当前程序可能有高达 26.8w 的异步请求可能没有得到响应,要想找到这个答案,就需要对 OverlappedData 进行穿刺。
3. OverlappedData 穿刺检查
对 OverlappedData 穿刺的目的就是要活检内部的 AsyncCallback 回调函数,看看到底是良性还是恶性的,相关命令如下:
0:037> !dumpheap -stat
...
34f38ac4 71761c34 64
34f39088 71761c34 64
...
0:037> !mdt 34f39088
34f39088 (System.Threading.OverlappedData)
m_asyncResult:33e8aafc (System.Net.Sockets.OverlappedAsyncResult)
m_iocb:03c077a0 (System.Threading.IOCompletionCallback)
...
m_nativeOverlapped:(System.Threading.NativeOverlapped) VALTYPE (MT=7176dfe0, ADDR=34f390b0)
0:037> !mdt 33e8aafc
33e8aafc (System.Net.Sockets.OverlappedAsyncResult)
m_AsyncObject:03c71d44 (System.Net.Sockets.Socket)
m_AsyncState:33e8aaec (xxx)
m_AsyncCallback:03e8f214 (System.AsyncCallback)
...
0:037> !mdt 03e8f214
03e8f214 (System.AsyncCallback)
_target:03c065a8 (xxx)
_methodPtr:19432480 (System.IntPtr)
0:037> u 19432480
19432480 e933932102 jmp 1b64b7b8
19432485 5f pop edi
...
0:037> !ip2md 1b64b7b8
MethodDesc: 131605ac
Method Name:xxxDevices.ReceiveCallback(System.IAsyncResult)卦中的信息量还是蛮大的,可以看到这是一个和 Socket 相关的异步函数,并且也成功找到了 xxxDevices.ReceiveCallback 回调函数,接下来就是检查下这个方法附近的业务逻辑,由于代码会涉及到一些隐私,我就多模糊一点,请见谅,截图如下:
仔细阅读这段代码,他是想用异步的方式一次次的用byte去丈量一段可能的大数据,直到这个 Stream 不能再读了,所以用了 if (stream.CanRead) 判断。
对 Socket 编程比较熟悉的朋友相信很快就能发现问题,判断 Stream 中的数据是否读完应该用 DataAvailable 属性,而不是 CanRead,比如下面这段正确的代码:
最后再贴VS中对 CanRead 和 DataAvailable 属性的解释。
//
// Summary:
// Gets a value that indicates whether the System.Net.Sockets.NetworkStream supports
// reading.
//
// Returns:
// true if data can be read from the stream; otherwise, false. The default value
// is true.
public override bool CanRead { get; }
//
// Summary:
// Gets a value that indicates whether data is available on the System.Net.Sockets.NetworkStream
// to be read.
//
// Returns:
// true if data is available on the stream to be read; otherwise, false.
//
public virtual bool DataAvailable { get; }三:总结
这个事故非常有意思,一个简简单单的 CanRead 误用就对程序造成了毁灭性的打击,这也警示大家在用某个属性某个方法前,一定要先搞清楚它到底是怎么玩的。
来源:https://www.cnblogs.com/huangxincheng/p/17972069
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页:
[1]