天天看點

記一次 .NET 某WMS倉儲打單系統 記憶體暴漲分析

七月中旬有一位朋友加wx求助,他的程式在生産上跑着跑着記憶體就飙起來了,貌似沒有回頭的趨勢,詢問如何解決,截圖如下:

記一次 .NET 某WMS倉儲打單系統 記憶體暴漲分析

和這位朋友聊下來,感覺像是自己在小縣城當了個小老闆,規律的生活,有當地資源,各種小關系,有一股财務自由的味道,這也是我一直向往的生活方式 😄😄😄。

既然朋友找到我了,我得想辦法給他解決問題,既然是記憶體暴漲,我就賭一把在托管層面吧,嘿嘿,上windbg說話。

一直在追這個系列的朋友應該知道,我無數次的用 <code>!address -summary</code> 和 <code>!eeheap -gc</code> 這兩個指令來判斷目前記憶體屬于托管層還是非托管層。

卦象上程序名額為 <code>3.98G</code> ,GC堆名額為 <code>3209543552 = 3G</code> ,很顯然,本次事故屬于 <code>托管層面</code>。

我們都知道C#是托管語言,是以甭管有用沒用的對象都逃不出GC堆,言外之意就是看GC堆準沒錯,挑幾個大對象看看。

從托管堆上看,<code>System.Linq.Expressions.MemberAssignment</code> 對象高達 498w ,很明顯有問題,從類名看可能和 ExpressionTree 有關,那就抽幾個對象看看它的引用鍊上是否有過大的對象。

引用鍊特别長,這裡我就截取一下,經過一頓排查,我發現大對象居然是 <code>Remotion.Linq.Clauses.SelectClause</code>,objsize 這個對象直接爆掉了,真的很奇葩,如下代碼所示:

有點懵,這個對象居然是罪魁禍首,從引用鍊看它是 EF 下的一個建構表達式樹的小部件,可以肯定的是,朋友在用EF的時候出了什麼問題,不過還得硬着頭皮繼續挖 SelectClause,經過深挖,我發現這個類有大量這樣的 <code>char[]</code> 數組,導出來後大概是下面這樣。

從内容看,應該是 select 語句的ExpressionTree表示,問了下朋友,說大概是報表業務,不過這些資訊給他貌似也沒有多大幫助,說實話到這裡我其實也不知道怎麼繼續往下排查了,陷入了絕望。

我在想,既然EF建構了大量這樣的 ExpressionTree,肯定有問題,但也想不出是什麼問題,隔了半天,我突然靈光一現,EF既然建構了樹,有可能sql也出來了,對,我何不直接在 heap 上搜尋 select 的sql語句。。。。

果然發現了大量重複的 select 語句,而且從最左邊的記憶體位址看都是非常接近的,也就說明他們是在某一個操作中同時生成的,然後我們導出幾個sql語句。

拿到這 <code>1.8w</code> 重複的sql 給朋友看,朋友說這是查詢報表的sql。

那這裡就存在着很大問題,既然是查詢報表,為什麼會有 1.8w 相同的sql,唯一不同的就是 <code>a."OrderNumber" = @__pagination_OrderNumber_0</code> 中的訂單号,難道不應該是 <code>a.OrderNumber in (xxxx)</code> 或者是表關聯查詢嗎??? 整理一下就是下面這樣的猜想:

加上每個sql記憶體位址相近,再結合爆表的 <code>Remotion.Linq.Clauses.SelectClause</code> 對象,整個流程大概就是:本該表關聯或者in操作,結果變成了無數個單條sql語句查詢,導緻EF底層出現記憶體爆炸式增長。

看了下朋友查詢ef的寫法,猜測大多都是人肉建構 ExpressionTree 去查詢資料庫,大寫的🐂👃,比如下面的這張圖:

記一次 .NET 某WMS倉儲打單系統 記憶體暴漲分析

解決方案就是讓朋友檢查下表示式樹的寫法問題,或者直接灌寫好的sql得了,說實話這個dump還是費了九牛二虎之力,本以為很簡單,實操起來還是碰到了一點小困難,就當曆練成長吧!

更多高品質幹貨:參見我的 GitHub: dotnetfly

記一次 .NET 某WMS倉儲打單系統 記憶體暴漲分析