調試.NET Web應用程序High Memory - Part 1

最近遇到.NET Web應用程序內存使用的各種問題,總結一些具體的現象和調試方法。常見的不正確的內存使用造成高內存使用量主要原因有以下這麼幾種,


問題分類


大數據量DataTable

大多數web應用程序都會用到DataTable,DataTable中會有很多的Cell來存儲表格中的數據,一旦表格中有過多的列便會導致內存使用量的急劇上升,如果不能夠得到及時釋放,內存就會被大量的表格數據佔用。從而導致高內存使用量的問題。同時也增加內存中對數據的查找過濾的性能損耗,更糟糕的是很多情況下這些數據是要從數據庫服務器通過網絡傳輸過來,費力不討好的典型。 

解決這個問題還是要從數據量入手,儘量增加查詢條件減少返回的數據量,或者分頁查詢,另外要及時的釋放佔用的內存。 


Session或者Application State中存儲太多數據

程序員經常會發明各種各樣的提高性能方式,於是有些時候Session或者Application State中也變成着眼於性能的程序員的一塊可利用資源,把很多數據塞在其中作爲緩存,看上去是個很不錯的想法,但是這很有可能讓系統的內存使用量失去控制,從而再次造成高內存使用的問題。 

那我總歸要緩存一些必要的數據,我應該放在哪裏?ASP.NET Cache是個不錯選擇,因爲他有內建的機制來決定何時cache的內存被回收,例如在內存有壓力的時候,減少cache至少比程序崩潰要好。


Debug模式下的應用程序和庫文件(DLL)

經常發現生產環境中存在debug模式下的庫文件DLL,或者乾脆這個應用程序跑在debug模式下。

Web.config中如此設置 

<compilationdebug=”true”/>

注意,debug模式是爲了debug用的,對於生產環境不適用。

  • 他會讓變量生命週期變長,長到直到出了scope才考慮回收。所以在debug模式下發現內存使用就是比release模式下高一些,這就是原因了。 
  • 另外debug模式下webresource.axd,scriptresource.axd處理模塊中不會讓客戶端緩存經其處理的腳本,客戶端每次都要重新下載這些腳本!
  • Page編譯時間會變長,因爲一些batch optimization被禁用了
  • 代碼執行時間變長,debug模式下會插入一些供debug的代碼

所以如果你負責一個應用程序的部署,一定要注意稍微花一點時間把dll在release下模式下編譯,同時debug設成false。或者乾脆在機器級別配置文件中更改如下配置,該機器web應用程無一例外都要運行在release模式下。 

Machine.config

<configuration>
    <system.web>
          <deployment retail=”true”/>
    </system.web>
</configuration>

大量異常拋出

不要忽視異常,很多時候覺得異常沒關係,catch住系統別崩潰就行了,但是別忘了異常也是一個消耗內存的object,而且有的時候消耗內存並不小,他不僅需要內存放他的調用棧信息,錯誤消息,很多時候他還包含了inner exception以及相應的錯誤相關的object。大量這樣的異常放在內存中也會對內存造成相當大的損耗。 

所以還是老老實實發現一個異常就看看是不是邏輯錯誤造成的,把問題消滅在萌芽狀態。如果你想知道你的應用程序中有多少異常正在發生,performance counter可以幫你。


內存碎片

內存碎片是連續內存分配的殺手,一個很簡單的例子,比如我有100M內存,但是第50M的點上存在一個4k的碎片,那如果64M連續空間的分配請求就會失敗,因爲沒有連續的64M空間,OOM就發生了。

 

造成內存碎片的原因主要有哪些?

Debug = true

頁面編譯出來的dll被加載到內存中,如果在非batchcompile的應用程序中,頁面都是在第一次被請求之後編譯成相應的dll然後加載到內存中,那麼這些dll就沒有一個同一個位置來存放,於是散落在內存各個角落。成爲內存碎片。

 

解決方式就是要將<compilation debug="false"/>從而打開batch compilebatch compile會將同一個文件夾的頁面編譯到一個dll中,這樣就大大減少了內存碎片來避免該問題的發生。

 

Dynamic assemblies

XSLTtransformation

