1. 程式人生 > >漫遊測試之效能測試(4.8通過監控發現的一個案例)

漫遊測試之效能測試(4.8通過監控發現的一個案例)

很早以前在《51測試天地》發表的一篇關於windows平臺上面C#的效能問題分析的文章:

前端時間測試一個系統的效能狀況,其主要業務的HTTP請求內容在Loadrunner中的程式碼為:

web_url("Index_3",

"URL=http://192.168.102.43:8003/Home/Index?token={token}",

"TargetFrame=",

"Resource=0",

"RecContentType=text/html",

"Referer=http://192.168.102.43:8001/home/UserIndex?token={token}",

"Snapshot=t12.inf",

"Mode=HTML",

EXTRARES,

"URL=GetUserData/?opkey=GetTypeList", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

"URL=GetUserData?info=from+shortcutBar&opkey=GetToolBarRes", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

"URL=GetUserData?pageIndex=1&pageSize=32&info=from+loadScrnData&opkey=GetResListByUser", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

"URL=../imgs/icon.png", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

"URL=../imgs/icon_seek.png", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

"URL=../imgs/icon_subscription.png", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

"URL=../imgs/icon_resManage.png", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

"URL=../imgs/icon_delete.png", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

"URL=../imgs/toolbar_repeat.png", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

"URL=../imgs/toolbar_left.png", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

"URL=../imgs/toolbar_right.png", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

"URL=http://192.168.102.43:9090/20131218/Thumbnail/docx_ec030663-c741-4067-9929-1bc5e8898d60/thumb.jpg", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

"URL=../imgs/bg108.png", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

"URL=GetUserData?pageIndex=2&pageSize=32&info=from+loadScrnData&opkey=GetResListByUser", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

"URL=../imgs/icon_corner.png", "Referer=http://192.168.102.43:8003/Home/Index?token={token}", ENDITEM,

LAST);

在100併發下持續略5分鐘時,Loadrunner出現以下錯誤提示,隨後應用伺服器崩潰:

檢視Errors Per Second可以看到在所有使用者載入後,約5分鐘出現了大量的錯誤。

檢視Throughput從06分鐘後,幾乎伺服器不再處理請求了。

檢視伺服器的CPU使用資源情況在05:30的時候資源使用達到了一個峰值,隨後降低了下來。

檢視可用實體記憶體,壓力開始後記憶體持續下降,崩潰後記憶體進行了回收,記憶體最小的時候仍有10592MB,記憶體資源充足。

綜上,懷疑是CPU方面的峰值時產生了連鎖影響,導致出現了相關的問題。

出現CPU問題時,首先考慮本程式是否為強演算法型應用。本程式從業務實現的技術角度來看屬於純記憶體型應用,並且相關的資料操作都存在記憶體中,所以首先可以排除是CPU本身的問題。

其次考慮是否為記憶體不足引起。從上圖的資料來看,記憶體是充足的,所以記憶體方面不是引起該問題的主要原因。

那麼剩下的就只有可能是由於IO或者Thread方面引起的現象,從上面的介紹可知,本程式是記憶體型應用,所以IO可以排除,目前最有可能是Thread方面的原因引起,新增效能計數器,進行再次測試監控,發現Thread是如下發展:

從05:30至07:30這2分鐘內,執行緒出現急降急升的狀態,執行緒使用和回收不正常。通常來說,執行緒使用正常的情況下應該如下所示。

從上面的系列資訊,我們目前所知的問題的初步線索為:

結論:100併發訪問業務請求5分鐘後出現崩潰,系統的穩定性不足。

出錯原因:Thread的使用和回收極有可能存在重大嫌疑。

至此,Loadrunner能告訴我們的只有這麼多,但是具體的原因倒底是什麼呢?我們還能夠繼續麼,做為測試人員我們被告誡需要盡最大的努力找到程式出現錯誤的最短路徑,所以讓我們繼續,檢視原始碼吧。一路追查,發現發出請求的主要是這個方法的這部分,但這個方法似乎很簡單,裡面只使用了一個方法client.GetAsync(apiurl)。

{

            //略

            client.GetAsync(apiurl);

}

檢視MSDN中對於HttpClient相關方法的解釋,見下:

Name

Description

Send a GET request to the specified Uri as an asynchronous operation.

Send a GET request to the specified Uri as an asynchronous operation.

Send a GET request to the specified Uri with an HTTP completion option as an asynchronous operation.

