记一次 .NET 某外贸Web站 内存泄漏分析
一:背景
1. 講故事
上周四有位朋友加wx咨詢他的程序內(nèi)存存在一定程度的泄漏,并且無(wú)法被GC回收,最終機(jī)器內(nèi)存耗盡,很尷尬。
溝通下來(lái),這位朋友能力還是很不錯(cuò)的,也已經(jīng)做了初步的dump分析,發(fā)現(xiàn)了托管堆上有 10w+ 的 byte[] 數(shù)組,并占用了大概 1.1G 的內(nèi)存,在抽取幾個(gè) byte[] 的 gcroot 后發(fā)現(xiàn)沒(méi)有引用,接下來(lái)就排查不下去了,雖然知道問(wèn)題可能在 byte[],但苦于找不到證據(jù)。????????????
那既然這么信任的找到我,我得要做一個(gè)相對(duì)全面的輸出報(bào)告,不能辜負(fù)大家的信任哈,還是老規(guī)矩,上 windbg 說(shuō)話。
二:windbg 分析
1. 排查泄漏源
看過(guò)我文章的老讀者應(yīng)該知道,排查這種內(nèi)存泄露的問(wèn)題,首先要二分法找出到底是托管還是非托管出的問(wèn)題,方便后續(xù)采取相應(yīng)的應(yīng)對(duì)措施。
接下來(lái)使用 !address -summary 看一下進(jìn)程的提交內(nèi)存。
||2:2:080>?!address?-summary---?Type?Summary?(for?busy)?------?RgnCount?-----------?Total?Size?--------?%ofBusy?%ofTotal MEM_PRIVATE?????????????????????????????573????????1`5c191000?(???5.439?GB)??95.19%????0.00% MEM_IMAGE??????????????????????????????1115????????0`0becf000?(?190.809?MB)???3.26%????0.00% MEM_MAPPED???????????????????????????????44????????0`05a62000?(??90.383?MB)???1.54%????0.00%---?State?Summary?----------------?RgnCount?-----------?Total?Size?--------?%ofBusy?%ofTotal MEM_FREE????????????????????????????????201?????7ffe`9252e000?(?127.994?TB)??????????100.00% MEM_COMMIT?????????????????????????????1477????????0`d439f000?(???3.316?GB)??58.04%????0.00% MEM_RESERVE?????????????????????????????255????????0`99723000?(???2.398?GB)??41.96%????0.00%從卦象的 MEM_COMMIT 指標(biāo)看:當(dāng)前只有 3.3G 的內(nèi)存占用,說(shuō)實(shí)話,我一般都建議 5G+ 是做內(nèi)存泄漏分析的最低門(mén)檻,畢竟內(nèi)存越大,越容易分析,接下來(lái)看一下托管堆的內(nèi)存占用。
||2:2:080>?!eeheap?-gc Number?of?GC?Heaps:?1 generation?0?starts?at?0x00000002b37c0c48 generation?1?starts?at?0x00000002b3781000 generation?2?starts?at?0x0000000000cc1000------------------------------ GC?Heap?Size:????????????Size:?0xbd322bb0?(3174181808)?bytes.可以看到,當(dāng)前托管堆占用 3174181808/1024/1024/1024= 2.95G,哈哈,看到這個(gè)數(shù),心里一陣狂喜,托管堆上的問(wèn)題,對(duì)我來(lái)說(shuō)差不多就十拿九穩(wěn)了。。。畢竟還沒(méi)有失手過(guò),接下來(lái)趕緊排查一下托管堆,看下是哪里出的問(wèn)題。
2. 查看托管堆
要想查看托管堆,可以使用 !dumpheap -stat 命令,下面我把 Top10 Size 給顯示出來(lái)。
||2:2:080>?!dumpheap?-stat Statistics:MT????Count????TotalSize?Class?Name 00007ffd7e130ab8???116201?????13014512?Newtonsoft.Json.Linq.JProperty 00007ffdd775e560????66176?????16411648?System.Data.SqlClient._SqlMetaData 00007ffddbcc9da8????68808?????17814644?System.Int32[] 00007ffddbcaf788????14140?????21568488?System.String[] 00007ffddac72958????50256?????22916736?System.Net.Sockets.SocketAsyncEventArgs 00007ffd7deb64b0??????369?????62115984?System.Collections.Generic.Dictionary`2+Entry[[System.Reflection.ICustomAttributeProvider,?mscorlib],[System.Type,?mscorlib]][] 00007ffddbcc8610?????8348????298313756?System.Char[] 00007ffddbcc74c0??1799807????489361500?System.String 000000000022e250???312151????855949918??????Free 00007ffddbccc768???109156???1135674368?System.Byte[]從上面的輸出中可以看到,當(dāng)前狀元是 Byte[],榜眼是 Free,探花是 String,這里還是有一些經(jīng)驗(yàn)之談的,深究 Byte[] 和 String 這種基礎(chǔ)類(lèi)型,投入產(chǎn)出比是不高的,畢竟大量的復(fù)雜類(lèi)型,它的內(nèi)部結(jié)構(gòu)都含有 String 和 Byte[],比如我相信 MemoryStream 內(nèi)部肯定有 Byte[],對(duì)吧,所以暫且放下?tīng)钤吞交?#xff0c;看一下榜眼或者其他的復(fù)雜類(lèi)型。
如果你的眼睛犀利,你會(huì)發(fā)現(xiàn) Free 的個(gè)數(shù)有 31W+,你肯定想問(wèn)這是什么意思?對(duì),這表明當(dāng)前托管堆上有 31W+ 的空閑塊,它的專(zhuān)業(yè)術(shù)語(yǔ)叫 碎片化,所以這條信息透露出了當(dāng)前托管堆有相對(duì)嚴(yán)重的碎片化現(xiàn)象,接下來(lái)的問(wèn)題就是為什么會(huì)這樣?大多數(shù)情況出現(xiàn)這種碎片化的原因在于托管堆上有很多的 pinned 對(duì)象,這種對(duì)象可以阻止 GC 在回收時(shí)對(duì)它的移動(dòng),長(zhǎng)此以往就會(huì)造成托管堆的支離破碎,所以找出這種現(xiàn)象對(duì)解決泄漏問(wèn)題有很大的幫助。
補(bǔ)充一下,這里可以借助 dotmemory ,紅色表示 pinned 對(duì)象,肉眼可見(jiàn)的大量的紅色間隔分布,最后的碎片率為 85% 。
接下來(lái)的問(wèn)題是如何找到這些 pinned 對(duì)象,其實(shí)在 CLR 中有一張 GCHandles 表,里面就記錄了這些玩意。
3. 查看 GCHandles
要想找到所有的 pinned 對(duì)象,可以使用 !gchandles -stat 命令,簡(jiǎn)化輸出如下:
||2:2:080>?!gchandles?-stat Statistics:MT????Count????TotalSize?Class?Name 00007ffddbcc88a0??????278????????26688?System.Threading.Thread 00007ffddbcb47a8?????1309???????209440?System.RuntimeType+RuntimeTypeCache 00007ffddbcc7b38??????100???????348384?System.Object[] 00007ffddbc94b60?????9359???????673848?System.Reflection.Emit.DynamicResolver 00007ffddb5b7b98????25369??????2841328?System.Threading.OverlappedData Total?36566?objectsHandles:Strong?Handles:???????174Pinned?Handles:???????15Async?Pinned?Handles:?25369Ref?Count?Handles:????1Weak?Long?Handles:????10681Weak?Short?Handles:???326從卦象中可以看出,當(dāng)前有一欄為:Async Pinned Handles: 25369 ,這表示當(dāng)前有 2.5w 的異步操作過(guò)程中被pinned住的對(duì)象,這個(gè)指標(biāo)就相當(dāng)不正常了,而且可以看出與 2.5W 的System.Threading.OverlappedData 遙相呼應(yīng),有了這個(gè)思路,可以回過(guò)頭來(lái)看一下托管堆,是否有相對(duì)應(yīng)的 2.5w 個(gè)類(lèi)似封裝過(guò)異步操作的復(fù)雜類(lèi)型對(duì)象?這里我再把 top10 Size 的托管堆列出來(lái)。
||2:2:080>?!dumpheap?-stat Statistics:MT????Count????TotalSize?Class?Name 00007ffd7e130ab8???116201?????13014512?Newtonsoft.Json.Linq.JProperty 00007ffdd775e560????66176?????16411648?System.Data.SqlClient._SqlMetaData 00007ffddbcc9da8????68808?????17814644?System.Int32[] 00007ffddbcaf788????14140?????21568488?System.String[] 00007ffddac72958????50256?????22916736?System.Net.Sockets.SocketAsyncEventArgs 00007ffd7deb64b0??????369?????62115984?System.Collections.Generic.Dictionary`2+Entry[[System.Reflection.ICustomAttributeProvider,?mscorlib],[System.Type,?mscorlib]][] 00007ffddbcc8610?????8348????298313756?System.Char[] 00007ffddbcc74c0??1799807????489361500?System.String 000000000022e250???312151????855949918??????Free 00007ffddbccc768???109156???1135674368?System.Byte[]有了這種先入為主的思想,我想你肯定發(fā)現(xiàn)了托管堆上的這個(gè) 50256 的 System.Net.Sockets.SocketAsyncEventArgs,看樣子這回泄漏和 Socket 脫不了干系了,接下來(lái)可以查下這些 SocketAsyncEventArgs 到底被誰(shuí)引用著?
4. 查看 SocketAsyncEventArgs 引用根
要想查看引用根,先從 SocketAsyncEventArgs 中導(dǎo)幾個(gè) address 出來(lái)。
||2:2:080>?!dumpheap?-mt?00007ffddac72958?0?0000000001000000Address???????????????MT?????Size 0000000000cc9dc0?00007ffddac72958??????456????? 0000000000ccc0d8?00007ffddac72958??????456????? 0000000000ccc358?00007ffddac72958??????456????? 0000000000cce670?00007ffddac72958??????456????? 0000000000cce8f0?00007ffddac72958??????456????? 0000000000cd0c08?00007ffddac72958??????456????? 0000000000cd0e88?00007ffddac72958??????456????? 0000000000cd31a0?00007ffddac72958??????456????? 0000000000cd3420?00007ffddac72958??????456????? 0000000000cd5738?00007ffddac72958??????456????? 0000000000cd59b8?00007ffddac72958??????456????? 0000000000cd7cd0?00007ffddac72958??????456?????然后查看第一個(gè)和第二個(gè)address的引用根。
||2:2:080>?!gcroot?0000000000cc9dc0 Thread?86e4:0000000018ecec20?00007ffd7dff06b4?xxxHttpServer.DaemonThread`2[[System.__Canon,?mscorlib],[System.__Canon,?mscorlib]].DaemonThreadStart()rbp+10:?0000000018ececb0->??000000000102e8c8?xxxHttpServer.DaemonThread`2[[xxx.xxx,?xxx],[xxx.RequestInfo,?xxx]]->??00000000010313a8?xxxHttpServer.xxxHttpRequestServer`2[[xxx.xxx,?xxx],[xxx.RequestInfo,?xxx]]->??000000000105b330?xxxHttpServer.HttpSocketTokenPool`2[[xxx.xxx,?xxx],[xxx.RequestInfo,?xxx]]->??000000000105b348?System.Collections.Generic.Stack`1[[xxxHttpServer.HttpSocketToken`2[[xxx.xxx,?xxx],[xxx.RequestInfo,?xxx]],?xxxHttpServer]]->??0000000010d36178?xxxHttpServer.HttpSocketToken`2[[xxx.xxx,?xxx],[xxx.RequestInfo,?xxx]][]->??0000000008c93588?xxxHttpServer.HttpSocketToken`2[[xxx.xxx,?xxx],[xxx.RequestInfo,?xxx]]->??0000000000cc9dc0?System.Net.Sockets.SocketAsyncEventArgs ||2:2:080>?!gcroot?0000000000ccc0d8 Thread?86e4:0000000018ecec20?00007ffd7dff06b4?xxxHttpServer.DaemonThread`2[[System.__Canon,?mscorlib],[System.__Canon,?mscorlib]].DaemonThreadStart()rbp+10:?0000000018ececb0->??000000000102e8c8?xxxHttpServer.DaemonThread`2[[xxx.xxx,?xxx],[xxx.RequestInfo,?xxx]]->??00000000010313a8?xxxHttpServer.xxxHttpRequestServer`2[[xxx.xxx,?xxx],[xxx.RequestInfo,?xxx]]->??000000000105b330?xxxHttpServer.HttpSocketTokenPool`2[[xxx.xxx,?xxx],[xxx.RequestInfo,?xxx]]->??000000000105b348?System.Collections.Generic.Stack`1[[xxxHttpServer.HttpSocketToken`2[[xxx.xxx,?xxx],[xxx.RequestInfo,?xxx]],?xxxHttpServer]]->??0000000010d36178?xxxHttpServer.HttpSocketToken`2[[xxx.xxx,?xxx],[xxx.RequestInfo,?xxx]][]->??0000000000ccc080?xxxHttpServer.HttpSocketToken`2[[xxx.xxx,?xxx],[xxx.RequestInfo,?xxx]]->??0000000000ccc0d8?System.Net.Sockets.SocketAsyncEventArgs從輸出信息看,貌似程序自己搭了一個(gè) HttpServer,還搞了一個(gè) HttpSocketTokenPool 池,好奇心來(lái)了,把這個(gè)類(lèi)導(dǎo)出來(lái)看看怎么寫(xiě)的?
5. 尋找問(wèn)題代碼
還是老辦法,使用 !savemodule 導(dǎo)出問(wèn)題代碼,然后使用 ILSpy 進(jìn)行反編譯。
說(shuō)實(shí)話,這個(gè) pool 封裝的挺簡(jiǎn)陋的,既然 SocketAsyncEventArgs 有 5W+,我猜測(cè)這個(gè) m_pool 池中估計(jì)也得好幾萬(wàn),為了驗(yàn)證思路,可以用 windbg 把它挖出來(lái)。
從圖中的size可以看出,這個(gè) pool 有大概 2.5w 的 HttpSocket,這就說(shuō)明這個(gè)所謂的 Socket Pool 其實(shí)并沒(méi)有封裝好。
三:總結(jié)
想自己封裝一個(gè)Pool,得要實(shí)現(xiàn)一些復(fù)雜的邏輯,而不能僅僅是一個(gè) PUSH 和 POP 就完事了。。。所以優(yōu)化方向也很明確,想辦法控制住這個(gè) Pool,實(shí)現(xiàn) Pool 該實(shí)現(xiàn)的效果。
END
工作中的你,是否已遇到 ...?
1. CPU爆高
2. 內(nèi)存暴漲
3. 資源泄漏
4. 崩潰死鎖
5. 程序呆滯
等緊急事件,全公司都指望著你能解決...? 危難時(shí)刻才能展現(xiàn)你的技術(shù)價(jià)值,作為專(zhuān)注于.NET高級(jí)調(diào)試的技術(shù)博主,歡迎微信搜索: 一線碼農(nóng)聊技術(shù),免費(fèi)協(xié)助你分析Dump文件,希望我能將你的踩坑經(jīng)驗(yàn)分享給更多的人。
總結(jié)
以上是生活随笔為你收集整理的记一次 .NET 某外贸Web站 内存泄漏分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: TLS是如何保障数据传输安全(中间人攻击
- 下一篇: ML.NET Cookbook:(3)如