排錯案例分享

前幾天另一個組的同事遇到了一個問題,他們的服務器cpu總是很高,用性能監視器看來,情況更是奇怪,開始時cpu一直很低,接着忽然漲到25%,然後再也下不來了,隔一段時間cpu又漲25%,然後就一直保持50%以上,就這樣一漲25%,最多漲四次就100%,佔用CPU的是w3wp進程,到了100%的時候,就不得不把w3wp強行終止

除了cpu外其他屬性均正常

在性能監視器裏添加Asp.net 的Current Request(當前請求數),發現有一些請求始終沒有結束,對比cpu的使用率,可以看到當有一個這樣的請求,cpu爲25%兩個則爲50%,三個75%....


看到這個問題,我的第一個反應是請求爲什麼不過期,而是一直佔用cpu,第二個,奇怪的地方,是cpu增長的方式和幅度很有趣,固定每次增長25%,並且增長的時間不定.

開始我沒有什麼太好的想法,但是光cpu高,其他指標正常,讓我覺得起一定有某項工作在努力進行,而且不涉及大量內存和io操作,最簡單的模擬方法莫過於寫一個什麼都不做的死循環,於是我新建了一個web項目,在本機的的iis上建立一個站點指向他,爲了真實一些,我還給這個站點單獨設置了一個應用程序池,然後,我在一個空頁面的onload里加上while(true){}看看效果,下面是訪問這個空頁面的時候的cpu情況,我們可以看到,佔用率超過了50%



還有:對這個頁面的請求一直不超時,無論我回收甚至停止應用程序池,w3wp都執着的佔用我50%的cpu,而我是一個雙核處理器,而有問題的那臺是四核的.每次漲25%,呵呵,有點意思哈,是不是實際問題很像了呢?

那麼爲什麼請求不超時呢?本地的代碼web.config中設置爲debug="true",這時默認是不會超時的,但是發佈後一般會改爲debug="false",這時纔有請求超時,推測另一組的開發人員出於某種原因,發佈後沒有進行修改.所以造成了一直存在的請求.趕忙叫請人看了看,果然webconfig中debug="true"

但現在的問題是到底是什麼請求導致了這些超長時間請求呢?難道真是一個死循環嗎?

要解決這個問題,我們首先要找到有問題的請求,開始的時候我們不知道怎麼查看iis當前的具體請求,而iis只提供了當前請求數,但是起碼我們有iis的日誌吧,所以我想使用一個程序來監視cpu的值,一旦發生了25%的長期提升,我就記錄下發生的時間,然後到log裏面看看對應時間到底有些什麼請求,下面就是我的程序,使用的是程序操控performace counter,大概邏輯我會在下面講述
   public class TestPerformanceTool
    {
        
static PerformanceCounter pc;
        
public static void Test()
        {
            TimeSpan span
=new TimeSpan(0,2,0);//cpu升高持續多長時間需要記錄:現爲2分鐘
            int interval=1000;//兩次查看cpu使用率的間隔:現爲1秒
            float limit=20f;//cpu升高百分比限制:現爲20%


            List
<TimeValueRecord> list = new List<TimeValueRecord>();
            pc 
= new PerformanceCounter();
            
// 獲取或設置此性能計數器的性能計數器類別的名稱。
            pc.CategoryName = "Processor";
            
// 獲取或設置與此 PerformanceCounter 實例關聯的性能計數器的名稱。
            pc.CounterName = "% Processor Time";
            pc.InstanceName 
= "_Total";
            pc.ReadOnly 
= true;
            
float pre = 0f;
            
float cur = 0f;
            
bool firstTime = true;//排除第一次運行時的躍升干擾
            DateTime currDate = DateTime.Now;
            DateTime preDate 
= DateTime.Now;
            
float diff = 0f;
            
while (true)
            {
                cur 
= pc.NextValue();
                
if (!firstTime)
                {
                    currDate 
= DateTime.Now;
                    diff 
= cur - pre;
                    
if (diff > limit)
                        list.Add(
new TimeValueRecord(preDate,pre));
                    
foreach (TimeValueRecord entry in list)
                    {
                        
if (cur - entry.Value < limit)
                            entry.Value 
= int.MaxValue;//foreach中不能remove,這樣就等於使得cur-entry.value爲一個負數,肯定小於limit,
                        else if (currDate - entry.Key > span)
                        {
                            Console.WriteLine(
"Jump Occurred at {0}", entry.Key);
                            entry.Value 
= int.MaxValue;//以免多次輸出
                        }
                    }
                }
                pre 
= cur;
                preDate 
= currDate;
                firstTime 
= false;
                Thread.Sleep(interval);
            }
        }
    }

    
internal class TimeValueRecord
    {
        
public TimeValueRecord(DateTime key,float value)
        {
            Key 
= key;
            Value 
= value;
        }
        
public DateTime Key;
        
public float Value;
    }

大概邏輯是每隔interval秒查看一次cpu的使用率,如果當前值和上次值之間的差距<25%(程序裏用的是20%,爲的是靈敏度高一些),就放在一個數組裏,注意由於有的時候服務器cpu會出現在一瞬間很高的情況,然後很快降下來的情況,因此,我們還需要知道這次提升是否持續了一段時間,程序裏是2分鐘,一旦持續超過兩分鐘,就把這次提升的時間輸出,這裏簡單的輸出在了控制檯裏

寫好之後在自己的機器上運行的不錯,完全可以監視自己故意製造的cpu使用率持久提升,但是麻煩的是,放在服務器上就沒效果了,大概是因爲在服務器上,cpu並不是如我想象的瞬間提升,而是有一個過程,我的兩次cpu監視的間隔設的是一秒,在這一秒內服務器的cpu沒有提高到20%,所以程序就沒有捕獲,所以只好把間隔調大一些,不過自然輸出時間的誤差就要大了一些.

