压力测试不同于功能测试,软件的正确性并不是它的测试重点。它所看重的是软件的执行效率,尤其是短时间内访问用户数爆炸性增长时软件的响应速度,压力测试往往是在功能测试之后进行的。在实际的开发过程中,软件潜在的效率瓶颈一般都是那些可能有多个用户同时访问的节点。
就目前 Java EE 的平台下开发的软件来说,这种节点通常可能是:Web 服务器、数据库服务器和 JMS 服务器。它们都是请求主要发生的地点,请求频率较其它的节点要高,而且处于请求序列的关键路径之上。如果它们效率无法提高的话,对于整个软件的效率有致命的影响。而且在这些节点上一般都会发生较大规模的数据交换,有时其中还包含有业务逻辑处理,它们正是在进行压力测试时首先需要考虑的。
本文以这三种节点为例,介绍如何使用 JMeter 来完成针对于它们的压力测试。
Web 服务器
对于大多数的项目来说,并不会自行开发一个Web服务器,因此Web服务器压力测试的对象实际就是--发布到Web服务器中的软件。最简单的Web测试计划只需要三个 JMeter 的测试元件,如下图:
其中:
- 在线程组中定义线程数、产生线程发生的时间和测试循环次数。
- 在http请求中定义服务器、端口、协议和方法、请求路径等。
- 表格监听器负责收集和显示结果。
这种设置对于包含了安全机制的 web 应用是不够的,典型的 web 应用一般都会:
1. 有一个登录页,它是整个应用的入口。当用户登录之后,应用会将用户相关的安全信息放到 session 中。
2. 有一个 filter,它拦截请求,检查每个请求相关的 session 中是否包含有用户安全信息。如果没有,那么请求被重定向到登录页,要求用户提供安全信息。
在这种配置下应用上面的测试计划,那么除了登录页之外的其它请求都将因为缺少用户安全信息,而使请求实际定位到登录页。如果不加断言,那么在监听器看来所有的请求都是成功。而实际上,这些请求最终都没有到达它们应该去的地方。显然,这种测试结果不是我们所期望的。
为了成功的测试,至少有2种方法:
- 方法一,去掉程序的安全设置,如filter,使得不需要用户安全信息也能访问受限内容;
- 方法二,不修改程序,使用JMeter提供的"Http URL重写修饰符"或"Http COOKIE管理器"。
对于第一种方法,有其局限性:
- 需要修改程序配置,如去掉web.xml中关于安全filter的设置。需要维护多个版本的web.xml,如压力测试和功能测试分别各自的web.xml,增加了维护成本,而且有可能会在测试之后忘记将web.xml修改回来。
- 对于一些需要用户安全信息的页面无能为力,如某些业务审计操作需要用户安全信息来记录。因为缺少这样的信息,注定了测试的失败。如果解决为了这个问题进一步的修改程序,那么因为存在多个版本的程序,那么其维护难度将大大增加。
虽然,第二种方法配置难度增加了,但是它不用修改程序。而且还可将测试计划保存成文件,以便重复使用。因此,选用第二种方法是较为理想的做法。下面以一个简化的例子说明使用方法二的配置步骤。
1. 例子由以下几个文件组成:
-
AuthorizenFilter.java,过滤器负责检验session中是否存在用户信息。如果没有,那么就转向到 login.jsp。它的主要方法 doFilter 内容如下:
public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)throws IOException, ServletException {HttpServletRequest req = (HttpServletRequest)request;HttpServletResponse res = (HttpServletResponse)response;HttpSession session= req.getSession();User user = (User)session.getAttribute("user");if(null == user){String uri= req.getRequestURI();//如果请求页是登录页,不转向if( uri.equalsIgnoreCase("/gWeb/login.jsp")){chain.doFilter(request, response);} else{res.sendRedirect("/gWeb/login.jsp");}}else{chain.doFilter(request, response);}
}
-
User.java,用户类负责记录用户的信息。为了简化,这里的登录操作只允许指定用户名和密码。主要内容如下:
public class User {private String user;private String pwd;public User(String user, String pwd) {this.user = user;this.pwd = pwd;}public boolean login(){return user.equals("foxgem") && pwd.equals("12345678");}public String getUser() {return user;}public void setUser(String user) {this.user = user;}
}
-
Login.jsp 和welcome.jsp。其中 login.jsp 负责生成 User 对象,并调用 User 的login。当 login 返回为 true 时转向到 welcome.jsp。其验证部分的代码:
<%if( request.getParameter("Submit") !&#61; null) {User ur&#61; new User( request.getParameter("user"), request.getParameter("pwd"));if( ur.login()){session.setAttribute("user", ur);response.sendRedirect("/gWeb/welcome.jsp");} else{session.setAttribute( "LOGIN_ERROR_MSG",
"无效的用户&#xff0c;可能原因&#xff1a;用户不存在或被禁用。");response.sendRedirect("/gWeb/index.jsp");return;}}
%>
-
web.xml&#xff0c;配置 filter 拦截所有访问 JSP 页面的请求&#xff1a;
authorizen org.foxgem.jmeter.AuthorizenFilter authorizen *.jsp
2. 创建如下结构的Web测试计划&#xff1a;
其中主要测试元件说明如下&#xff1a;
- http请求默认值负责记录请求的默认值&#xff0c;如服务器、协议、端口等。
- 第一个http请求&#xff0c;请求login.jsp&#xff0c;并附加验证所需要的参数&#xff08;user&#61;foxgem&#xff0c;pwd&#61;12345678&#xff0c;Submit&#61;Submit&#xff09;&#xff1b;其包含的响应断言验证url中包含"welcome.jsp"&#xff0c;这一点可以从程序中反应。
- 第二个http请求&#xff0c;请求是welcome.jsp&#xff1b;其包含的响应断言验证响应文本中包含"foxgem"&#xff0c;它是welcome.jsp页面逻辑的一部分。
- http COOKIE管理器负责管理整个测试过程中使用的COOKIE&#xff0c;它不需要设置任何属性。
- 循环控制器设置发送第二个请求的循环次数&#xff0c;表格监听器负责收集和显示第二个请求的测试结果。
启动测试计划之后&#xff0c;执行的顺序是&#xff1a;首先&#xff0c;第一个请求登录页进行登录&#xff1b;成功登录之后&#xff0c;使用循环控制器执行第二个请求。请求welcome.jsp时&#xff0c;响应断言用来验证是否确实是welocme.jsp来处理请求&#xff0c;而不是因为其它页。在这个测试计划中需要注意的是http COOKIE管理器。正是由于它的作用&#xff0c;使得第二个请求能顺利的发送到welcome.jsp进行处理&#xff0c;而不是因为缺少用户安全信息转发到login.jsp。
在这个例子中&#xff0c;我们并没有在程序中使用COOKIE&#xff08;使用的是session&#xff09;&#xff0c;那么http COOKIE管理器怎么会起作用呢&#xff1f;这是因为在servlet/jsp规范中对于session的状态跟踪有2种方式&#xff1a;
- 使用COOKIE&#xff0c;保留和传递sessionid。它不要求程序对于url有什么特殊的处理&#xff0c;但是要求浏览器允许COOKIE。在这个例子中&#xff0c;就是这种情形。
- 使用url重写&#xff0c;每次显式的在浏览器和服务器之间传递sessionid。它要求程序对url进行编码&#xff0c;对浏览器没有要求。
对于第二种情形&#xff0c;可以使用JMeter前置管理器中的http url重写修饰符来完成。对于Tomcat&#xff0c;Session参数是jsessionid&#xff0c;路径扩展使用"&#xff1b;"。使用url编码时需要注意&#xff0c;必须将浏览器的COOKIE功能关闭。因为url编码函数&#xff0c;如encodeURL&#xff0c;会判断是否需要将sessionid编码到url中。当浏览器允许COOKIE时&#xff0c;就不会进行编码。
如果COOKIE而不是session来保存用户安全信息&#xff0c;那么直接使用http COOKIE管理器就行了。此时&#xff0c;需要将使用的COOKIE参数和值直接写到管理器中&#xff0c;由它负责管理。对于其它的COOKIE使用&#xff0c;也是如此操作。
登录问题解决之后&#xff0c;对于 Web 服务器的测试就没什么难点了。剩下的就是根据实际需要&#xff0c;灵活运用相关的测试组件搭建编写的测试计划。&#xff08;当然&#xff0c;对于安全问题还有其它的使用情景。在使用时需要明确&#xff1a;JMeter 是否支持&#xff0c;如果支持使用哪种测试组件解决。&#xff09;
数据库服务器
数据库服务器在大多数企业项目中是不可缺少的&#xff0c;对于它进行压力测试是为了找出&#xff1a;数据库对象是否可以有效地承受来自多个用户的访问。这些对象主要是&#xff1a;索引、触发器、存储过程和锁。通过对于SQL语句和存储过程的测试&#xff0c;JMeter 可以间接的反应数据库对象是否需要优化。
JMeter 使用 JDBC 发送请求&#xff0c;完成对于数据库的测试。一个数据库测试计划&#xff0c;建立如下结构即可&#xff1a;
其中&#xff1a;
- JDBC连接配置&#xff0c;负责配置数据库连接相关的信息。如&#xff1a;数据库url、数据库驱动类名、用户名和密码等等。在这些配置中&#xff0c;"绑定到池的变量名"&#xff08;Variable Name Bound to Pool&#xff09;是一个非常重要的属性&#xff0c;这个属性会在JDBC请求中被引用。通过它&#xff0c; JDBC请求和JDBC连接配置建立关联。&#xff08;测试前&#xff0c;请将所需要的数据库驱动放到JMeter的classpath中&#xff09;。
- JDBC请求&#xff0c;负责发送请求进行测试。
- 图形结果&#xff0c;收集显示测试结果。
在实际的项目中&#xff0c;至少有2种类型的JDBC请求需要关注&#xff1a;select语句和存储过程。前者反应了select语句是否高效&#xff0c;以及表的索引等是否需要优化&#xff1b;后者则是反应存储过程的算法是否高效。它们如果效率低下&#xff0c;必然会带来响应上的不尽如人意。对于这两种请求&#xff0c;JDBC请求的配置略有区别&#xff1a;
-
Select语句
-
存储过程
如果对于Oracle&#xff0c;如果测试的是函数&#xff0c;那么也可以使用select语句来进行配置&#xff0c;此时可以使用&#xff1a;select 函数(入参) from dual形式的语句来测试&#xff0c;其中dual是oracle的关键字&#xff0c;表示哑表。对于其它厂商的数据库产品&#xff0c;请查找手册。
JMS服务器
MOM 作为消息数据交换的平台&#xff0c;也是影响应用执行效率的潜在环节。在 Java 程序中&#xff0c;是通过 JMS 与 MOM 进行交互的。作为 Java 实现的压力测试工具&#xff0c;JMeter 也能使用 JMS 对应用的消息交换和相关的数据处理能力进行测试。这一点应该不难理解&#xff0c;因为在整个测试过程中&#xff0c;JMeter 测试的重点应该是消息的产生者和消费者的本身能力&#xff0c;而不是 MOM本身。
根据 JMS 规范&#xff0c;消息交换有2种方式&#xff1a;发布/订阅和点对点。JMeter针对这两种情形&#xff0c;分别提供了不同的Sampler进行支持。以下MOM我们使用ActiveMQ 3.2.1&#xff0c;分别描述这两种消息交换方式是如何使用 JMeter 进行测试。
1. 测试前的准备&#xff08;两种情况都适用&#xff09;
JMeter 虽然能使用 JMS 对 MOM 进行测试&#xff0c;但是它本身并没有提供JMS需要使用的包。因此&#xff0c;在测试之前需要将这些包复制到 %JMETER_HOME%/lib 下。对于 ActiveMQ 来说&#xff0c;就是复制 %ACTIVEMQ_HOME%/lib。%ACTIVEMQ_HOME%/optional 是可选包&#xff0c;可根据实际情况来考虑是否复制。
JMeter 在测试时使用了 JNDI&#xff0c;为了提供 JNDI 提供者的信息&#xff0c;需要提供 jndi.properties。同时需要将 jndi.properties 放到 JMeter 的 classpath 中&#xff0c;建议将它与 bin下的 ApacheJMeter.jar 打包在一起。对于 ActiveMQ&#xff0c;jndi.properties 的示例内容如下&#xff1a;
java.naming.factory.initial &#61; org.activemq.jndi.ActiveMQInitialContextFactory |
2. 发布/订阅
在实际测试时&#xff0c;发布者和订阅者并不是需要同时出现的。例如&#xff0c;有时我们可能想测试单位时间内消息发布者的消息产生量&#xff0c;此时就不需要消息发布者&#xff0c;只需要订阅者就可以了。本例为了说明这两种Sampler的使用&#xff0c;因此建立如下的测试计划&#xff1a;
其中JMS Publisher和JMS Subscriber的属性&#xff1a;选择"使用jndi.properties"&#xff0c;连接工厂是connectionFactory&#xff0c;主题是MyTopic&#xff0c;其它使用默认配置。对于JMS Publisher&#xff0c;还需提供测试用的文本消息。
启动ActiveMQ&#xff0c;运行测试计划。如果配置正确&#xff0c;那么与ActiveMQ成功连接之后&#xff0c;在JMeter的后台会打印出相关信息。在测试过程中&#xff0c;JMeter 后台打印可能会出现java.lang.InterruptedException 信息&#xff0c;这个是正常现象&#xff0c;不会影响测试过程和结果。这一点可以从 bin 下的 jmeter.log 看出。
3. 点对点
对于点对点&#xff0c;JMeter只提供了一种Sampler&#xff1a;JMS Point-to-Point。在例子中&#xff0c;建立如下图的测试计划&#xff1a;
其中&#xff1a;Communication style是Request Only。对于另一种风格&#xff1a;Request Response&#xff0c;会验证收到消息的JMS Header中的JMSCorrelationID&#xff0c;以判断是否是对请求消息的响应。