XSLT的加載過程中會產生動態assmebly,例如如下代碼就會動態產生1000dll

For(int i=0;i<1000;i++)
{
      xslt.Load(stylesheet);
      //Do other stuff
      xslt.Transform(doc, null, writer);
}

如果我們確定XSLT是一樣的那就不需要加載多次。可以改爲以下方式。

xslt.Load(stylesheet);
For(int i=0;i<1000;i++)
{
//Do other stuff
      xslt.Transform(doc, null, writer);
}

XmlSerializer

msdn文檔中對XmlSerializer  (.NET framework 2.0)描述如下

Dynamically Generated Assemblies

To increaseperformance, the XML serialization infrastructure dynamically generatesassemblies to serialize and deserialize specified types. The infrastructurefinds and reuses those assemblies. This behavior occurs only when using thefollowing constructors:

System.Xml.Serialization.XmlSerializer(Type)

System.Xml.Serialization.XmlSerializer(Type,String)

If you use any of the other constructors, multiple versions of thesame assembly are generated and never unloaded, resulting in a memory leak andpoor performance. The simplest solution is to use one of the two constructorsabove. Otherwise, you must cache the assemblies in a Hashtable, as shown in the following example.

Hashtable serializers = new Hashtable();
// Use the constructor that takes a type and XmlRootAttribute.
XmlSerializer s = new XmlSerializer(typeof(MyClass), myRoot);
// Implement a method named GenerateKey that creates unique keys 
// for each instance of the XmlSerializer. The code should take 
// into account all parameters passed to the XmlSerializer 
// constructor.
object key = GenerateKey(typeof(MyClass), myRoot);
// Check the local cache for a matching serializer.
XmlSerializer ser = (XmlSerializer)serializers[key];
if (ser == null) 
{
    ser = new XmlSerializer(typeof(MyClass), myRoot);
    // Cache the serializer.
    serializers[key] = ser;
}
else
{
    // Use the serializer to serialize, or deserialize.
}

也就是說如果我們使用的不是上面列出的兩個構造函數來構造XmlSerializer,那注意要通過緩存的方式將其緩存起來以備後續使用。因爲其他的構造函數不會重用生成的dll,從而可能會造成內存碎片過多的問題。

 

我就是要加載很多dll去做某件事情,但是我不希望產生這樣的副作用! 

如果真的有這種要求其實也是有辦法可以滿足的。已經加載的dll只有到applicationdomain卸載的時候纔會從內存中卸載掉。所以接下來我們要介紹的方式是通過創建和卸載一個單獨的application domain來完成的。

