天天看點

項目優化經驗——垃圾回收導緻的性能問題

談談最近優化一個網站項目的經驗,首先說一下背景情況:

1) 在頁面背景代碼中我們把頁面上大部分的HTML都使用字元串來拼接生成然後直接指派給LiteralControl。

2) 網站CPU很高,基本都在80%左右,即使使用了StringBuilder來拼接字元串性能也不理想。

3) 為了改善性能,把整個字元串儲存在memcached中,性能還是不理想。

在比較了這個網站和其它網站伺服器上相關性能螢幕名額後發現有一個參數特别顯眼:

項目優化經驗——垃圾回收導緻的性能問題

就是其中的每秒配置設定位元組數,這個性能比較差的網站每秒配置設定2GB的記憶體(而且需要注意由于性能螢幕是每秒更新一下,對于一個非常健康的網站這個值應該經常看到是0才對)!而其它一些網站隻配置設定200M左右的記憶體。伺服器配備4G記憶體,而每秒配置設定2G記憶體,我想垃圾回收器一定需要不斷運作來回收這些記憶體。觀察%Time in GC可以發現,這個值一直在10%左右,也就是說上次回收到這次回收間隔10秒的話,這次垃圾回收1秒,由于回收的時間相對固定,那麼這個值可以反映回收的頻繁度。

知道了這個要點就知道了方向,在項目中找可能的問題點:

1) 是否配置設定了大量臨時的小對象

2) 是否配置設定了數量不多但比較大的大對象

在經曆了一番查找之後,發現一個比較大的問題,雖然使用了memcached來緩存整個頁面的HTML,但是在輸出之前居然進行了幾次string的Replace操作,這樣就産生了幾個大的字元串,我們來做一個實驗模拟這種場景:

public partial class _Default : System.Web.UI.Page
{
    static string template;
    protected void Page_Load(object sender, EventArgs e)
    {
        if (template == null)
        {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 10000; i++)
                sb.Append("1234567890");
            template = sb.ToString(); 
        }

        Stopwatch sw = Stopwatch.StartNew();

        for (int i = 0; i < 1; i++)
        {
            long mem1 = GC.GetTotalMemory(false);
            string s = template + i;
            long mem2 = GC.GetTotalMemory(false);
            Response.Write((mem2 - mem1).ToString("N0"));
            Response.Write("<br/>");
            GC.KeepAlive(s);
        }

        for (int i = 0; i < 100000; i++)
        {
            double d = Math.Sqrt(i);
        }

        Thread.Sleep(30);
        Response.Write(sw.ElapsedMilliseconds);
    }
}      

在這段代碼中:

1) 我們首先使用一個靜态變量模拟緩存中的待輸出的HTML

2) 我們中間的一段代碼測算一下這個字元串占用的記憶體空間

3) 随後我們做了一些消耗CPU的運算操作來模拟頁面的一些計算

4) 然後休眠一段時間

4) 最後我們輸出了頁面執行時間

我們這麼做的目的是模拟一個比較“正常的”ASP.NET頁面需要做的一些工作:

1) 記憶體上的配置設定

2) 一些計算

3) 涉及到IO通路的一些等待

來看看輸出結果:

項目優化經驗&amp;mdash;&amp;mdash;垃圾回收導緻的性能問題

這裡可以看到,我們這個字元串占用差不多200K的位元組,字元串是字元數組,CLR中字元采用Unicode雙位元組存儲,是以10萬長度的字元串占用200千位元組,并且也可以看到這個頁面執行時間30毫秒,差不多是一個正常aspx頁面的時間,而200K不到的字元串也差不多相當于這個頁面的HTML片段,現在我們來改一下其中的一段代碼模拟優化前進行的Replace操作帶來的幾個大字元串:

for (int i = 0; i < 10; i++)
{
    //long mem1 = GC.GetTotalMemory(false);
    string s = template + i;
    //long mem2 = GC.GetTotalMemory(false);
    //Response.Write((mem2 - mem1).ToString("N0"));
    //Response.Write("<br/>");
    //GC.KeepAlive(s);
}      

