1. 问题发现
在给上海某行做的一个HTTP/POST挡板服务程序过程中,自测时发现Loadrunner客户端显示的响应时间比预期的长很多。
LR压测脚本和场景描述如下:
脚本:
vuser_init.c :
char * transname = "lr_post_keepalive";
vuser_init()
{
web_enable_keep_alive();
return 0;
}
Action.c :
lr_start_transaction(transname);
//web_reg_save_param("ALL","LB=","RB=",LAST);
web_custom_request("httpmocker",
"Method=POST",
"URL=http://localhost:8080/httpmocker/httpmocker",
"EncType=application/json;charset=utf-8",
"RecCOntentType=application/json;charset=utf-8",
"Body={\""
" "
" "
" "
//..................(字串内容省略n行)
" "
"",
"TargetFrame=",
LAST );
lr_end_transaction(transname, LR_AUTO);
//lr_output_message(lr_eval_string("{ALL}"));
vuser_end.c :
vuser_end()
{
return 0;
}
Action.c中的字串为固定字串,长度大约是2KB,挡板程序收到该报文后根据配置信息中加载的回挡模板内容进行回挡处理(此处配置是对两个模板进行循环迭代,挡板程序的配置参考文档<性能测试组HTTP通用报文挡板使用说明.docx>>)。LR脚本和服务器在调试通过后均关闭日志,以使客户端和服务器性能不损失。
压力环境为 :服务端和压力发起机均为一台4C/8G的笔记本电脑(我自己的哈,键盘都不完整了!)。
为保证CPU和内存资源够用,对场景做了些限制,增加pacing设置,所以:LR场景设置为 :50VU并发,pacing=20毫秒(目标TPS=2500),运行时长为10分钟。
得到交易平均响应时间如下:即平均响应时间达到39毫秒(TPS均值为623),超过了我们pacing的控制值。
同时,在稳定阶段,发现后端挡板程序输出的处理时间平均不到1毫秒:如下图,图中Handle表示服务端接收post报文并返回模板配置信息的总体时间(单位毫秒),即纯服务端处理的时间;Delay表示服务端挡板配置的延迟时间(测试中延迟设置为0)。
很显然,客户端发送和收到响应的时间远远大于服务端处理的耗时。而脚本中web_enable_keep_alive()的是长连接,貌似不会在Action迭代中进行http重连了,所以是网络传输问题么?
2. 问题定位
通过观察网络发现,根本没有流量通过,因为这里服务端和客户端都在一台机器上,这也符合预期。所以,这里根本不是网络传输问题。
再通过netstat -ano|findstr 8080观察发现,客户端有大量连接在主动关闭(连接进入TIME_WAIT状态),同时有新的连接在ESTABLISHED,所以这个web_enable_keep_alive()好像和预期的不符啊(没有长连的作用),那么后面的post请求其实是每次迭代都要建立TCP连接的,不难想象客户端看到的耗时为什么占了交易时间的绝大部分。
后来注释掉init脚本中的web_enable_keep_alive()函数再次进行测试,看到的效果一样,由此看来此处这种用法是没有任何作用的。
其实,http连接是没有长短之分的,而我们http处理代码里面的一般的请求头中的keep-alive是对上层的TCP连接来说的,即:如果你要多次请求(在LR里的话,就是一个Action里多次使用web_url、web_custom_request、web_submit_data等函数)或请求的页面包含多个资源,比如一般的页面都含有图片资源,css文件,js文件等静态资源,这时keep-alive可以让你的若干请求和响应在同一个连接中去完成。我们这里LR脚本的本意是要一次连接,并keep alive然后通过Action迭代进行若干次post请求,可事与愿违——这个HTTP长连接是假的!
3.SK对比测试
因为我们手头上有自己的一款性能测试工具:SharpKnife(简称SK),所以经常会找机会把它与神坛上的东西对比。对同样的短连接,我做了个对比测试,结果让我对自己的工具“肃然起敬”(请允许我吹个牛哈!)。
对以上LR脚本在我们SK测试框架下需要实现的代码(脚本)如下(还需要增加apache的httpclient-4.5.2.jar和httpcore-4.4.5.jar):
import java.io.IOException;
import java.net.URI;
import java.util.Map;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.impl.client.BasicCOOKIEStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
//这是SK框架的代码接口
import perftest.sharpknife.inter.JVUser;
public class HttpMockerPostTest implements JVUser{
StringBuffer sb = new StringBuffer();
CloseableHttpResponse respOnse= null;
CloseableHttpClient httpclient = null;
BasicCOOKIEStore COOKIEStore = null;
public String postMessage(String mockerUrl,StringBuffer sb) {
byte[] cOntent= new byte[20480];
try{
HttpUriRequest postRequest = RequestBuilder.post()
.setUri(new URI(mockerUrl))
.setHeader("Content-Type","application/x-www-form-urlencoded")
.addParameter("", sb.toString())
.build();
respOnse= httpclient.execute(postRequest);
HttpEntity entity = response.getEntity();
entity.getContent().read(content);
EntityUtils.consume(entity);
}catch(Exception e){
e.printStackTrace();
} finally {
}
//调试时查看结果返回
//return (new String(content));
return null;
}
@Override
public int start(Map arg0, int arg1) {
COOKIEStore = new BasicCOOKIEStore();
httpclient = HttpClients.custom().setDefaultCOOKIEStore(COOKIEStore).build();
sb = new StringBuffer("{\""
+" "
+" "
//............此处省略n行字串组合
+" ");
return 0;
}
@Override
public String action(Map arg0, int arg1) {
return postMessage((String) arg0.get("param1"),sb);
}
@Override
public int exit(Map arg0, int arg1) {
try {
if(null!=response)
response.close();
if(null!=httpclient)
httpclient.close();
} catch (IOException e) {
e.printStackTrace();
}
sb = null;
return 0;
}
}
测试代码套路如下:
1.覆盖start(Map arg0, int arg1)方法。这相当于LR里面的init方法,用来初始化一些全局变量。
2.覆盖action(Map arg0, int arg1)方法。这相当于LR里面的Action.c的作用,用来进行业务迭代。
3.覆盖exit(Map arg0, int arg1)方法。这相当于LR里面的end部分,用来释放资源。
其中,arg0是参数集合,arg1是vu唯一标识号,与arg0结合使用,可以获取schedule调度文件中配置的特定参数。
把以上代码打包,放到SK工具的代理上,编辑一个调度schedule,即可运行压力测试。
本工具B/S版具体使用方法参考线上demo :http://105.115.108.120:6781/,此次测试是基于本工具的C/S版。
在同样是短连接的情况下(注意SK实现代码里post之前没有keep alive),SK工具代理端POST请求的响应时间平均是0.81毫秒(从响应时间上看,SK工具比LR提升约40倍,这是我前面提到“肃然起敬”的原因),相同的代理机压力发起能力为2566TPS,达到设定的预期值2500(50并发/0.02秒的迭代间隔时间=2500TPS)。
尽管SK工具的响应时间最高还是有比较高的(4毫秒算高么?),但效果对比非常明显:LR的HTTP短连接请求性能比apache httpclient在我们SK工具中实现的短连接请求差了很多。
LR短连接比不了,那么它能不能做到HTTP长连接呢?LR的http长连接肯定能提高性的,但这里如何做到呢?请继续阅读本文第4部分。
4. 对LR脚本的改进
根据HTTP长连接的本质可以看出,其目的是要实现一次连接,多次请求和回应,而根据上面的实验可以发现LR的Action的一次迭代是一个创建和关闭TCP连接的全过程,即每次的Action迭代是在不同的TCP连接里处理的HTTP请求和响应。
那么,如果在一个Action里去实现多次http请求就可以实现长连接的目的了。所以,对Aciton里面实现POST请求的web_custom_request()方法添加for循环就可以了。修改后Action.c代码如下:
Action()
{
int k = 0;
//web_reg_save_param("ALL","LB=","RB=",LAST);
for(k=0;k<10000000;k++){
lr_think_time(0.020);
lr_start_transaction(transname);
web_custom_request("httpmocker",...........,LAST);//此方法内内容同前
lr_end_transaction(transname, LR_AUTO);
}
//lr_output_message(lr_eval_string("{ALL}"));
return 0;
}
脚本中把循环次数设定为一个很大的整数,让循环来代替Action的迭代,从而使TCP连接不从客户端主动关闭。
下面截图是最初脚本和修改后脚本TCP连接的截图:
修改前,设置的长连接无效,客户端TCP连接端口在不断变化:
修改后,连接保持不变,如图:
修改后,同样并发压力场景的平均响应时间达到SK工具的水平(平均0.001秒即1毫秒,从图中可以看到实际是在0.8毫秒以上,略高于SK平均响应时间):
从TPS图像上看脚本修改后50并发的TPS均值接近2400(不算两头),低于拟设定的2500(这是LR不稳定造成的,即在全场景中还有很多响应时间超过了pacing值造成的误差,比如响应时间图中显示的最大值达到了366毫秒,而这个最大值受图像刻度的影响在响应时间图上却没有体现):
5.本文总结
下面通过几个问答来总结本文想要告诉大家的一些经验:
1. 测试中,服务器CPU等资源都还比较空闲,交易响应时间却比较长,为什么呢?
服务端资源空闲表明服务器处理能力没有饱和,不存在存储IO等问题,客户端看到的交易响应时间如果比较长的话,要考虑来回网络的延迟及网络连接的开启和关闭时间可能存在的问题,看客户端网络TCP层的缓冲是否够用。
2. 对LR的常见函数你深入考察了么?
LoadRunner性能测试工具对大部分协议进行了封装,方便了初级用户的使用,好多情况下你所从函数字面量理解功能可能并没有实现,要通过多种方法去验证。
3. 怎样识别客户端压力不足的问题?
压测过程中关注客户端的资源利用率和稳定性非常重要,一般压力机的CPU使用率不要超过70%,如果有文件读写的话磁盘平均响应时间应在15毫秒以下,网络延迟平均时间也不要超过10毫秒,如果达到40毫秒以上则你所处的网络环境很不理想。很多环境是使用虚拟机来发生压力的,在空闲状态下应关注虚拟机的稳定性,尽量使用Unix操作系统的虚拟机,如果是JDK那么应该选择64位的最新版。