OutOfMemoryException異常解析

一、概述

在國慶休假快結束的最後一天晚上接到了部門老大的電話,某省的服務會出現崩潰問題。需要趕緊修復,沒錯這次的主角依舊是上次的「遠古項目」沒有辦法同事都在休假沒有人能幫忙開電腦遠程只能打車去公司。遠程鏈接上服務器之後查看日誌發現拋出的堆棧異常信息中包含了這樣一句話「OutOfMemoryException」,在A.dll中。

這個「遠古項目」大致情況如下:

  • 1.框架版本為.net framework 4
  • 2.代碼結構混亂
  • 3.需要通過socket連接大量的物理設備採集數據(1000台左右)
  • 4.每小時採集一次,並由「遠古項目」接收並轉發

二、問題分析

(1)根據日誌定位問題

其實日誌中能給定的錯誤信息有限,但是也有很大幫助起碼知道問題出在哪一塊。這時候直接找到A類庫查看源碼,這時候發現項目當中這一塊代碼非常多大約1000行左右,這麼多代碼到底哪一句出了問題不得而知。同時如果我想復現的話並不能有那麼多的設備去模擬測試。這時候其實是有點暈的,這時候只能硬着頭皮把「OutOfMemoryException」這個異常拿去google一樣,結果發現是線程方面的內存溢出問題。那麼這時候縮小了查看代碼的範圍,就開始在代碼中搜索Thread對象的使用。查看了半天果然有發現,看到了如下一段代碼:

//...代碼上下文
byte[] sendbytes = new byte[] { xxx };
var thread = new Thread((_) =>
{
   Send(sendbytes);
});
thread.Start();
//...代碼上下文

這段代碼存在的地方大概是,在所有設備在每小時採集一次數據的時候會集中在某一個時間段里大量的發送數據若干次,且每台設備每次發送數據的時候都會創建線程去發送數據。看到這一段代碼的時候我人都麻了。

(2)根據問題代碼繼續分析

在程序開發中,創建線程的代價是非常高昂的。而且都集中在一個時間點上去頻繁創建線程這樣的代碼肯定不行。這段代碼極有可能就是引發這個異常的原因之一。分析到這裡突然想起之前看過的一本書,書中描述了這樣一段話:

「線程棧往往都很小。windows上默認情況下棧最大為1MB,並且大多數線程通常只使用很少的棧頁(stack page)。….」(書名:.NET性能分析)

基於以上理論增加了那段代碼引起崩潰的可能性懷疑。那麼只能抱着試一試的心態繼續往下做。

三、解決方案

那麼發現了可能導致異常的代碼如何去解決呢?這時候又有點頭疼了,因為我暫時能想到的解是:

Answer:利用生產消費者模式建立發送隊列,然後開啟一個常駐的發送線程慢慢發就可以了。

但是問題來了,「遠古項目」中結構太過混亂牽一髮動全身的連最簡單的這種思路實現都會有很多阻礙。而且框架版本過於低一些新語法特性也不能使用。

這個時候在想如果想解決這個問題應該要死扣住以下幾點:

  • 1.不能頻繁創建線程
  • 2.不能對代碼有過大的改動
  • 3.對線程創建以及數量要有良好的控制
  • 4.不能考慮使用新語法特性

ThreadPool這個對象不是剛好滿足這個情況嗎,這時候將代碼修改為:

byte[] sendbytes = new byte[] { 0 };
ThreadPool.QueueUserWorkItem(_=> 
{
   Send(sendbytes);
});

其實實現這一段代碼之後,心裏依舊沒有解決問題的喜悅。因為根據線程池的原理,如果任務量過大的話還是會開闢默認線程數量以外的新線程。但是線程池對線程管理比較好,這樣的該的結果就能直接提交代碼重新去服務器上部署嗎?心裏還是沒有底我又做了如下測試:

//模擬在同一個時間點內大量開啟線程模擬多設備發送數據
for (int i = 0; i < 10000; i++)
{
   var thread = new Thread((_) =>
   {
       //模擬發送數據耗時
       Thread.Sleep(1000);
   });
   thread.Start();
}

觀察VS內存監測變化。

 

 

發現在大量創建線程的時候CPU和內存佔用會陡增。那麼接下來我在試一試用線程池去執行這些操作會是一個什麼情況代碼修改如下:

for (int i = 0; i < 100000; i++)
{
    ThreadPool.QueueUserWorkItem(_=> 
    {
       Thread.Sleep(1000);
    });
}

繼續觀察VS內存監測變化。

 

 

發現幾乎沒有任何波瀾,看到這個測試結果只能說感謝微軟實現了一個如此優秀的線程池。這個時候由於時間緊迫只能先改一版本拿到服務器上去頂一陣肯定比上一個版本要好。

問題又來了如果再繼續出現問題怎麼繼續排查?下一次不一定能拋出更有用的信息。這個時候想到的解決方案如下:

  • 1.添加DUMP文件輸出
  • 2.關鍵敏感地方加強日誌信息詳細程度和適量try塊捕獲異常

到此耗時大約3小時左右,編譯好版本部署到服務器上再做觀察。就這樣觀察了一個多星期沒有再次出現崩潰異常。其實分析下來,發現對這個問題發生原理可能還沒有玩明白需要繼續研究。