Send a GET request to the specified Uri with a cancellation token as an asynchronous operation.

Send a GET request to the specified Uri with an HTTP completion option as an asynchronous operation.

Send a GET request to the specified Uri with a cancellation token as an asynchronous operation.

Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation.

Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation.

Asynchronous中文含義,非同步。即GetAsync(uri)方法是一個傳送非同步請求的方法,顯示非同步將會使用到Thread來進行,難道是這個方法本身出了問題麼?

先不要著急,我們繼續看MSDN對於HttpClient的介紹,我們可以發現這麼一段示例程式碼:

static async void Main()

{

      // Create a New HttpClient object.

      HttpClient client = new HttpClient();

      // Call asynchronous network methods in a try/catch block to handle exceptions 

      try

      {

         HttpResponseMessage response = await client.GetAsync("http://www.contoso.com/");

         response.EnsureSuccessStatusCode();

         string responseBody = await response.Content.ReadAsStringAsync();

         // Above three lines can be replaced with new helper method below 

         // string responseBody = await client.GetStringAsync(uri);

         Console.WriteLine(responseBody);

      }  

      catch(HttpRequestException e)

      {

         Console.WriteLine("\nException Caught!");

         Console.WriteLine("Message :{0} ",e.Message);

      }

      // Need to call dispose on the HttpClient object 

      // when done using it, so the app doesn't leak resources

      client.Dispose(true);

   }

哈哈,似乎問題找到了所在,我們的程式沒有呼叫Dispose方法釋放HttpClient物件。

// Need to call dispose on the HttpClient object 翻譯:需要呼叫disponse方法釋放HttpClient物件

// when done using it, so the app doesn't leak resources 翻譯:不使用它,有一些應用不會釋放資源

問題是.Net不是存在拉圾回收機制麼?client物件按道理應該進行自動回收,不應該出現這個問題。

抱著這樣的疑問,讓開發修改程式碼為,並執行相同的測試。

{

       //略

client.GetAsync(apiurl);

client = null;

}

系統業務響應時間快取載入後穩定,持續運行了1個小時。

流量也穩定,沒有出現原有急速下降的現象。

檢視Thread使用情況,呈波動發展,有使用也有回收,表現良好。

至此可以證明,效能問題已被解決。

那麼回到為什麼垃圾回收機制沒有起作用呢?網上有較多文章進行了解釋,其中下面的內容是主要原因:

首先,系統將託管堆內所有的物件視為可以回收的垃圾,然後系統從GCRoot開始遍歷託管堆內所有的物件,將遍歷到的物件標記為可達物件,在遍歷完成之後,回收所有的非可達物件,完成一遍垃圾收集。

注意,託管堆的垃圾收集只會自己收集託管物件

由於在執行完垃圾收集之後,託管堆中會產生很多的記憶體碎片,導致記憶體不再連續,因此在垃圾收集完成之後,系統會執行一次記憶體壓縮,將不連續的記憶體重新排列整齊,變成連續的記憶體。(關於垃圾收集的詳細資訊,大家可以參考《CLR Via C#》)

通過上面的簡述,大家都知道什麼樣的物件不會被收集,即能從GCRoot開始遍歷到的物件。

最常見的GCRoot是執行緒的棧,執行緒的棧裡面通常包含方法的引數、臨時變數等。另外常見的GCRoot還有靜態欄位、CPU暫存器以及LOH堆上 的大的集合。因此,如果想要讓託管物件的記憶體順利的釋放,只需要斷開與跟之間的聯絡即可。而對於非託管物件的記憶體,必須進行手動釋放。

非託管物件無論在什麼時候,都不會被垃圾收集所回收,必須手動釋放

在.net記憶體洩露的原因當中,事件佔據了非常大的一部分比例,事件是一種委託的例項,也就是與我們類中其他的欄位一樣,也是一個欄位。

我們檢視msdn可以瞭解到HTTPClient有這麼一段解釋:

By default, HttpWebRequest will be used to send requests to the server. This behavior can be modified by specifying a different channel in one of the constructor overloads taking a HttpMessageHandler instance as parameter. If features like authentication or caching are required, WebRequestHandler can be used to configure settings and the instance can be passed to the constructor. The returned handler can be passed to one of the constructor overloads taking a HttpMessageHandler parameter.

至此也就解釋了為什麼大併發下會出現這個問題。