漫游测试之性能测试(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出现以下错误提示,随后应用服务器崩溃:

Action2.c(11): Error -26612: HTTP Status-Code=500 (Internal Server Error) for http://192.168.102.43:8003/Home/Index?token=66e279bb2fc34c28ae119cdb26abab0b

查看Errors Per Second可以看到在所有用户加载后,约5分钟出现了大量的错误。

漫游测试之性能测试(4.8通过监控发现的一个案例)

查看Throughput从06分钟后,几乎服务器不再处理请求了。

漫游测试之性能测试(4.8通过监控发现的一个案例)

查看服务器的CPU使用资源情况在05:30的时候资源使用达到了一个峰值,随后降低了下来。

漫游测试之性能测试(4.8通过监控发现的一个案例)

查看可用物理内存,压力开始后内存持续下降,崩溃后内存进行了回收,内存最小的时候仍有10592MB,内存资源充足。

漫游测试之性能测试(4.8通过监控发现的一个案例)

综上,怀疑是CPU方面的峰值时产生了连锁影响,导致出现了相关的问题。

出现CPU问题时,首先考虑本程序是否为强算法型应用。本程序从业务实现的技术角度来看属于纯内存型应用,并且相关的数据操作都存在内存中,所以首先可以排除是CPU本身的问题。

其次考虑是否为内存不足引起。从上图的数据来看,内存是充足的,所以内存方面不是引起该问题的主要原因。

那么剩下的就只有可能是由于IO或者Thread方面引起的现象,从上面的介绍可知,本程序是内存型应用,所以IO可以排除,目前最有可能是Thread方面的原因引起,添加性能计数器,进行再次测试监控,发现Thread是如下发展:

漫游测试之性能测试(4.8通过监控发现的一个案例)

从05:30至07:30这2分钟内,线程出现急降急升的状态,线程使用和回收不正常。通常来说,线程使用正常的情况下应该如下所示。

漫游测试之性能测试(4.8通过监控发现的一个案例)

从上面的系列信息,我们目前所知的问题的初步线索为:

结论:100并发访问业务请求5分钟后出现崩溃,系统的稳定性不足。

出错请求:http://192.168.102.43:8003/Home/Index?token={token}

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

 

至此,Loadrunner能告诉我们的只有这么多,但是具体的原因倒底是什么呢?我们还能够继续么,做为测试人员我们被告诫需要尽最大的努力找到程序出现错误的最短路径,所以让我们继续,查看原代码吧。一路追查,发现发出请求的主要是这个方法的这部分,但这个方法似乎很简单,里面只使用了一个方法client.GetAsync(apiurl)。

public void OnAuthorization(AuthorizationContext filterContext)

{

            //略

            HttpClient client = new HttpClient();

            string apiurl = ConfigHelper.MasterSiteUrl + "/api/User?token=" + (!string.IsNullOrEmpty(token) ? token : user.Token);

            client.GetAsync(apiurl);

}

查看MSDN中对于HttpClient相关方法的解释,见下:

Name

Description

GetAsync(String)

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

GetAsync(Uri)

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

GetAsync(String, HttpCompletionOption)

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

GetAsync(String, CancellationToken)

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

GetAsync(Uri, HttpCompletionOption)

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

GetAsync(Uri, CancellationToken)

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

GetAsync(String, HttpCompletionOption, CancellationToken)

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

GetAsync(Uri, HttpCompletionOption, CancellationToken)

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对象按道理应该进行自动回收,不应该出现这个问题。

抱着这样的疑问,让开发修改代码为,并执行相同的测试。

public void OnAuthorization(AuthorizationContext filterContext)

{

       //略

HttpClient client = new HttpClient();

string apiurl = ConfigHelper.MasterSiteUrl + "/api/User?token=" + (!string.IsNullOrEmpty(token) ? token : user.Token);

client.GetAsync(apiurl);

client.Dispose();

client = null;

}

系统业务响应时间缓存加载后稳定,持续运行了1个小时。

漫游测试之性能测试(4.8通过监控发现的一个案例)

流量也稳定,没有出现原有急速下降的现象。

漫游测试之性能测试(4.8通过监控发现的一个案例)

查看Thread使用情况,呈波动发展,有使用也有回收,表现良好。

漫游测试之性能测试(4.8通过监控发现的一个案例)

至此可以证明,性能问题已被解决。

那么回到为什么垃圾回收机制没有起作用呢?网上有较多文章进行了解释,其中下面的内容是主要原因:

首先,系统将托管堆内所有的对象视为可以回收的垃圾,然后系统从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.

至此也就解释了为什么大并发下会出现这个问题。