性能调优
性能测试的目标
性能测试不同于功能测试,不是对与错的检验,而是快与慢的衡量。在进行真正的性能测试之前要先搞清楚目标:
1. 在确定的硬件条件下,可以支持的并发数越大越好,响应时间越快越好。具体需要达到的并发数是多大,要求的响应时间是多快,由产品经理来提出。
2. 在确定的硬件条件下,测试得到最大并发数和相应的响应时间之后。如果增加硬件投入,可以得到怎样的性能提升回报? (系统扩展性和伸缩性测试,Scalability)
这里的硬件条件包括:cpu,memery,I/O,network bandwidth。
性能测试中的基准测试 Benchmarking
与功能测试相似,性能测试也要设计测试用例,不同的是在正式开始你的业务测试用例之前你要先进行一下基准测试。为什么呢?其实就是先要量一下你的硬件的能力,不然,如果你的测试结果不好,你怎么知道是硬件慢还是你的软件的问题。这些硬件测试包括:
1. 网络带宽测试, 你可以通过copy大文件的方式测试你的网络的最大带宽是多少。
2. cpu,你可以利用比较复杂的算法来衡量cpu的快慢
3. memery,这个不用测试,你知道memery的大小
4. IO, 也可以通过copy大文件来测试
这些基准测试用例在后面的调优过程中,还可以用来衡量你修改之后真的变好了吗。
设计你的业务测试用例
比较理想的测试用例就是要尽可能模仿真实世界的情况,这往往做不到,尤其是对于新产品来说。你可以先录制一些用户最常用,最典型的case作为起点。
另外,对于并发的概念需要搞清楚。并发用户,通常是指同时在线的用户,这些用户可以能在用你的系统的不同的功能,注意并不是说大家都在做同一件事情。对某一个事务并发请求是指某一个request的并发调用。
对于后一种并发,你往往需要计算在用户量最大的时候,大概大家都集中的在干哪一件事情,这个请求一定要够快才好。
设计好这两种测试用例以后,在后面的调优过程中,他们就成了衡量你的改进的成效的衡量的标尺。
性能调优
性能调优要从底层开始,基本上要从OS开始,到JVM,Cache,Buffer Pool, SQL,DB Schema, 算法。
一次不要改的太多,改一点,测一下,这可是个慢功夫,需要有耐心。
在执行测试的时候还要注意,要遵循相同的过程,系统需要在重启之后先热身再开始真正的测试,不然你会发现你的测试结果很不一样,琢磨不定。
还有,要注意你的客户端的能力,比如JMeter,很需要内存,别因为客户端不行,误以为是你的系统的问题,那就太乌龙了。
在测试调优的时候,需要借助一些监控工具比如JConsole,来监控系统的状况,找到系统的瓶颈,所谓瓶颈,就是最慢的那个部分,也常表现为100%被占满。比如你的内存或者cpu被用尽了。如果cpu和内存还没有用尽,说明他们在等某个资源。这时候需要用profile工具去寻找,比如JProfile,YourKit。
利用性能监控日志
因为性能的问题不是很容易重现,当product环境中遇到性能问题的时候,如果是数据的问题,也许当你把product 数据copy到你的测试环境中,就能重现比较慢点查询,加以改进。但是如果是并发用户或者网络等运行时环境的问题,你就很难重现。这时,如果你能通过日志看到那些关键的响应慢的方法,也许可以帮助你快点找到问题所在。下面的代码可以帮你做到这一点,仅供参考:
- import org.slf4j.Logger;
- public class TraceUtil {
- final Logger logger;
- final long threshold = 1000;
- private long begin;
- private long offtime = 0;
- private String threadInfo;
- private String targetId;
- public TraceUtil(Logger logger, Thread thread, String targetId, long begin) {
- this.logger = logger;
- this.threadInfo = thread.getId() + "-" + thread.toString();
- this.targetId = targetId;
- this.begin = begin;
- }
- public void trace(String targetEvent) {
- long duration = System.currentTimeMillis() - begin;
- long increment = duration - offtime;
- offtime = duration;
- float percentage = (float) increment / (float) duration * 100;
- if (duration > threshold && percentage > 20) {
- logger.error(
- "Response time is too large: [{}], {}/{} ({}), {}, {}",
- new String[] { threadInfo + "", increment + "",
- duration + "", percentage + "%", targetEvent,
- targetId });
- }
- }
- }
利用JVM的MXBean找到blocked的点
当你发现JVM占用的cpu很高,而且响应时间比较慢,很可能是被IO或者网络等慢速设备拖住了。也有可能是你的方法中某个同步点(同步方法或者对象)成为性能的瓶颈。这时候你可以利用JVM提供的monitor API来监控:
- <%@ page import="java.lang.management.*, java.util.*" %>
- <%!
- Map cpuTimes = new HashMap();
- Map cpuTimeFetch = new HashMap();
- %>
- <%
- out.println("Threads Monitoring");
- long cpus = Runtime.getRuntime().availableProcessors();
- ThreadMXBean threads = ManagementFactory.getThreadMXBean();
- threads.setThreadContentionMonitoringEnabled(true);
- long now = System.currentTimeMillis();
- ThreadInfo[] t = threads.dumpAllThreads(false, false);
- for (int i = 0; i < t.length; i++) {
- long id = t[i].getThreadId();
- Long idObj = new Long(id);
- long current = 0;
- if (cpuTimes.get(idObj) != null) {
- long prev = ((Long) cpuTimes.get(idObj)).longValue();
- current = threads.getThreadCpuTime(t[i].getThreadId());
- long catchTime = ((Long) cpuTimeFetch.get(idObj)).longValue();
- double percent = (double)(current - prev) / (double)((now - catchTime) * cpus * 1000);
- if (percent > 0 && prev > 0) {
- out.println("<li>" + t[i].getThreadName()+"#"+t[i].getThreadId() + " Time: " + percent + " (" + prev + ", " + current + ") ");
- String locked = t[i].getLockInfo()==null?"":t[i].getLockInfo().getClassName();
- out.println(" Blocked: (" + t[i].getBlockedTime() + ", " + t[i].getBlockedCount() + ", " + locked + ")</li>");
- }
- }
- cpuTimes.put(idObj, new Long(current));
- cpuTimeFetch.put(idObj, new Long(now));
- }
- %>
同步是性能的一大瓶颈
通过监控发现,大量线程block在一个同步方法上,这样cpu也使不上劲。当你发现性能上不去,IO和网络等慢速设备也不是问题的时候,你就得检查一下是否在某个关键点上使用了同步(synchronizae)。有时候也许是你应用的第三方的jar里面的某个方法是同步的,这种情况下,你就很难找到问题所在。只能在编写代码的时候看一下你引用的方法是否是同步的。