摘要
改善Java服务器的性能需要模拟负载下的服务器。创建一个模拟环境、搜集数据并且分析结果可能是对许多开发人员的挑战。这篇文章中的示例介绍了JAVA服务器性能分析的概念和工具。作者使用这个示例来研究超额请求次数下内存使用和同步竟争的影响。
作者Ivan Small
项目团队已经很熟悉如何组织一些具体的任务并完成他们。简单的性能问题很轻易由一个开发人员分离并解决。然而大的性能问题,通常在系统处于高负载情况下发生,就不是这么简单能处理的了。这些问题需要一个独立的测试环境、一个模拟的负载,并且需要仔细地分析和跟踪。
在这篇文章中,我使用比较通用的工具和设备创建了一个测试环境。我会专注于两个性能问题,内存和同步,他们很难用简单的分析得到。通过一个具体的例子,我希望比较轻易地解决复杂的性能问题而且可以提供处理问题过程中的细节。
改善服务器的性能
服务器的性能改善是依靠于数据的。没有可靠的数据基础而更改应用或环境会导致更差的结果。分析器提供有用的JAVA服务器应用信息,但由于从单用户负载下的数据与多用户负载下得到的数据是完全不同的,这导致分析器的数据并不精确。在开发阶段使用分析器来优化应用的性能是一个好的方式,但在高负载下的应用分析可以取到更好的效果。
在负载下分析服务器应用的性能需要一些基本的元素:
1、可控的进行应用负载测试的环境。
2、可控的人造负载使得应用满负荷运行。
3、来自监视器、应用和负载测试工具自身的数据搜集。
4、性能改变的跟踪。
不要低估最后一个需求(性能跟踪)的重要性因为假如不能跟踪性能你就不能实际的治理项目。性能上10-20%的改善对单用户环境来说并没有什么不同,但对支持人员来说就不一样了。20%的改善是非常大的,而且通过跟踪性能的改善,你可以提供重要的反馈和持续跟踪。
虽然性能跟踪很重要,但有时为了使后续的测试更加精确而不得不抛弃先前的测试结果。在性能测试中,改善负载测试的精确性可能需要修改模拟环境,而这些变化是必须的,通过变化前后的负载测试你可以观察到其中的转变。
可控的环境
可控的环境最少也需要两台独立的机器和第三台控制的机器。其中一台用来生成负载,另一台作为控制机与前一台建立测试应用并接受反馈,第三台机器运行应用。此外,负载和应用机器间的网络应该与局域网分开。控制机接受运行应用机器的反馈如操作系统、硬件使用率、应用(非凡是VM)的状态。
负载模拟
最精确的模拟通常用实际的用户数据和WEB服务器端的访问日志。假如你还没有实际布署或者缺少实际的用户数据,你可以通过构造类似的场景或询问销售和产品治理团队或做一些有依据的猜想。协调负载测试和实际用户体验是一个持续的过程。
在模拟中一些用户场景是必须的。如在一个通用地址薄应用中,你应该区分更新和查询操作。在我的测试应用中GrinderServlet类只有一个场景。单用户连接10次访问这个servlet(在每一次访问间有一段暂停)。虽然这个应用很小,我认为这可以重复一些常见的东西。用户通常不会连接给服务器请求而没有间断。假如没有间断,我们可能不能得到更精确的实际用户上限。
串行10个请求的另一个原因是实际应用中不会只有一个HTTP请求。单一而又分离的请求可以影响环境中的许多因素。对Tomcat来说,会为每一个请求创建一个会话,并且HTTP协议答应不同的请求重用连接。我会修改一下负载测试来避免混洧。
GrinderServlet类不会执行任何排序操作,但这个需求在大部分应用中都很普通。在这些应用中,你需要创建模拟的数据集并且用他们来构造相关用例的负载测试。
例如,假如用例涉及到用户登录一个WEB应用,从可能的用户列表中选取随机的用户会只使用一个用户更精确。否则,你可能不经意地使用了系统缓存或其他的优化或一些微妙的东西,而这会使得结果不正确。
负载测试软件
负载测试软件可以构造测试场景并且对服务进行负载测试。我会在下面的示例中使用OpenSTA测试软件。这软件简单易学,结果也很轻易导出,并且支持参数化脚本,还可以监视信息的变化,他的主要缺点是基于Windows,但在这儿不是个问题。当然还有很多可选项如Apache的JMeter和Mercury的LoadRunner。
The GrinderServlet
列表1中显示了GrinderServlet类,列表2中显示了Grinder类
Listing 1
package pub.capart;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class GrindServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
Grinderv1 grinder = Grinderv1.getGrinder();
long t1 = System.currentTimeMillis();
grinder.grindCPU(13);
long t2 = System.currentTimeMillis();
PrintWriter pw = res.getWriter();
pw.print("<Html>\n< body> \n");
pw.print("Grind Time = "+(t2-t1));
pw.print("< body> \n< /html> \n");
}
}
Listing 2
package pub.capart;
/**
* This is a simple class designed to simulate an application consuming
* CPU, memory, and contending for a synchronization lock.
*/
public class Grinderv1 {
private static Grinderv1 singleton = new Grinderv1();
private static final String randstr =
"this is just a random string that I'm going to add up many many times";
public static Grinderv1 getGrinder() {
return singleton;
}
public synchronized void grindCPU(int level) {
StringBuffer sb = new StringBuffer();
String s = randstr;
for (int i=0;i<level;++i) {
sb.append(s);
s = getReverse(sb.toString());
}
}
public String getReverse(String s) {
StringBuffer sb = new StringBuffer(s);
sb = sb.reverse();
return sb.toString();
}
}
类很简单,但他们会产生两个很常见的问题。咋一看瓶颈可能由grindCPU()方法的同步修饰符引起,但实际上内存消耗才是真正的问题所在。如图1,我的第一个负载测试显示了常见的负载变化。在这里负载变化很重要因为你正在模拟一个高的负载。这种热身的方式也更精确因为避免了jsp编译引起的问题。我通常习惯于在进行负载测试前先进行单用户模拟。