韩亮 发表于 2024-1-20 09:12:30

记一次 .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]
查看完整版本: 记一次 .NET某道闸收费系统 内存溢出分析