然後使用IDE自帶壓力測試1000常量使用者來測試這個頁面:

項目優化經驗&amp;mdash;&amp;mdash;垃圾回收導緻的性能問題

可以看到每秒配置設定了超過400M位元組(這和我們線上環境比還差點畢竟請求少),CPU占用基本在120-160左右(雙核),我們去掉每秒配置設定記憶體這個數值,來看看垃圾回收頻率和CPU占用兩個值的圖表:

項目優化經驗&amp;mdash;&amp;mdash;垃圾回收導緻的性能問題

可以看到紅色的CPU波動基本和藍色的垃圾回收波動保持一緻(這裡不太準确的另外一個原因是壓力測試用戶端運作于本機,而為w3wp關聯2個處理器)!為什麼說垃圾回收會帶來CPU的波動,從理論上來說有以下原因:

1) 垃圾回收的時候會暫時挂起所有線程,然後GC會檢測掃描每一個線程棧上可回收對象,然後會移動對象,并且重新設定對象指針,這整個過程首先是消耗CPU的

2) 而且在這個過程之後恢複線程執行,這個時候CPU往往會引起一個高峰因為已經有更多的請求等待了

我們把Math.Sqrt這段代碼注釋掉并且把w3wp和VSTestHost關聯到不同的處理器來看看對于CPU計算很少的頁面,上圖更明顯的對比:

項目優化經驗&amp;mdash;&amp;mdash;垃圾回收導緻的性能問題

這說明垃圾回收的确會占用很多CPU資源,但這隻是一部分,其實我覺得網站的CPU壓力來自于幾個地方:

1) 就是大量的記憶體配置設定帶來的垃圾回收所占用的CPU,對于ASP.NET架構内部的很多行為無法控制,但是可以在代碼中盡量避免在堆上産生很多不必要的對象

2) 是實際的CPU運算,不涉及IO的運算,這些可以通過改良算法來優化,但是優化比較有限

3) 是IO操作這塊,資料量的多少很關鍵,還有要考慮memcached等外部緩存對象序列化反序列化的消耗

4) 雖然很多IO操作不占用CPU資源,線程處于休眠狀态,但是很多時候其實是依托新線程進行的,帶來的就是線程切換和線程建立消耗的消耗,這一塊可以通過合理使用多線程來優化

發現了這個問題之後優化就很簡單了,把Replace操作放到memcached的Set操作之前,取出之後不産生過多大字元串,把for循環改為一次,再來看一下:

項目優化經驗&amp;mdash;&amp;mdash;垃圾回收導緻的性能問題
項目優化經驗&amp;mdash;&amp;mdash;垃圾回收導緻的性能問題

這次記憶體配置設定明顯少了很多,CPU降下來了,降的不多,但從壓力測試螢幕中看到頁面執行平均時間從5秒變為3秒了,每秒平均請求數從170到了200(最高從200到了300)。在這裡要說明一點很多時候網站的性能優化不能光看CPU還要對比優化前後網站的負載,因為在優化之後頁面執行時間降低了,負載量就增大了CPU消耗也随之增大。并且可以看到垃圾回收頻率的縮短很明顯,從長期在30%到幾十秒一次30%。

最後想補充幾點:

1) 有的時候我們會使用GC.GetTotalMemory(true); 來得到垃圾回收之後記憶體配置設定數,類似這樣涉及到垃圾回收的代碼在項目上線後千萬不能出現,否則很可能會% Time in GC達到80%以上大量占用CPU。

2) 對于放在緩存中的對象我們往往會覺得性能得到保障大量去使用,其實緩存實作的隻是把創造這個對象過程的時間轉化為空間,而在拿到這個對象之後再進行很多運算帶來的大量空間始終會進行垃圾回收。做網站和做應用程式不一樣,一個操作如果申請200K堆記憶體,一個頁面執行這個操作10次,一秒200多個請求,大家可以自己算一下平均每秒需要配置設定多少記憶體,這個數值是相當可怕的,網站是一個多線程的環境,我們對記憶體的使用要考慮更多。

作者:

lovecindywang

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。