class Program
{
    static void Main(string[] args)
    {
        AppDomain tempDomain = null;
        try
        {
            Console.WriteLine("Create temp domain");
            tempDomain = AppDomain.CreateDomain("TempDomain");
            TempDomain domain = (TempDomain)tempDomain.CreateInstanceFromAndUnwrap(
                Assembly.GetAssembly(typeof(TempDomain)).Location, typeof(TempDomain).FullName);
            domain.DoWork();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
        finally
        {
            Console.Read();
            AppDomain.Unload(tempDomain);
            Console.WriteLine("Unload temp domain");
        }
        Console.Read();
    }
}

public class TempDomain : MarshalByRefObject
{
    public void DoWork()
    {
        int i = 0;
        try
        {
            for (i = 0; i < 100; i++)
            {
                Console.WriteLine("Creating {0} dynamic assembly", i);
                AppDomain myCurrentDomain = AppDomain.CurrentDomain;
                AssemblyName myAssemblyName = new AssemblyName();
                myAssemblyName.Name = "TempAssembly";
                AssemblyBuilder myAssemblyBuilder = myCurrentDomain.DefineDynamicAssembly(myAssemblyName, AssemblyBuilderAccess.Run);
                ModuleBuilder myModuleBuilder = myAssemblyBuilder.DefineDynamicModule("TempModule");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }
}


Regular Expression正則表達式匹配大字符串

正則表達式是把典型的雙刃劍,用的好可以幫你快速的完成極其複雜的字符串匹配或替換,但是一知半解的使用正則表達式很有可能掉進一些陷阱,造成內存或者CPU使用率的問題。

 

比如你在一個非常大的字符串(MB級別的字符串)中匹配字符串.

static void Main(string[] args)
        {
            Regex regex = new Regex("<body(.|\n)*</body>");
            string html = File.ReadAllText("asp.net.txt");
            MatchCollection matches = regex.Matches(html);
            Console.WriteLine("Match count {0}", matches.Count);
            Console.Read();
        }


注意爲什麼堆中會存在如此大的int數組呢?這與正則表達式的實驗有關,正則表達式需要數組來維護在匹配過程中的所有匹配位置信息,如果在這個過程中發現需要更大的數組的話,其實現是直接把數組變爲原來的兩倍長度。由於5Mhtml.txt中有很多的匹配,所以這些數組的大小便失去了控制。

 

注意這個問題不是.netframework實現的問題,而是對於所有的基於

非確定有限狀態自動機的正則表達式實現均存在的問題。


調試方法

調試內存問題有各種各樣的工具和方法,我們先從現成的工具開始,

 

Performance counter

性能計數器是系統性能最重要的觀察工具,沒有之一。無論是問題出現之前還是問題正在發生,打開性能計數器收集一些相關的的數據對調試一般都大有裨益。

 

觀察託管代碼的內存問題可以參考以下這些性能技術

 

•Process/PrivateBytes

•Process/VirtualBytes

•.NET CLRMemory/All counters

•.NET CLRLoading/All counters

 

通過.NET CLR Memory/%Time in GC這個性能計數,我們可以知道GC時間佔用了多少百分比,如果發現這個百分比在50%以上,那麼我們就要繼續往下看看程序裏再發生什麼事情,如果這個時間只有10%或者更小,那就不用在這上面浪費時間了。

 

現在我們來說GC時間百分比較高的情形,接下來我們可以看看Allocated Bytes/sec,注意這個值經常會比太精確,如果性能計數的採樣時間間隔大於內存回收的間隔,那你很有可能看到這個值是0,因爲這個值只有在回收開始的時候開始刷新。Allocated Bytes/sec比較高一般都會伴隨%Time in GC比例較高,接下來我們要着重看一下

#Gen 0 Collections, # Gen 1 Collections, and # Gen 2 Collections來分辨到底各個代的回收頻率如何。第0代,第1代的回收相對都比較迅速,對應用程序的性能不會有大的影響,但是第2代的回收就要猛烈很多。一個比較健康的情況是101代回收發生12代回收。

 

有一種比較特殊的現象% Time in GC比例高但Allocated Bytes/sec卻很小,這時候要注意第二代的回收情況,原因多數是因爲很多object經常很容易的被提升至第二代,要查看這種問題可以通過CLR Profiler或者windbg來查看第二代的對象。

 

Debug Diagnostic Tool v1.2

Debug Diag是調試託管程序尤其是IIS Web Application的利器,1.2版本集成了性能分析腳本,使得很多複雜的問題變得一目瞭然,無所遁形。比如內存問題,當問題發生的時候,直接打開Debug Diag,切換到Processes選項卡,右鍵選擇Create Full Userdump,然後在Advanced Analysis裏面加載進來,選擇性能分析腳本進行分析,各種問題以及相應的細節,解決方案都會以一個html格式的報告呈現給用戶。


Windbg

Windbg目前是隨Windows SDK一起安裝的。通過windbg打開dump文件。加載相應的sos調試擴展庫,這裏要說一句,debugdiag的安裝目錄下包含了兩個更加強大的調試擴展庫,psscor2, psscor4分別對應調試.NET framework 2.04.0的程序。通過.load命令加載相應的調試擴展,之後開始我們的調試過程。

針對上面列舉各種問題具體的調試方式在part 2會詳細介紹。


參考文檔

http://support.microsoft.com/default.aspx?scid=kb;EN-US;316775

http://support.microsoft.com/kb/872800

http://support.microsoft.com/default.aspx?scid=kb;EN-US;886385

http://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlserializer(v=vs.80).aspx

http://msdn.microsoft.com/en-us/library/x2tyfybc.aspx

http://msdn.microsoft.com/en-us/library/fxk122b4.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章