不過這時我們的運維人員找到一個很好的工具,微軟的iis診斷工具,可以看到當前請求內容,在這裏我提供了一個下載連接,所以上面我的程序就沒有什麼用處了,不過可以作爲參考,改改的話可以監視很多性能指標的變動時間:)

很快,運維人員就找到了有問題的請求,是一個插入高亮代碼的頁面,裏面代碼特簡單,實在看不出有死循環,實際這個頁面前一天大家曾經懷疑過,但是就是因爲裏面代碼一幕瞭然,就放過了,我們來看看這個頁面的代碼
 1 using System;
 2 using System.Data;
 3 using System.Configuration;
 4 using System.Collections;
 5 using System.Web;
 6 using System.Web.Security;
 7 using System.Web.UI;
 8 using System.Web.UI.WebControls;
 9 using System.Web.UI.WebControls.WebParts;
10 using System.Web.UI.HtmlControls;
11 
12 public partial class InsertCode : System.Web.UI.Page
13 {
14     protected void Page_Load(object sender, EventArgs e)
15     {       
16             if (Request["Language"!= null)
17             {
18                 HighLightService s = new HighlightService();
19                 bool outlining = true;
20                 string outText;
21                 if (string.IsNullOrEmpty(Request["outlining"]))
22                     outlining = false;
23                 if (Request["Code"== null || Request["Language"== null)
24                     outText = s.Transform("public int c""C#", outlining);
25                 else
26                     outText = s.Transform(Request["Code"], Request["Language"], outlining);
27                 outText = outText.Replace("/"""///"");
28                 outText = outText.Replace("/n""<br>");
29                 outText = outText.Replace("/r""<br>");
30                 outText = outText.Replace("<pre>""");
31                 outText = outText.Replace("</pre>""");
32                 outText = Globals.ReplaceSpace(outText);
33                 outText = @"<div style='BORDER-RIGHT: windowtext 0.5pt solid;PADDING-RIGHT: 5.4pt; BORDER-TOP: windowtext 0.5pt solid; PADDING-LEFT: 5.4pt; BACKGROUND: #e6e6e6; PADDING-BOTTOM: 4px;PADDING-TOP: 4px;     BORDER-LEFT: windowtext 0.5pt solid;WIDTH: 95%;    BORDER-BOTTOM: windowtext 0.5pt solid;word-break:break-all'>" + outText + "</DIV>";
34                 clientScript.Text = "var outText=/"" + outText + "/";  FCK.InsertHtml(outText);window.close();";
35             }        
36     }
37 }
加上using才37行,本身邏輯不超過14行,不用管具體是幹什麼用的,只用分析一下這十幾行代碼,看似沒有什麼可能造成死循環的地方啊?
再仔細看看,代碼裏有三個方法調用(見加粗的地方那個),前兩個經檢查是webservice,webservice的站點也不在那臺服務器上,所以和cpu高沒有關係,其他語句都很簡單,不太可能有問題,而後面的一個方法調用很可能就是問題所在的地方了

打開這個方法看看,裏面是一個正則替換,也很簡單,看來所有的問題就在這個正則上了,讀不明白沒關係,我也沒讀明白:P
        public static string ReplaceSpace(string content)
        {
            
string findstr="(?<fore>(?:(?:[^< ])*(?:<(?:!--(?:(?:[^-])*(?:(?=-->)|-))*--|(?:[^>])+)>)?)*)[ ](?<back>(?:(?:[^< ])*(?:<(?:!--(?:(?:[^-])*(?:(?=-->)|-))*--|(?:[^>])+)>)?)*)";
            
string replacestr="${fore}&nbsp;${back}";
            
string targetstr=System.Text.RegularExpressions.Regex.Replace(content,findstr,replacestr,System.Text.RegularExpressions.RegexOptions.IgnoreCase);
            
return targetstr;
        }

顯然這個正則並不是所有的情況都有問題,不然後臺監視到的cpu佔用會厲害的多,所以我找了幾段文本去測試,經過反覆測試,發現匹配諸如<span>&dll;&dfasfd;&dsd;BB()</span>
這樣的代碼,會造成死循環(當然也可能是執行時間巨長無比)而在此期間cpu會被佔用,內存一切正常下圖是我用自己的工具執行匹配時的截圖



具體爲什麼,到底是什麼文字模式會導致問題,我還不知道,但是可以肯定的是,問題就在這裏了,具體怎麼解決,就交給那組的同事了


總結:

身爲程序員,通過自己的努力能解決一個問題,心裏都是很愉快的,不過高興之餘也應該有所總結,爲了自己不被同樣的問題再困擾,也爲了下次能夠更快的找到問題

webconfig中的debug屬性,發佈時要注意修改,當然保持debug=true,也能讓你查看到許多自己程序的問題,所以最好是最初發布時能夠把他打開

適應了調試環境,我們在又不能調試,又沒法記錄日誌的時候,需要能靜下心來看代碼.許多問題通過仔細的閱讀和分析代碼都能找到.

正則表達式,特別是相對複雜的表達式,一定不要忘記測試其有效性和性能,所以自己最好是手邊常備一個習慣的正則工具,我用的是自己寫的,下面這個blog上的正則工具比較全面,大家也可以用用看
http://www.cnblogs.com/Heroman/archive/2005/01/13/91265.html

多核服務器處理多線程時,我們可以看到線程和cpu數目之間的直接關係,但不等於說一個線程是由一個cpu全權處理的,具體調度問題我還不太清楚

上面那個正則哪裏有問題,我還不知道,大家如果能看出來一定要告訴我

關於排錯這篇文章講的還不錯
http://news.csdn.net/n/20071228/112244.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章